From 23acb5bf8362d6af52c0297a13bd935eb8e7f7b1 Mon Sep 17 00:00:00 2001
From: stephannn <38951729+stephannn@users.noreply.github.com>
Date: Mon, 13 Oct 2025 13:07:33 +0200
Subject: [PATCH 1/8] Add files via upload
---
db.js | 8444 +++++++++++++++---------------
meshagent.js | 4416 ++++++++--------
meshcentral-config-schema.json | 5 +
meshcentral.js | 8870 ++++++++++++++++----------------
meshctrl.js | 6360 +++++++++++------------
5 files changed, 14084 insertions(+), 14011 deletions(-)
diff --git a/db.js b/db.js
index faebf9a487..bde1afa76a 100644
--- a/db.js
+++ b/db.js
@@ -1,4213 +1,4231 @@
-/**
-* @description MeshCentral database module
-* @author Ylian Saint-Hilaire
-* @copyright Intel Corporation 2018-2022
-* @license Apache-2.0
-* @version v0.0.2
-*/
-
-/*xjslint node: true */
-/*xjslint plusplus: true */
-/*xjslint maxlen: 256 */
-/*jshint node: true */
-/*jshint strict: false */
-/*jshint esversion: 6 */
-"use strict";
-
-//
-// Construct Meshcentral database object
-//
-// The default database is NeDB
-// https://github.com/louischatriot/nedb
-//
-// Alternativety, MongoDB can be used
-// https://www.mongodb.com/
-// Just run with --mongodb [connectionstring], where the connection string is documented here: https://docs.mongodb.com/manual/reference/connection-string/
-// The default collection is "meshcentral", but you can override it using --mongodbcol [collection]
-//
-module.exports.CreateDB = function (parent, func) {
- var obj = {};
- var Datastore = null;
- var expireEventsSeconds = (60 * 60 * 24 * 20); // By default, expire events after 20 days (1728000). (Seconds * Minutes * Hours * Days)
- var expirePowerEventsSeconds = (60 * 60 * 24 * 10); // By default, expire power events after 10 days (864000). (Seconds * Minutes * Hours * Days)
- var expireServerStatsSeconds = (60 * 60 * 24 * 30); // By default, expire server stats after 30 days (2592000). (Seconds * Minutes * Hours * Days)
- const common = require('./common.js');
- const path = require('path');
- const fs = require('fs');
- const DB_NEDB = 1, DB_MONGOJS = 2, DB_MONGODB = 3,DB_MARIADB = 4, DB_MYSQL = 5, DB_POSTGRESQL = 6, DB_ACEBASE = 7, DB_SQLITE = 8;
- const DB_LIST = ['None', 'NeDB', 'MongoJS', 'MongoDB', 'MariaDB', 'MySQL', 'PostgreSQL', 'AceBase', 'SQLite']; //for the info command
- let databaseName = 'meshcentral';
- let datapathParentPath = path.dirname(parent.datapath);
- let datapathFoldername = path.basename(parent.datapath);
- const SQLITE_AUTOVACUUM = ['none', 'full', 'incremental'];
- const SQLITE_SYNCHRONOUS = ['off', 'normal', 'full', 'extra'];
- obj.sqliteConfig = {
- maintenance: '',
- startupVacuum: false,
- autoVacuum: 'full',
- incrementalVacuum: 100,
- journalMode: 'delete',
- journalSize: 4096000,
- synchronous: 'full',
- };
- obj.performingBackup = false;
- const BACKUPFAIL_ZIPCREATE = 0x0001;
- const BACKUPFAIL_ZIPMODULE = 0x0010;
- const BACKUPFAIL_DBDUMP = 0x0100;
- obj.backupStatus = 0x0;
- obj.newAutoBackupFile = null;
- obj.newDBDumpFile = null;
- obj.identifier = null;
- obj.dbKey = null;
- obj.dbRecordsEncryptKey = null;
- obj.dbRecordsDecryptKey = null;
- obj.changeStream = false;
- obj.pluginsActive = ((parent.config) && (parent.config.settings) && (parent.config.settings.plugins != null) && (parent.config.settings.plugins != false) && ((typeof parent.config.settings.plugins != 'object') || (parent.config.settings.plugins.enabled != false)));
- obj.dbCounters = {
- fileSet: 0,
- fileRemove: 0,
- powerSet: 0,
- eventsSet: 0
- }
-
- // MongoDB bulk operations state
- if (parent.config.settings.mongodbbulkoperations) {
- // Added counters
- obj.dbCounters.fileSetPending = 0;
- obj.dbCounters.fileSetBulk = 0;
- obj.dbCounters.fileRemovePending = 0;
- obj.dbCounters.fileRemoveBulk = 0;
- obj.dbCounters.powerSetPending = 0;
- obj.dbCounters.powerSetBulk = 0;
- obj.dbCounters.eventsSetPending = 0;
- obj.dbCounters.eventsSetBulk = 0;
-
- /// Added bulk accumulators
- obj.filePendingGet = null;
- obj.filePendingGets = null;
- obj.filePendingRemove = null;
- obj.filePendingRemoves = null;
- obj.filePendingSet = false;
- obj.filePendingSets = null;
- obj.filePendingCb = null;
- obj.filePendingCbs = null;
- obj.powerFilePendingSet = false;
- obj.powerFilePendingSets = null;
- obj.powerFilePendingCb = null;
- obj.powerFilePendingCbs = null;
- obj.eventsFilePendingSet = false;
- obj.eventsFilePendingSets = null;
- obj.eventsFilePendingCb = null;
- obj.eventsFilePendingCbs = null;
- }
-
- obj.SetupDatabase = function (func) {
- // Check if the database unique identifier is present
- // This is used to check that in server peering mode, everyone is using the same database.
- obj.Get('DatabaseIdentifier', function (err, docs) {
- if (err != null) { parent.debug('db', 'ERROR (Get DatabaseIdentifier): ' + err); }
- if ((err == null) && (docs.length == 1) && (docs[0].value != null)) {
- obj.identifier = docs[0].value;
- } else {
- obj.identifier = Buffer.from(require('crypto').randomBytes(48), 'binary').toString('hex');
- obj.Set({ _id: 'DatabaseIdentifier', value: obj.identifier });
- }
- });
-
- // Load database schema version and check if we need to update
- obj.Get('SchemaVersion', function (err, docs) {
- if (err != null) { parent.debug('db', 'ERROR (Get SchemaVersion): ' + err); }
- var ver = 0;
- if ((err == null) && (docs.length == 1)) { ver = docs[0].value; }
- if (ver == 1) { console.log('This is an unsupported beta 1 database, delete it to create a new one.'); process.exit(0); }
-
- // TODO: Any schema upgrades here...
- obj.Set({ _id: 'SchemaVersion', value: 2 });
-
- func(ver);
- });
- };
-
- // Perform database maintenance
- obj.maintenance = function () {
- parent.debug('db', 'Entering database maintenance');
- if (obj.databaseType == DB_NEDB) { // NeDB will not remove expired records unless we try to access them. This will force the removal.
- obj.eventsfile.remove({ time: { '$lt': new Date(Date.now() - (expireEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
- obj.powerfile.remove({ time: { '$lt': new Date(Date.now() - (expirePowerEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
- obj.serverstatsfile.remove({ time: { '$lt': new Date(Date.now() - (expireServerStatsSeconds * 1000)) } }, { multi: true }); // Force delete older events
- } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) { // MariaDB or MySQL
- sqlDbQuery('DELETE FROM events WHERE time < ?', [new Date(Date.now() - (expireEventsSeconds * 1000))], function (doc, err) { }); // Delete events older than expireEventsSeconds
- sqlDbQuery('DELETE FROM power WHERE time < ?', [new Date(Date.now() - (expirePowerEventsSeconds * 1000))], function (doc, err) { }); // Delete events older than expirePowerSeconds
- sqlDbQuery('DELETE FROM serverstats WHERE expire < ?', [new Date()], function (doc, err) { }); // Delete events where expiration date is in the past
- sqlDbQuery('DELETE FROM smbios WHERE expire < ?', [new Date()], function (doc, err) { }); // Delete events where expiration date is in the past
- } else if (obj.databaseType == DB_ACEBASE) { // AceBase
- //console.log('Performing AceBase maintenance');
- obj.file.query('events').filter('time', '<', new Date(Date.now() - (expireEventsSeconds * 1000))).remove().then(function () {
- obj.file.query('stats').filter('time', '<', new Date(Date.now() - (expireServerStatsSeconds * 1000))).remove().then(function () {
- obj.file.query('power').filter('time', '<', new Date(Date.now() - (expirePowerEventsSeconds * 1000))).remove().then(function () {
- //console.log('AceBase maintenance done');
- });
- });
- });
- } else if (obj.databaseType == DB_SQLITE) { // SQLite3
- //sqlite does not return rows affected for INSERT, UPDATE or DELETE statements, see https://www.sqlite.org/pragma.html#pragma_count_changes
- obj.file.serialize(function () {
- obj.file.run('DELETE FROM events WHERE time < ?', [new Date(Date.now() - (expireEventsSeconds * 1000))]);
- obj.file.run('DELETE FROM power WHERE time < ?', [new Date(Date.now() - (expirePowerEventsSeconds * 1000))]);
- obj.file.run('DELETE FROM serverstats WHERE expire < ?', [new Date()]);
- obj.file.run('DELETE FROM smbios WHERE expire < ?', [new Date()]);
- obj.file.exec(obj.sqliteConfig.maintenance, function (err) {
- if (err) {console.log('Maintenance error: ' + err.message)};
- if (parent.config.settings.debug) {
- sqliteGetPragmas(['freelist_count', 'page_size', 'page_count', 'cache_size' ], function (pragma, pragmaValue) {
- parent.debug('db', 'SQLite Maintenance: ' + pragma + '=' + pragmaValue);
- });
- };
- });
- });
- }
- obj.removeInactiveDevices();
- }
-
- // Remove inactive devices
- obj.removeInactiveDevices = function (showall, cb) {
- // Get a list of domains and what their inactive device removal setting is
- var removeInactiveDevicesPerDomain = {}, minRemoveInactiveDevicesPerDomain = {}, minRemoveInactiveDevice = 9999;
- for (var i in parent.config.domains) {
- if (typeof parent.config.domains[i].autoremoveinactivedevices == 'number') {
- var v = parent.config.domains[i].autoremoveinactivedevices;
- if ((v >= 1) && (v <= 2000)) {
- if (v < minRemoveInactiveDevice) { minRemoveInactiveDevice = v; }
- removeInactiveDevicesPerDomain[i] = v;
- minRemoveInactiveDevicesPerDomain[i] = v;
- }
- }
- }
-
- // Check if any device groups have a inactive device removal setting
- for (var i in parent.webserver.meshes) {
- if (typeof parent.webserver.meshes[i].expireDevs == 'number') {
- var v = parent.webserver.meshes[i].expireDevs;
- if ((v >= 1) && (v <= 2000)) {
- if (v < minRemoveInactiveDevice) { minRemoveInactiveDevice = v; }
- if ((minRemoveInactiveDevicesPerDomain[parent.webserver.meshes[i].domain] == null) || (minRemoveInactiveDevicesPerDomain[parent.webserver.meshes[i].domain] > v)) {
- minRemoveInactiveDevicesPerDomain[parent.webserver.meshes[i].domain] = v;
- }
- } else {
- delete parent.webserver.meshes[i].expireDevs;
- }
- }
- }
-
- // If there are no such settings for any domain, we can exit now.
- if (minRemoveInactiveDevice == 9999) { if (cb) { cb("No device removal policy set, nothing to do."); } return; }
- const now = Date.now();
-
- // For each domain with a inactive device removal setting, get a list of last device connections
- for (var domainid in minRemoveInactiveDevicesPerDomain) {
- obj.GetAllTypeNoTypeField('lastconnect', domainid, function (err, docs) {
- if ((err != null) || (docs == null)) return;
- for (var j in docs) {
- const days = Math.floor((now - docs[j].time) / 86400000); // Calculate the number of inactive days
- var expireDays = -1;
- if (removeInactiveDevicesPerDomain[docs[j].domain]) { expireDays = removeInactiveDevicesPerDomain[docs[j].domain]; }
- const mesh = parent.webserver.meshes[docs[j].meshid];
- if (mesh && (typeof mesh.expireDevs == 'number')) { expireDays = mesh.expireDevs; }
- var remove = false;
- if (expireDays > 0) {
- if (expireDays < days) { remove = true; }
- if (cb) { if (showall || remove) { cb(docs[j]._id.substring(2) + ', ' + days + ' days, expire ' + expireDays + ' days' + (remove ? ', removing' : '')); } }
- if (remove) {
- // Check if this device is connected right now
- const nodeid = docs[j]._id.substring(2);
- const conn = parent.GetConnectivityState(nodeid);
- if (conn == null) {
- // Remove the device
- obj.Get(nodeid, function (err, docs) {
- if (err != null) return;
- if ((docs == null) || (docs.length != 1)) { obj.Remove('lc' + nodeid); return; } // Remove last connect time
- const node = docs[0];
-
- // Delete this node including network interface information, events and timeline
- obj.Remove(node._id); // Remove node with that id
- obj.Remove('if' + node._id); // Remove interface information
- obj.Remove('nt' + node._id); // Remove notes
- obj.Remove('lc' + node._id); // Remove last connect time
- obj.Remove('si' + node._id); // Remove system information
- obj.Remove('al' + node._id); // Remove error log last time
- if (obj.RemoveSMBIOS) { obj.RemoveSMBIOS(node._id); } // Remove SMBios data
- obj.RemoveAllNodeEvents(node._id); // Remove all events for this node
- obj.removeAllPowerEventsForNode(node._id); // Remove all power events for this node
- if (typeof node.pmt == 'string') { obj.Remove('pmt_' + node.pmt); } // Remove Push Messaging Token
- obj.Get('ra' + node._id, function (err, nodes) {
- if ((nodes != null) && (nodes.length == 1)) { obj.Remove('da' + nodes[0].daid); } // Remove diagnostic agent to real agent link
- obj.Remove('ra' + node._id); // Remove real agent to diagnostic agent link
- });
-
- // Remove any user node links
- if (node.links != null) {
- for (var i in node.links) {
- if (i.startsWith('user/')) {
- var cuser = parent.webserver.users[i];
- if ((cuser != null) && (cuser.links != null) && (cuser.links[node._id] != null)) {
- // Remove the user link & save the user
- delete cuser.links[node._id];
- if (Object.keys(cuser.links).length == 0) { delete cuser.links; }
- obj.SetUser(cuser);
-
- // Notify user change
- var targets = ['*', 'server-users', cuser._id];
- var event = { etype: 'user', userid: cuser._id, username: cuser.name, action: 'accountchange', msgid: 86, msgArgs: [cuser.name], msg: 'Removed user device rights for ' + cuser.name, domain: node.domain, account: parent.webserver.CloneSafeUser(cuser) };
- if (obj.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
- parent.DispatchEvent(targets, obj, event);
- }
- } else if (i.startsWith('ugrp/')) {
- var cusergroup = parent.userGroups[i];
- if ((cusergroup != null) && (cusergroup.links != null) && (cusergroup.links[node._id] != null)) {
- // Remove the user link & save the user
- delete cusergroup.links[node._id];
- if (Object.keys(cusergroup.links).length == 0) { delete cusergroup.links; }
- obj.Set(cusergroup);
-
- // Notify user change
- var targets = ['*', 'server-users', cusergroup._id];
- var event = { etype: 'ugrp', ugrpid: cusergroup._id, name: cusergroup.name, desc: cusergroup.desc, action: 'usergroupchange', links: cusergroup.links, msgid: 163, msgArgs: [node.name, cusergroup.name], msg: 'Removed device ' + node.name + ' from user group ' + cusergroup.name };
- if (obj.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
- parent.DispatchEvent(targets, obj, event);
- }
- }
- }
- }
-
- // Event node deletion
- var meshname = '(unknown)';
- if ((parent.webserver.meshes[node.meshid] != null) && (parent.webserver.meshes[node.meshid].name != null)) { meshname = parent.webserver.meshes[node.meshid].name; }
- var event = { etype: 'node', action: 'removenode', nodeid: node._id, msgid: 87, msgArgs: [node.name, meshname], msg: 'Removed device ' + node.name + ' from device group ' + meshname, domain: node.domain };
- // TODO: We can't use the changeStream for node delete because we will not know the meshid the device was in.
- //if (obj.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to remove the node. Another event will come.
- parent.DispatchEvent(parent.webserver.CreateNodeDispatchTargets(node.meshid, node._id), obj, event);
- });
- }
- }
- }
- }
- });
- }
- }
-
- // Remove all reference to a domain from the database
- obj.removeDomain = function (domainName, func) {
- var pendingCalls;
- // Remove all events, power events and SMBIOS data from the main collection. They are all in seperate collections now.
- if (obj.databaseType == DB_ACEBASE) {
- // AceBase
- pendingCalls = 3;
- obj.file.query('meshcentral').filter('domain', '==', domainName).remove().then(function () { if (--pendingCalls == 0) { func(); } });
- obj.file.query('events').filter('domain', '==', domainName).remove().then(function () { if (--pendingCalls == 0) { func(); } });
- obj.file.query('power').filter('domain', '==', domainName).remove().then(function () { if (--pendingCalls == 0) { func(); } });
- } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL) || (obj.databaseType == DB_POSTGRESQL)) {
- // MariaDB, MySQL or PostgreSQL
- pendingCalls = 2;
- sqlDbQuery('DELETE FROM main WHERE domain = $1', [domainName], function () { if (--pendingCalls == 0) { func(); } });
- sqlDbQuery('DELETE FROM events WHERE domain = $1', [domainName], function () { if (--pendingCalls == 0) { func(); } });
- } else if (obj.databaseType == DB_MONGODB) {
- // MongoDB
- pendingCalls = 3;
- obj.file.deleteMany({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
- obj.eventsfile.deleteMany({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
- obj.powerfile.deleteMany({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
- } else {
- // NeDB or MongoJS
- pendingCalls = 3;
- obj.file.remove({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
- obj.eventsfile.remove({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
- obj.powerfile.remove({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
- }
- }
-
- obj.cleanup = function (func) {
- // TODO: Remove all mesh links to invalid users
- // TODO: Remove all meshes that dont have any links
-
- // Remove all events, power events and SMBIOS data from the main collection. They are all in seperate collections now.
- if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL) || (obj.databaseType == DB_POSTGRESQL)) {
- // MariaDB, MySQL or PostgreSQL
- obj.RemoveAllOfType('event', function () { });
- obj.RemoveAllOfType('power', function () { });
- obj.RemoveAllOfType('smbios', function () { });
- } else if (obj.databaseType == DB_MONGODB) {
- // MongoDB
- obj.file.deleteMany({ type: 'event' }, { multi: true });
- obj.file.deleteMany({ type: 'power' }, { multi: true });
- obj.file.deleteMany({ type: 'smbios' }, { multi: true });
- } else if ((obj.databaseType == DB_NEDB) || (obj.databaseType == DB_MONGOJS)) {
- // NeDB or MongoJS
- obj.file.remove({ type: 'event' }, { multi: true });
- obj.file.remove({ type: 'power' }, { multi: true });
- obj.file.remove({ type: 'smbios' }, { multi: true });
- }
-
- // List of valid identifiers
- var validIdentifiers = {}
-
- // Load all user groups
- obj.GetAllType('ugrp', function (err, docs) {
- if (err != null) { parent.debug('db', 'ERROR (GetAll user): ' + err); }
- if ((err == null) && (docs.length > 0)) {
- for (var i in docs) {
- // Add this as a valid user identifier
- validIdentifiers[docs[i]._id] = 1;
- }
- }
-
- // Fix all of the creating & login to ticks by seconds, not milliseconds.
- obj.GetAllType('user', function (err, docs) {
- if (err != null) { parent.debug('db', 'ERROR (GetAll user): ' + err); }
- if ((err == null) && (docs.length > 0)) {
- for (var i in docs) {
- var fixed = false;
-
- // Add this as a valid user identifier
- validIdentifiers[docs[i]._id] = 1;
-
- // Fix email address capitalization
- if (docs[i].email && (docs[i].email != docs[i].email.toLowerCase())) {
- docs[i].email = docs[i].email.toLowerCase(); fixed = true;
- }
-
- // Fix account creation
- if (docs[i].creation) {
- if (docs[i].creation > 1300000000000) { docs[i].creation = Math.floor(docs[i].creation / 1000); fixed = true; }
- if ((docs[i].creation % 1) != 0) { docs[i].creation = Math.floor(docs[i].creation); fixed = true; }
- }
-
- // Fix last account login
- if (docs[i].login) {
- if (docs[i].login > 1300000000000) { docs[i].login = Math.floor(docs[i].login / 1000); fixed = true; }
- if ((docs[i].login % 1) != 0) { docs[i].login = Math.floor(docs[i].login); fixed = true; }
- }
-
- // Fix last password change
- if (docs[i].passchange) {
- if (docs[i].passchange > 1300000000000) { docs[i].passchange = Math.floor(docs[i].passchange / 1000); fixed = true; }
- if ((docs[i].passchange % 1) != 0) { docs[i].passchange = Math.floor(docs[i].passchange); fixed = true; }
- }
-
- // Fix subscriptions
- if (docs[i].subscriptions != null) { delete docs[i].subscriptions; fixed = true; }
-
- // Save the user if needed
- if (fixed) { obj.Set(docs[i]); }
- }
-
- // Remove all objects that have a "meshid" that no longer points to a valid mesh.
- // Fix any incorrectly escaped user identifiers
- obj.GetAllType('mesh', function (err, docs) {
- if (err != null) { parent.debug('db', 'ERROR (GetAll mesh): ' + err); }
- var meshlist = [];
- if ((err == null) && (docs.length > 0)) {
- for (var i in docs) {
- var meshChange = false;
- docs[i] = common.unEscapeLinksFieldName(docs[i]);
- meshlist.push(docs[i]._id);
-
- // Make sure all mesh types are number type, if not, fix it.
- if (typeof docs[i].mtype == 'string') { docs[i].mtype = parseInt(docs[i].mtype); meshChange = true; }
-
- // If the device group is deleted, remove any invite codes
- if (docs[i].deleted && docs[i].invite) { delete docs[i].invite; meshChange = true; }
-
- // Take a look at the links
- if (docs[i].links != null) {
- for (var j in docs[i].links) {
- if (validIdentifiers[j] == null) {
- // This identifier is not known, let see if we can fix it.
- var xid = j, xid2 = common.unEscapeFieldName(xid);
- while ((xid != xid2) && (validIdentifiers[xid2] == null)) { xid = xid2; xid2 = common.unEscapeFieldName(xid2); }
- if (validIdentifiers[xid2] == 1) {
- //console.log('Fixing id: ' + j + ' to ' + xid2);
- docs[i].links[xid2] = docs[i].links[j];
- delete docs[i].links[j];
- meshChange = true;
- } else {
- // TODO: here, we may want to clean up links to users and user groups that do not exist anymore.
- //console.log('Unknown id: ' + j);
- }
- }
- }
- }
-
- // Save the updated device group if needed
- if (meshChange) { obj.Set(docs[i]); }
- }
- }
- if (obj.databaseType == DB_SQLITE) {
- // SQLite
-
- } else if (obj.databaseType == DB_ACEBASE) {
- // AceBase
-
- } else if (obj.databaseType == DB_POSTGRESQL) {
- // Postgres
- sqlDbQuery('DELETE FROM Main WHERE ((extra != NULL) AND (extra LIKE (\'mesh/%\')) AND (extra != ANY ($1)))', [meshlist], function (err, response) { });
- } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
- // MariaDB
- sqlDbQuery('DELETE FROM Main WHERE (extra LIKE ("mesh/%") AND (extra NOT IN ?)', [meshlist], function (err, response) { });
- } else if (obj.databaseType == DB_MONGODB) {
- // MongoDB
- obj.file.deleteMany({ meshid: { $exists: true, $nin: meshlist } }, { multi: true });
- } else {
- // NeDB or MongoJS
- obj.file.remove({ meshid: { $exists: true, $nin: meshlist } }, { multi: true });
- }
-
- // We are done
- validIdentifiers = null;
- if (func) { func(); }
- });
- }
- });
- });
- };
-
- // Get encryption key
- obj.getEncryptDataKey = function (password, salt, iterations) {
- if (typeof password != 'string') return null;
- let key;
- try {
- key = parent.crypto.pbkdf2Sync(password, salt, iterations, 32, 'sha384');
- } catch (ex) {
- // If this previous call fails, it's probably because older pbkdf2 did not specify the hashing function, just use the default.
- key = parent.crypto.pbkdf2Sync(password, salt, iterations, 32);
- }
- return key
- }
-
- // Encrypt data
- obj.encryptData = function (password, plaintext) {
- let encryptionVersion = 0x01;
- let iterations = 100000
- const iv = parent.crypto.randomBytes(16);
- var key = obj.getEncryptDataKey(password, iv, iterations);
- if (key == null) return null;
- const aes = parent.crypto.createCipheriv('aes-256-gcm', key, iv);
- var ciphertext = aes.update(plaintext);
- let versionbuf = Buffer.allocUnsafe(2);
- versionbuf.writeUInt16BE(encryptionVersion);
- let iterbuf = Buffer.allocUnsafe(4);
- iterbuf.writeUInt32BE(iterations);
- let encryptedBuf = aes.final();
- ciphertext = Buffer.concat([versionbuf, iterbuf, aes.getAuthTag(), iv, ciphertext, encryptedBuf]);
- return ciphertext.toString('base64');
- }
-
- // Decrypt data
- obj.decryptData = function (password, ciphertext) {
- // Adding an encryption version lets us avoid try catching in the future
- let ciphertextBytes = Buffer.from(ciphertext, 'base64');
- let encryptionVersion = ciphertextBytes.readUInt16BE(0);
- try {
- switch (encryptionVersion) {
- case 0x01:
- let iterations = ciphertextBytes.readUInt32BE(2);
- let authTag = ciphertextBytes.slice(6, 22);
- const iv = ciphertextBytes.slice(22, 38);
- const data = ciphertextBytes.slice(38);
- let key = obj.getEncryptDataKey(password, iv, iterations);
- if (key == null) return null;
- const aes = parent.crypto.createDecipheriv('aes-256-gcm', key, iv);
- aes.setAuthTag(authTag);
- let plaintextBytes = Buffer.from(aes.update(data));
- plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
- return plaintextBytes;
- default:
- return obj.oldDecryptData(password, ciphertextBytes);
- }
- } catch (ex) { return obj.oldDecryptData(password, ciphertextBytes); }
- }
-
- // Encrypt data
- // The older encryption system uses CBC without integraty checking.
- // This method is kept only for testing
- obj.oldEncryptData = function (password, plaintext) {
- let key = parent.crypto.createHash('sha384').update(password).digest('raw').slice(0, 32);
- if (key == null) return null;
- const iv = parent.crypto.randomBytes(16);
- const aes = parent.crypto.createCipheriv('aes-256-cbc', key, iv);
- var ciphertext = aes.update(plaintext);
- ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
- return ciphertext.toString('base64');
- }
-
- // Decrypt data
- // The older encryption system uses CBC without integraty checking.
- // This method is kept only to convert the old encryption to the new one.
- obj.oldDecryptData = function (password, ciphertextBytes) {
- if (typeof password != 'string') return null;
- try {
- const iv = ciphertextBytes.slice(0, 16);
- const data = ciphertextBytes.slice(16);
- let key = parent.crypto.createHash('sha384').update(password).digest('raw').slice(0, 32);
- const aes = parent.crypto.createDecipheriv('aes-256-cbc', key, iv);
- let plaintextBytes = Buffer.from(aes.update(data));
- plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
- return plaintextBytes;
- } catch (ex) { return null; }
- }
-
- // Get the number of records in the database for various types, this is the slow NeDB way.
- // WARNING: This is a terrible query for database performance. Only do this when needed. This query will look at almost every document in the database.
- obj.getStats = function (func) {
- if (obj.databaseType == DB_ACEBASE) {
- // AceBase
- // TODO
- } else if (obj.databaseType == DB_POSTGRESQL) {
- // PostgreSQL
- // TODO
- } else if (obj.databaseType == DB_MYSQL) {
- // MySQL
- // TODO
- } else if (obj.databaseType == DB_MARIADB) {
- // MariaDB
- // TODO
- } else if (obj.databaseType == DB_MONGODB) {
- // MongoDB
- obj.file.aggregate([{ "$group": { _id: "$type", count: { $sum: 1 } } }]).toArray(function (err, docs) {
- var counters = {}, totalCount = 0;
- if (err == null) { for (var i in docs) { if (docs[i]._id != null) { counters[docs[i]._id] = docs[i].count; totalCount += docs[i].count; } } }
- func(counters);
- });
- } else if (obj.databaseType == DB_MONGOJS) {
- // MongoJS
- obj.file.aggregate([{ "$group": { _id: "$type", count: { $sum: 1 } } }], function (err, docs) {
- var counters = {}, totalCount = 0;
- if (err == null) { for (var i in docs) { if (docs[i]._id != null) { counters[docs[i]._id] = docs[i].count; totalCount += docs[i].count; } } }
- func(counters);
- });
- } else if (obj.databaseType == DB_NEDB) {
- // NeDB version
- obj.file.count({ type: 'node' }, function (err, nodeCount) {
- obj.file.count({ type: 'mesh' }, function (err, meshCount) {
- obj.file.count({ type: 'user' }, function (err, userCount) {
- obj.file.count({ type: 'sysinfo' }, function (err, sysinfoCount) {
- obj.file.count({ type: 'note' }, function (err, noteCount) {
- obj.file.count({ type: 'iploc' }, function (err, iplocCount) {
- obj.file.count({ type: 'ifinfo' }, function (err, ifinfoCount) {
- obj.file.count({ type: 'cfile' }, function (err, cfileCount) {
- obj.file.count({ type: 'lastconnect' }, function (err, lastconnectCount) {
- obj.file.count({}, function (err, totalCount) {
- func({ node: nodeCount, mesh: meshCount, user: userCount, sysinfo: sysinfoCount, iploc: iplocCount, note: noteCount, ifinfo: ifinfoCount, cfile: cfileCount, lastconnect: lastconnectCount, total: totalCount });
- });
- });
- });
- });
- });
- });
- });
- });
- });
- });
- }
- }
-
- // This is used to rate limit a number of operation per day. Returns a startValue each new days, but you can substract it and save the value in the db.
- obj.getValueOfTheDay = function (id, startValue, func) { obj.Get(id, function (err, docs) { var date = new Date(), t = date.toLocaleDateString(); if ((err == null) && (docs.length == 1)) { var r = docs[0]; if (r.day == t) { func({ _id: id, value: r.value, day: t }); return; } } func({ _id: id, value: startValue, day: t }); }); };
- obj.escapeBase64 = function escapeBase64(val) { return (val.replace(/\+/g, '@').replace(/\//g, '$')); }
-
- // Encrypt an database object
- obj.performRecordEncryptionRecode = function (func) {
- var count = 0;
- obj.GetAllType('user', function (err, docs) {
- if (err != null) { parent.debug('db', 'ERROR (performRecordEncryptionRecode): ' + err); }
- if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } }
- obj.GetAllType('node', function (err, docs) {
- if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } }
- obj.GetAllType('mesh', function (err, docs) {
- if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } }
- if (obj.databaseType == DB_NEDB) { // If we are using NeDB, compact the database.
- obj.file.compactDatafile();
- obj.file.on('compaction.done', function () { func(count); }); // It's important to wait for compaction to finish before exit, otherwise NeDB may corrupt.
- } else {
- func(count); // For all other databases, normal exit.
- }
- });
- });
- });
- }
-
- // Encrypt an database object
- function performTypedRecordDecrypt(data) {
- if ((data == null) || (obj.dbRecordsDecryptKey == null) || (typeof data != 'object')) return data;
- for (var i in data) {
- if ((data[i] == null) || (typeof data[i] != 'object')) continue;
- data[i] = performPartialRecordDecrypt(data[i]);
- if ((data[i].intelamt != null) && (typeof data[i].intelamt == 'object') && (data[i].intelamt._CRYPT)) { data[i].intelamt = performPartialRecordDecrypt(data[i].intelamt); }
- if ((data[i].amt != null) && (typeof data[i].amt == 'object') && (data[i].amt._CRYPT)) { data[i].amt = performPartialRecordDecrypt(data[i].amt); }
- if ((data[i].kvm != null) && (typeof data[i].kvm == 'object') && (data[i].kvm._CRYPT)) { data[i].kvm = performPartialRecordDecrypt(data[i].kvm); }
- }
- return data;
- }
-
- // Encrypt an database object
- function performTypedRecordEncrypt(data) {
- if (obj.dbRecordsEncryptKey == null) return data;
- if (data.type == 'user') { return performPartialRecordEncrypt(Clone(data), ['otpkeys', 'otphkeys', 'otpsecret', 'salt', 'hash', 'oldpasswords']); }
- else if ((data.type == 'node') && (data.ssh || data.rdp || data.intelamt)) {
- var xdata = Clone(data);
- if (data.ssh || data.rdp) { xdata = performPartialRecordEncrypt(xdata, ['ssh', 'rdp']); }
- if (data.intelamt) { xdata.intelamt = performPartialRecordEncrypt(xdata.intelamt, ['pass', 'mpspass']); }
- return xdata;
- }
- else if ((data.type == 'mesh') && (data.amt || data.kvm)) {
- var xdata = Clone(data);
- if (data.amt) { xdata.amt = performPartialRecordEncrypt(xdata.amt, ['password']); }
- if (data.kvm) { xdata.kvm = performPartialRecordEncrypt(xdata.kvm, ['pass']); }
- return xdata;
- }
- return data;
- }
-
- // Encrypt an object and return a buffer.
- function performPartialRecordEncrypt(plainobj, encryptNames) {
- if (typeof plainobj != 'object') return plainobj;
- var enc = {}, enclen = 0;
- for (var i in encryptNames) { if (plainobj[encryptNames[i]] != null) { enclen++; enc[encryptNames[i]] = plainobj[encryptNames[i]]; delete plainobj[encryptNames[i]]; } }
- if (enclen > 0) { plainobj._CRYPT = performRecordEncrypt(enc); } else { delete plainobj._CRYPT; }
- return plainobj;
- }
-
- // Encrypt an object and return a buffer.
- function performPartialRecordDecrypt(plainobj) {
- if ((typeof plainobj != 'object') || (plainobj._CRYPT == null)) return plainobj;
- var enc = performRecordDecrypt(plainobj._CRYPT);
- if (enc != null) { for (var i in enc) { plainobj[i] = enc[i]; } }
- delete plainobj._CRYPT;
- return plainobj;
- }
-
- // Encrypt an object and return a base64.
- function performRecordEncrypt(plainobj) {
- if (obj.dbRecordsEncryptKey == null) return null;
- const iv = parent.crypto.randomBytes(12);
- const aes = parent.crypto.createCipheriv('aes-256-gcm', obj.dbRecordsEncryptKey, iv);
- var ciphertext = aes.update(JSON.stringify(plainobj));
- var cipherfinal = aes.final();
- ciphertext = Buffer.concat([iv, aes.getAuthTag(), ciphertext, cipherfinal]);
- return ciphertext.toString('base64');
- }
-
- // Takes a base64 and return an object.
- function performRecordDecrypt(ciphertext) {
- if (obj.dbRecordsDecryptKey == null) return null;
- const ciphertextBytes = Buffer.from(ciphertext, 'base64');
- const iv = ciphertextBytes.slice(0, 12);
- const data = ciphertextBytes.slice(28);
- const aes = parent.crypto.createDecipheriv('aes-256-gcm', obj.dbRecordsDecryptKey, iv);
- aes.setAuthTag(ciphertextBytes.slice(12, 28));
- var plaintextBytes, r;
- try {
- plaintextBytes = Buffer.from(aes.update(data));
- plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
- r = JSON.parse(plaintextBytes.toString());
- } catch (e) { throw "Incorrect DbRecordsDecryptKey/DbRecordsEncryptKey or invalid database _CRYPT data: " + e; }
- return r;
- }
-
- // Clone an object (TODO: Make this more efficient)
- function Clone(v) { return JSON.parse(JSON.stringify(v)); }
-
- // Read expiration time from configuration file
- if (typeof parent.args.dbexpire == 'object') {
- if (typeof parent.args.dbexpire.events == 'number') { expireEventsSeconds = parent.args.dbexpire.events; }
- if (typeof parent.args.dbexpire.powerevents == 'number') { expirePowerEventsSeconds = parent.args.dbexpire.powerevents; }
- if (typeof parent.args.dbexpire.statsevents == 'number') { expireServerStatsSeconds = parent.args.dbexpire.statsevents; }
- }
-
- // If a DB record encryption key is provided, perform database record encryption
- if ((typeof parent.args.dbrecordsencryptkey == 'string') && (parent.args.dbrecordsencryptkey.length != 0)) {
- // Hash the database password into a AES256 key and setup encryption and decryption.
- obj.dbRecordsEncryptKey = obj.dbRecordsDecryptKey = parent.crypto.createHash('sha384').update(parent.args.dbrecordsencryptkey).digest('raw').slice(0, 32);
- }
-
- // If a DB record decryption key is provided, perform database record decryption
- if ((typeof parent.args.dbrecordsdecryptkey == 'string') && (parent.args.dbrecordsdecryptkey.length != 0)) {
- // Hash the database password into a AES256 key and setup encryption and decryption.
- obj.dbRecordsDecryptKey = parent.crypto.createHash('sha384').update(parent.args.dbrecordsdecryptkey).digest('raw').slice(0, 32);
- }
-
-
- function createTablesIfNotExist(dbname) {
- var useDatabase = 'USE ' + dbname;
- sqlDbQuery(useDatabase, null, function (err, docs) {
- if (err != null) {
- console.log("Unable to connect to database: " + err);
- process.exit();
- }
- if (err == null) {
- parent.debug('db', 'Checking tables...');
- sqlDbBatchExec([
- 'CREATE TABLE IF NOT EXISTS main (id VARCHAR(256) NOT NULL, type CHAR(32), domain CHAR(64), extra CHAR(255), extraex CHAR(255), doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))',
- 'CREATE TABLE IF NOT EXISTS events (id INT NOT NULL AUTO_INCREMENT, time DATETIME, domain CHAR(64), action CHAR(255), nodeid CHAR(255), userid CHAR(255), doc JSON, PRIMARY KEY(id), CHECK(json_valid(doc)))',
- 'CREATE TABLE IF NOT EXISTS eventids (fkid INT NOT NULL, target CHAR(255), CONSTRAINT fk_eventid FOREIGN KEY (fkid) REFERENCES events (id) ON DELETE CASCADE ON UPDATE RESTRICT)',
- 'CREATE TABLE IF NOT EXISTS serverstats (time DATETIME, expire DATETIME, doc JSON, PRIMARY KEY(time), CHECK (json_valid(doc)))',
- 'CREATE TABLE IF NOT EXISTS power (id INT NOT NULL AUTO_INCREMENT, time DATETIME, nodeid CHAR(255), doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))',
- 'CREATE TABLE IF NOT EXISTS smbios (id CHAR(255), time DATETIME, expire DATETIME, doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))',
- 'CREATE TABLE IF NOT EXISTS plugin (id INT NOT NULL AUTO_INCREMENT, doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))'
- ], function (err) {
- parent.debug('db', 'Checking indexes...');
- sqlDbExec('CREATE INDEX ndxtypedomainextra ON main (type, domain, extra)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxextra ON main (extra)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxextraex ON main (extraex)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxeventstime ON events(time)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxeventsusername ON events(domain, userid, time)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxeventsdomainnodeidtime ON events(domain, nodeid, time)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxeventids ON eventids(target)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxserverstattime ON serverstats (time)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxserverstatexpire ON serverstats (expire)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxpowernodeidtime ON power (nodeid, time)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxsmbiostime ON smbios (time)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxsmbiosexpire ON smbios (expire)', null, function (err, response) { });
- setupFunctions(func);
- });
- }
- });
- }
-
- if (parent.args.sqlite3) {
- // SQLite3 database setup
- obj.databaseType = DB_SQLITE;
- const sqlite3 = require('sqlite3');
- let configParams = parent.config.settings.sqlite3;
- if (typeof configParams == 'string') {databaseName = configParams} else {databaseName = configParams.name ? configParams.name : 'meshcentral';};
- obj.sqliteConfig.startupVacuum = configParams.startupvacuum ? configParams.startupvacuum : false;
- obj.sqliteConfig.autoVacuum = configParams.autovacuum ? configParams.autovacuum.toLowerCase() : 'incremental';
- obj.sqliteConfig.incrementalVacuum = configParams.incrementalvacuum ? configParams.incrementalvacuum : 100;
- obj.sqliteConfig.journalMode = configParams.journalmode ? configParams.journalmode.toLowerCase() : 'delete';
- //allowed modes, 'none' excluded because not usefull for this app, maybe also remove 'memory'?
- if (!(['delete', 'truncate', 'persist', 'memory', 'wal'].includes(obj.sqliteConfig.journalMode))) { obj.sqliteConfig.journalMode = 'delete'};
- obj.sqliteConfig.journalSize = configParams.journalsize ? configParams.journalsize : 409600;
- //wal can use the more performant 'normal' mode, see https://www.sqlite.org/pragma.html#pragma_synchronous
- obj.sqliteConfig.synchronous = (obj.sqliteConfig.journalMode == 'wal') ? 'normal' : 'full';
- if (obj.sqliteConfig.journalMode == 'wal') {obj.sqliteConfig.maintenance += 'PRAGMA wal_checkpoint(PASSIVE);'};
- if (obj.sqliteConfig.autoVacuum == 'incremental') {obj.sqliteConfig.maintenance += 'PRAGMA incremental_vacuum(' + obj.sqliteConfig.incrementalVacuum + ');'};
- obj.sqliteConfig.maintenance += 'PRAGMA optimize;';
-
- parent.debug('db', 'SQlite config options: ' + JSON.stringify(obj.sqliteConfig, null, 4));
- if (obj.sqliteConfig.journalMode == 'memory') { console.log('[WARNING] journal_mode=memory: this can lead to database corruption if there is a crash during a transaction. See https://www.sqlite.org/pragma.html#pragma_journal_mode') };
- //.cached not usefull
- obj.file = new sqlite3.Database(path.join(parent.datapath, databaseName + '.sqlite'), sqlite3.OPEN_READWRITE, function (err) {
- if (err && (err.code == 'SQLITE_CANTOPEN')) {
- // Database needs to be created
- obj.file = new sqlite3.Database(path.join(parent.datapath, databaseName + '.sqlite'), function (err) {
- if (err) { console.log("SQLite Error: " + err); process.exit(1); }
- obj.file.exec(`
- CREATE TABLE main (id VARCHAR(256) PRIMARY KEY NOT NULL, type CHAR(32), domain CHAR(64), extra CHAR(255), extraex CHAR(255), doc JSON);
- CREATE TABLE events(id INTEGER PRIMARY KEY, time TIMESTAMP, domain CHAR(64), action CHAR(255), nodeid CHAR(255), userid CHAR(255), doc JSON);
- CREATE TABLE eventids(fkid INT NOT NULL, target CHAR(255), CONSTRAINT fk_eventid FOREIGN KEY (fkid) REFERENCES events (id) ON DELETE CASCADE ON UPDATE RESTRICT);
- CREATE TABLE serverstats (time TIMESTAMP PRIMARY KEY, expire TIMESTAMP, doc JSON);
- CREATE TABLE power (id INTEGER PRIMARY KEY, time TIMESTAMP, nodeid CHAR(255), doc JSON);
- CREATE TABLE smbios (id CHAR(255) PRIMARY KEY, time TIMESTAMP, expire TIMESTAMP, doc JSON);
- CREATE TABLE plugin (id INTEGER PRIMARY KEY, doc JSON);
- CREATE INDEX ndxtypedomainextra ON main (type, domain, extra);
- CREATE INDEX ndxextra ON main (extra);
- CREATE INDEX ndxextraex ON main (extraex);
- CREATE INDEX ndxeventstime ON events(time);
- CREATE INDEX ndxeventsusername ON events(domain, userid, time);
- CREATE INDEX ndxeventsdomainnodeidtime ON events(domain, nodeid, time);
- CREATE INDEX ndxeventids ON eventids(target);
- CREATE INDEX ndxserverstattime ON serverstats (time);
- CREATE INDEX ndxserverstatexpire ON serverstats (expire);
- CREATE INDEX ndxpowernodeidtime ON power (nodeid, time);
- CREATE INDEX ndxsmbiostime ON smbios (time);
- CREATE INDEX ndxsmbiosexpire ON smbios (expire);
- `, function (err) {
- // Completed DB creation of SQLite3
- sqliteSetOptions(func);
- //setupFunctions could be put in the sqliteSetupOptions, but left after it for clarity
- setupFunctions(func);
- }
- );
- });
- return;
- } else if (err) { console.log("SQLite Error: " + err); process.exit(0); }
-
- //for existing db's
- sqliteSetOptions();
- //setupFunctions could be put in the sqliteSetupOptions, but left after it for clarity
- setupFunctions(func);
- });
- } else if (parent.args.acebase) {
- // AceBase database setup
- obj.databaseType = DB_ACEBASE;
- const { AceBase } = require('acebase');
- // For information on AceBase sponsor: https://github.com/appy-one/acebase/discussions/100
- obj.file = new AceBase('meshcentral', { sponsor: ((typeof parent.args.acebase == 'object') && (parent.args.acebase.sponsor)), logLevel: 'error', storage: { path: parent.datapath } });
- // Get all the databases ready
- obj.file.ready(function () {
- // Create AceBase indexes
- obj.file.indexes.create('meshcentral', 'type', { include: ['domain', 'meshid'] });
- obj.file.indexes.create('meshcentral', 'email');
- obj.file.indexes.create('meshcentral', 'meshid');
- obj.file.indexes.create('meshcentral', 'intelamt.uuid');
- obj.file.indexes.create('events', 'userid', { include: ['action'] });
- obj.file.indexes.create('events', 'domain', { include: ['nodeid', 'time'] });
- obj.file.indexes.create('events', 'ids', { include: ['time'] });
- obj.file.indexes.create('events', 'time');
- obj.file.indexes.create('power', 'nodeid', { include: ['time'] });
- obj.file.indexes.create('power', 'time');
- obj.file.indexes.create('stats', 'time');
- obj.file.indexes.create('stats', 'expire');
- // Completed setup of AceBase
- setupFunctions(func);
- });
- } else if (parent.args.mariadb || parent.args.mysql) {
- var connectinArgs = (parent.args.mariadb) ? parent.args.mariadb : parent.args.mysql;
- if (typeof connectinArgs == 'string') {
- const parts = connectinArgs.split(/[:@/]+/);
- var connectionObject = {
- "user": parts[1],
- "password": parts[2],
- "host": parts[3],
- "port": parts[4],
- "database": parts[5]
- };
- var dbname = (connectionObject.database != null) ? connectionObject.database : 'meshcentral';
- } else {
- var dbname = (connectinArgs.database != null) ? connectinArgs.database : 'meshcentral';
-
- // Including the db name in the connection obj will cause a connection faliure if it does not exist
- var connectionObject = Clone(connectinArgs);
- delete connectionObject.database;
-
- try {
- if (connectinArgs.ssl) {
- if (connectinArgs.ssl.dontcheckserveridentity == true) { connectionObject.ssl.checkServerIdentity = function (name, cert) { return undefined; } };
- if (connectinArgs.ssl.cacertpath) { connectionObject.ssl.ca = [require('fs').readFileSync(connectinArgs.ssl.cacertpath, 'utf8')]; }
- if (connectinArgs.ssl.clientcertpath) { connectionObject.ssl.cert = [require('fs').readFileSync(connectinArgs.ssl.clientcertpath, 'utf8')]; }
- if (connectinArgs.ssl.clientkeypath) { connectionObject.ssl.key = [require('fs').readFileSync(connectinArgs.ssl.clientkeypath, 'utf8')]; }
- }
- } catch (ex) {
- console.log('Error loading SQL Connector certificate: ' + ex);
- process.exit();
- }
- }
-
- if (parent.args.mariadb) {
- // Use MariaDB
- obj.databaseType = DB_MARIADB;
- var tempDatastore = require('mariadb').createPool(connectionObject);
- tempDatastore.getConnection().then(function (conn) {
- conn.query('CREATE DATABASE IF NOT EXISTS ' + dbname).then(function (result) {
- conn.release();
- }).catch(function (ex) { console.log('Auto-create database failed: ' + ex); });
- }).catch(function (ex) { console.log('Auto-create database failed: ' + ex); });
- setTimeout(function () { tempDatastore.end(); }, 2000);
-
- connectionObject.database = dbname;
- Datastore = require('mariadb').createPool(connectionObject);
- createTablesIfNotExist(dbname);
- } else if (parent.args.mysql) {
- // Use MySQL
- obj.databaseType = DB_MYSQL;
- var tempDatastore = require('mysql2').createPool(connectionObject);
- tempDatastore.query('CREATE DATABASE IF NOT EXISTS ' + dbname, function (error) {
- if (error != null) {
- console.log('Auto-create database failed: ' + error);
- }
- connectionObject.database = dbname;
- Datastore = require('mysql2').createPool(connectionObject);
- createTablesIfNotExist(dbname);
- });
- setTimeout(function () { tempDatastore.end(); }, 2000);
- }
- } else if (parent.args.postgres) {
- // Postgres SQL
- let connectinArgs = parent.args.postgres;
- connectinArgs.database = (databaseName = (connectinArgs.database != null) ? connectinArgs.database : 'meshcentral');
-
- let DatastoreTest;
- obj.databaseType = DB_POSTGRESQL;
- const { Client } = require('pg');
- Datastore = new Client(connectinArgs);
- //Connect to and check pg db first to check if own db exists. Otherwise errors out on 'database does not exist'
- connectinArgs.database = 'postgres';
- DatastoreTest = new Client(connectinArgs);
- DatastoreTest.connect();
- connectinArgs.database = databaseName; //put the name back for backupconfig info
- DatastoreTest.query('SELECT 1 FROM pg_catalog.pg_database WHERE datname = $1', [databaseName], function (err, res) { // check database exists first before creating
- if (res.rowCount != 0) { // database exists now check tables exists
- DatastoreTest.end();
- Datastore.connect();
- Datastore.query('SELECT doc FROM main WHERE id = $1', ['DatabaseIdentifier'], function (err, res) {
- if (err == null) {
- (res.rowCount ==0) ? postgreSqlCreateTables(func) : setupFunctions(func)
- } else
- if (err.code == '42P01') { //42P01 = undefined table, https://www.postgresql.org/docs/current/errcodes-appendix.html
- postgreSqlCreateTables(func);
- } else {
- console.log('Postgresql database exists, other error: ', err.message); process.exit(0);
- };
- });
- } else { // If not present, create the tables and indexes
- //not needed, just use a create db statement: const pgtools = require('pgtools');
- DatastoreTest.query('CREATE DATABASE "'+ databaseName + '";', [], function (err, res) {
- if (err == null) {
- // Create the tables and indexes
- DatastoreTest.end();
- Datastore.connect();
- postgreSqlCreateTables(func);
- } else {
- console.log('Postgresql database create error: ', err.message);
- process.exit(0);
- }
- });
- }
- });
- } else if (parent.args.mongodb) {
- // Use MongoDB
- obj.databaseType = DB_MONGODB;
-
- // If running an older NodeJS version, TextEncoder/TextDecoder is required
- if (global.TextEncoder == null) { global.TextEncoder = require('util').TextEncoder; }
- if (global.TextDecoder == null) { global.TextDecoder = require('util').TextDecoder; }
-
- require('mongodb').MongoClient.connect(parent.args.mongodb, { useNewUrlParser: true, useUnifiedTopology: true, enableUtf8Validation: false }, function (err, client) {
- if (err != null) { console.log("Unable to connect to database: " + err); process.exit(); return; }
- Datastore = client;
- parent.debug('db', 'Connected to MongoDB database...');
-
- // Get the database name and setup the database client
- var dbname = 'meshcentral';
- if (parent.args.mongodbname) { dbname = parent.args.mongodbname; }
- const dbcollectionname = (parent.args.mongodbcol) ? (parent.args.mongodbcol) : 'meshcentral';
- const db = client.db(dbname);
-
- // Check the database version
- db.admin().serverInfo(function (err, info) {
- if ((err != null) || (info == null) || (info.versionArray == null) || (Array.isArray(info.versionArray) == false) || (info.versionArray.length < 2) || (typeof info.versionArray[0] != 'number') || (typeof info.versionArray[1] != 'number')) {
- console.log('WARNING: Unable to check MongoDB version.');
- } else {
- if ((info.versionArray[0] < 3) || ((info.versionArray[0] == 3) && (info.versionArray[1] < 6))) {
- // We are running with mongoDB older than 3.6, this is not good.
- parent.addServerWarning("Current version of MongoDB (" + info.version + ") is too old, please upgrade to MongoDB 3.6 or better.", true);
- }
- }
- });
-
- // Setup MongoDB main collection and indexes
- obj.file = db.collection(dbcollectionname);
- obj.file.indexes(function (err, indexes) {
- // Check if we need to reset indexes
- var indexesByName = {}, indexCount = 0;
- for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
- if ((indexCount != 5) || (indexesByName['TypeDomainMesh1'] == null) || (indexesByName['Email1'] == null) || (indexesByName['Mesh1'] == null) || (indexesByName['AmtUuid1'] == null)) {
- console.log('Resetting main indexes...');
- obj.file.dropIndexes(function (err) {
- obj.file.createIndex({ type: 1, domain: 1, meshid: 1 }, { sparse: 1, name: 'TypeDomainMesh1' }); // Speeds up GetAllTypeNoTypeField() and GetAllTypeNoTypeFieldMeshFiltered()
- obj.file.createIndex({ email: 1 }, { sparse: 1, name: 'Email1' }); // Speeds up GetUserWithEmail() and GetUserWithVerifiedEmail()
- obj.file.createIndex({ meshid: 1 }, { sparse: 1, name: 'Mesh1' }); // Speeds up RemoveMesh()
- obj.file.createIndex({ 'intelamt.uuid': 1 }, { sparse: 1, name: 'AmtUuid1' }); // Speeds up getAmtUuidMeshNode()
- });
- }
- });
-
- // Setup the changeStream on the MongoDB main collection if possible
- if (parent.args.mongodbchangestream == true) {
- obj.dbCounters.changeStream = { change: 0, update: 0, insert: 0, delete: 0 };
- if (typeof obj.file.watch != 'function') {
- console.log('WARNING: watch() is not a function, MongoDB ChangeStream not supported.');
- } else {
- obj.fileChangeStream = obj.file.watch([{ $match: { $or: [{ 'fullDocument.type': { $in: ['node', 'mesh', 'user', 'ugrp'] } }, { 'operationType': 'delete' }] } }], { fullDocument: 'updateLookup' });
- obj.fileChangeStream.on('change', function (change) {
- obj.dbCounters.changeStream.change++;
- if ((change.operationType == 'update') || (change.operationType == 'replace')) {
- obj.dbCounters.changeStream.update++;
- switch (change.fullDocument.type) {
- case 'node': { dbNodeChange(change, false); break; } // A node has changed
- case 'mesh': { dbMeshChange(change, false); break; } // A device group has changed
- case 'user': { dbUserChange(change, false); break; } // A user account has changed
- case 'ugrp': { dbUGrpChange(change, false); break; } // A user account has changed
- }
- } else if (change.operationType == 'insert') {
- obj.dbCounters.changeStream.insert++;
- switch (change.fullDocument.type) {
- case 'node': { dbNodeChange(change, true); break; } // A node has added
- case 'mesh': { dbMeshChange(change, true); break; } // A device group has created
- case 'user': { dbUserChange(change, true); break; } // A user account has created
- case 'ugrp': { dbUGrpChange(change, true); break; } // A user account has created
- }
- } else if (change.operationType == 'delete') {
- obj.dbCounters.changeStream.delete++;
- if ((change.documentKey == null) || (change.documentKey._id == null)) return;
- var splitId = change.documentKey._id.split('/');
- switch (splitId[0]) {
- case 'node': {
- //Not Good: Problem here is that we don't know what meshid the node belonged to before the delete.
- //parent.DispatchEvent(['*', node.meshid], obj, { etype: 'node', action: 'removenode', nodeid: change.documentKey._id, domain: splitId[1] });
- break;
- }
- case 'mesh': {
- parent.DispatchEvent(['*', change.documentKey._id], obj, { etype: 'mesh', action: 'deletemesh', meshid: change.documentKey._id, domain: splitId[1] });
- break;
- }
- case 'user': {
- //Not Good: This is not a perfect user removal because we don't know what groups the user was in.
- //parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', action: 'accountremove', userid: change.documentKey._id, domain: splitId[1], username: splitId[2] });
- break;
- }
- case 'ugrp': {
- parent.DispatchEvent(['*', change.documentKey._id], obj, { etype: 'ugrp', action: 'deleteusergroup', ugrpid: change.documentKey._id, domain: splitId[1] });
- break;
- }
- }
- }
- });
- obj.changeStream = true;
- }
- }
-
- // Setup MongoDB events collection and indexes
- obj.eventsfile = db.collection('events'); // Collection containing all events
- obj.eventsfile.indexes(function (err, indexes) {
- // Check if we need to reset indexes
- var indexesByName = {}, indexCount = 0;
- for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
- if ((indexCount != 5) || (indexesByName['UseridAction1'] == null) || (indexesByName['DomainNodeTime1'] == null) || (indexesByName['IdsAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
- // Reset all indexes
- console.log("Resetting events indexes...");
- obj.eventsfile.dropIndexes(function (err) {
- obj.eventsfile.createIndex({ userid: 1, action: 1 }, { sparse: 1, name: 'UseridAction1' });
- obj.eventsfile.createIndex({ domain: 1, nodeid: 1, time: -1 }, { sparse: 1, name: 'DomainNodeTime1' });
- obj.eventsfile.createIndex({ ids: 1, time: -1 }, { sparse: 1, name: 'IdsAndTime1' });
- obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
- });
- } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireEventsSeconds) {
- // Reset the timeout index
- console.log("Resetting events expire index...");
- obj.eventsfile.dropIndex('ExpireTime1', function (err) {
- obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
- });
- }
- });
-
- // Setup MongoDB power events collection and indexes
- obj.powerfile = db.collection('power'); // Collection containing all power events
- obj.powerfile.indexes(function (err, indexes) {
- // Check if we need to reset indexes
- var indexesByName = {}, indexCount = 0;
- for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
- if ((indexCount != 3) || (indexesByName['NodeIdAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
- // Reset all indexes
- console.log("Resetting power events indexes...");
- obj.powerfile.dropIndexes(function (err) {
- // Create all indexes
- obj.powerfile.createIndex({ nodeid: 1, time: 1 }, { sparse: 1, name: 'NodeIdAndTime1' });
- obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
- });
- } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expirePowerEventsSeconds) {
- // Reset the timeout index
- console.log("Resetting power events expire index...");
- obj.powerfile.dropIndex('ExpireTime1', function (err) {
- // Reset the expire power events index
- obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
- });
- }
- });
-
- // Setup MongoDB smbios collection, no indexes needed
- obj.smbiosfile = db.collection('smbios'); // Collection containing all smbios information
-
- // Setup MongoDB server stats collection
- obj.serverstatsfile = db.collection('serverstats'); // Collection of server stats
- obj.serverstatsfile.indexes(function (err, indexes) {
- // Check if we need to reset indexes
- var indexesByName = {}, indexCount = 0;
- for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
- if ((indexCount != 3) || (indexesByName['ExpireTime1'] == null)) {
- // Reset all indexes
- console.log("Resetting server stats indexes...");
- obj.serverstatsfile.dropIndexes(function (err) {
- // Create all indexes
- obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
- obj.serverstatsfile.createIndex({ 'expire': 1 }, { expireAfterSeconds: 0, name: 'ExpireTime2' }); // Auto-expire events
- });
- } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireServerStatsSeconds) {
- // Reset the timeout index
- console.log("Resetting server stats expire index...");
- obj.serverstatsfile.dropIndex('ExpireTime1', function (err) {
- // Reset the expire server stats index
- obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
- });
- }
- });
-
- // Setup plugin info collection
- if (obj.pluginsActive) { obj.pluginsfile = db.collection('plugins'); }
-
- setupFunctions(func); // Completed setup of MongoDB
- });
- } else if (parent.args.xmongodb) {
- // Use MongoJS, this is the old system.
- obj.databaseType = DB_MONGOJS;
- Datastore = require('mongojs');
- var db = Datastore(parent.args.xmongodb);
- var dbcollection = 'meshcentral';
- if (parent.args.mongodbcol) { dbcollection = parent.args.mongodbcol; }
-
- // Setup MongoDB main collection and indexes
- obj.file = db.collection(dbcollection);
- obj.file.getIndexes(function (err, indexes) {
- // Check if we need to reset indexes
- var indexesByName = {}, indexCount = 0;
- for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
- if ((indexCount != 5) || (indexesByName['TypeDomainMesh1'] == null) || (indexesByName['Email1'] == null) || (indexesByName['Mesh1'] == null) || (indexesByName['AmtUuid1'] == null)) {
- console.log("Resetting main indexes...");
- obj.file.dropIndexes(function (err) {
- obj.file.createIndex({ type: 1, domain: 1, meshid: 1 }, { sparse: 1, name: 'TypeDomainMesh1' }); // Speeds up GetAllTypeNoTypeField() and GetAllTypeNoTypeFieldMeshFiltered()
- obj.file.createIndex({ email: 1 }, { sparse: 1, name: 'Email1' }); // Speeds up GetUserWithEmail() and GetUserWithVerifiedEmail()
- obj.file.createIndex({ meshid: 1 }, { sparse: 1, name: 'Mesh1' }); // Speeds up RemoveMesh()
- obj.file.createIndex({ 'intelamt.uuid': 1 }, { sparse: 1, name: 'AmtUuid1' }); // Speeds up getAmtUuidMeshNode()
- });
- }
- });
-
- // Setup MongoDB events collection and indexes
- obj.eventsfile = db.collection('events'); // Collection containing all events
- obj.eventsfile.getIndexes(function (err, indexes) {
- // Check if we need to reset indexes
- var indexesByName = {}, indexCount = 0;
- for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
- if ((indexCount != 5) || (indexesByName['UseridAction1'] == null) || (indexesByName['DomainNodeTime1'] == null) || (indexesByName['IdsAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
- // Reset all indexes
- console.log("Resetting events indexes...");
- obj.eventsfile.dropIndexes(function (err) {
- obj.eventsfile.createIndex({ userid: 1, action: 1 }, { sparse: 1, name: 'UseridAction1' });
- obj.eventsfile.createIndex({ domain: 1, nodeid: 1, time: -1 }, { sparse: 1, name: 'DomainNodeTime1' });
- obj.eventsfile.createIndex({ ids: 1, time: -1 }, { sparse: 1, name: 'IdsAndTime1' });
- obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
- });
- } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireEventsSeconds) {
- // Reset the timeout index
- console.log("Resetting events expire index...");
- obj.eventsfile.dropIndex('ExpireTime1', function (err) {
- obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
- });
- }
- });
-
- // Setup MongoDB power events collection and indexes
- obj.powerfile = db.collection('power'); // Collection containing all power events
- obj.powerfile.getIndexes(function (err, indexes) {
- // Check if we need to reset indexes
- var indexesByName = {}, indexCount = 0;
- for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
- if ((indexCount != 3) || (indexesByName['NodeIdAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
- // Reset all indexes
- console.log("Resetting power events indexes...");
- obj.powerfile.dropIndexes(function (err) {
- // Create all indexes
- obj.powerfile.createIndex({ nodeid: 1, time: 1 }, { sparse: 1, name: 'NodeIdAndTime1' });
- obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
- });
- } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expirePowerEventsSeconds) {
- // Reset the timeout index
- console.log("Resetting power events expire index...");
- obj.powerfile.dropIndex('ExpireTime1', function (err) {
- // Reset the expire power events index
- obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
- });
- }
- });
-
- // Setup MongoDB smbios collection, no indexes needed
- obj.smbiosfile = db.collection('smbios'); // Collection containing all smbios information
-
- // Setup MongoDB server stats collection
- obj.serverstatsfile = db.collection('serverstats'); // Collection of server stats
- obj.serverstatsfile.getIndexes(function (err, indexes) {
- // Check if we need to reset indexes
- var indexesByName = {}, indexCount = 0;
- for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
- if ((indexCount != 3) || (indexesByName['ExpireTime1'] == null)) {
- // Reset all indexes
- console.log("Resetting server stats indexes...");
- obj.serverstatsfile.dropIndexes(function (err) {
- // Create all indexes
- obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
- obj.serverstatsfile.createIndex({ 'expire': 1 }, { expireAfterSeconds: 0, name: 'ExpireTime2' }); // Auto-expire events
- });
- } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireServerStatsSeconds) {
- // Reset the timeout index
- console.log("Resetting server stats expire index...");
- obj.serverstatsfile.dropIndex('ExpireTime1', function (err) {
- // Reset the expire server stats index
- obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
- });
- }
- });
-
- // Setup plugin info collection
- if (obj.pluginsActive) { obj.pluginsfile = db.collection('plugins'); }
-
- setupFunctions(func); // Completed setup of MongoJS
- } else {
- // Use NeDB (The default)
- obj.databaseType = DB_NEDB;
- try { Datastore = require('@seald-io/nedb'); } catch (ex) { } // This is the NeDB with Node 23 support.
- if (Datastore == null) {
- try { Datastore = require('@yetzt/nedb'); } catch (ex) { } // This is the NeDB with fixed security dependencies.
- if (Datastore == null) { Datastore = require('nedb'); } // So not to break any existing installations, if the old NeDB is present, use it.
- }
- var datastoreOptions = { filename: parent.getConfigFilePath('meshcentral.db'), autoload: true };
-
- // If a DB encryption key is provided, perform database encryption
- if ((typeof parent.args.dbencryptkey == 'string') && (parent.args.dbencryptkey.length != 0)) {
- // Hash the database password into a AES256 key and setup encryption and decryption.
- obj.dbKey = parent.crypto.createHash('sha384').update(parent.args.dbencryptkey).digest('raw').slice(0, 32);
- datastoreOptions.afterSerialization = function (plaintext) {
- const iv = parent.crypto.randomBytes(16);
- const aes = parent.crypto.createCipheriv('aes-256-cbc', obj.dbKey, iv);
- var ciphertext = aes.update(plaintext);
- ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
- return ciphertext.toString('base64');
- }
- datastoreOptions.beforeDeserialization = function (ciphertext) {
- const ciphertextBytes = Buffer.from(ciphertext, 'base64');
- const iv = ciphertextBytes.slice(0, 16);
- const data = ciphertextBytes.slice(16);
- const aes = parent.crypto.createDecipheriv('aes-256-cbc', obj.dbKey, iv);
- var plaintextBytes = Buffer.from(aes.update(data));
- plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
- return plaintextBytes.toString();
- }
- }
-
- // Start NeDB main collection and setup indexes
- obj.file = new Datastore(datastoreOptions);
- obj.file.setAutocompactionInterval(86400000); // Compact once a day
- obj.file.ensureIndex({ fieldName: 'type' });
- obj.file.ensureIndex({ fieldName: 'domain' });
- obj.file.ensureIndex({ fieldName: 'meshid', sparse: true });
- obj.file.ensureIndex({ fieldName: 'nodeid', sparse: true });
- obj.file.ensureIndex({ fieldName: 'email', sparse: true });
-
- // Setup the events collection and setup indexes
- obj.eventsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-events.db'), autoload: true, corruptAlertThreshold: 1 });
- obj.eventsfile.setAutocompactionInterval(86400000); // Compact once a day
- obj.eventsfile.ensureIndex({ fieldName: 'ids' }); // TODO: Not sure if this is a good index, this is a array field.
- obj.eventsfile.ensureIndex({ fieldName: 'nodeid', sparse: true });
- obj.eventsfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: expireEventsSeconds });
- obj.eventsfile.remove({ time: { '$lt': new Date(Date.now() - (expireEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
-
- // Setup the power collection and setup indexes
- obj.powerfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-power.db'), autoload: true, corruptAlertThreshold: 1 });
- obj.powerfile.setAutocompactionInterval(86400000); // Compact once a day
- obj.powerfile.ensureIndex({ fieldName: 'nodeid' });
- obj.powerfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: expirePowerEventsSeconds });
- obj.powerfile.remove({ time: { '$lt': new Date(Date.now() - (expirePowerEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
-
- // Setup the SMBIOS collection, for NeDB we don't setup SMBIOS since NeDB will corrupt the database. Remove any existing ones.
- //obj.smbiosfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-smbios.db'), autoload: true, corruptAlertThreshold: 1 });
- fs.unlink(parent.getConfigFilePath('meshcentral-smbios.db'), function () { });
-
- // Setup the server stats collection and setup indexes
- obj.serverstatsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-stats.db'), autoload: true, corruptAlertThreshold: 1 });
- obj.serverstatsfile.setAutocompactionInterval(86400000); // Compact once a day
- obj.serverstatsfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: expireServerStatsSeconds });
- obj.serverstatsfile.ensureIndex({ fieldName: 'expire', expireAfterSeconds: 0 }); // Auto-expire events
- obj.serverstatsfile.remove({ time: { '$lt': new Date(Date.now() - (expireServerStatsSeconds * 1000)) } }, { multi: true }); // Force delete older events
-
- // Setup plugin info collection
- if (obj.pluginsActive) {
- obj.pluginsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-plugins.db'), autoload: true });
- obj.pluginsfile.setAutocompactionInterval(86400000); // Compact once a day
- }
-
- setupFunctions(func); // Completed setup of NeDB
- }
-
- function sqliteSetOptions(func) {
- //get current auto_vacuum mode for comparison
- obj.file.get('PRAGMA auto_vacuum;', function(err, current){
- let pragma = 'PRAGMA journal_mode=' + obj.sqliteConfig.journalMode + ';' +
- 'PRAGMA synchronous='+ obj.sqliteConfig.synchronous + ';' +
- 'PRAGMA journal_size_limit=' + obj.sqliteConfig.journalSize + ';' +
- 'PRAGMA auto_vacuum=' + obj.sqliteConfig.autoVacuum + ';' +
- 'PRAGMA incremental_vacuum=' + obj.sqliteConfig.incrementalVacuum + ';' +
- 'PRAGMA optimize=0x10002;';
- //check new autovacuum mode, if changing from or to 'none', a VACUUM needs to be done to activate it. See https://www.sqlite.org/pragma.html#pragma_auto_vacuum
- if ( obj.sqliteConfig.startupVacuum
- || (current.auto_vacuum == 0 && obj.sqliteConfig.autoVacuum !='none')
- || (current.auto_vacuum != 0 && obj.sqliteConfig.autoVacuum =='none'))
- {
- pragma += 'VACUUM;';
- };
- parent.debug ('db', 'Config statement: ' + pragma);
-
- obj.file.exec( pragma,
- function (err) {
- if (err) { parent.debug('db', 'Config pragma error: ' + (err.message)) };
- sqliteGetPragmas(['journal_mode', 'journal_size_limit', 'freelist_count', 'auto_vacuum', 'page_size', 'wal_autocheckpoint', 'synchronous'], function (pragma, pragmaValue) {
- parent.debug('db', 'PRAGMA: ' + pragma + '=' + pragmaValue);
- });
- });
- });
- //setupFunctions(func);
- }
-
- function sqliteGetPragmas (pragmas, func){
- //pragmas can only be gotting one by one
- pragmas.forEach (function (pragma) {
- obj.file.get('PRAGMA ' + pragma + ';', function(err, res){
- if (pragma == 'auto_vacuum') { res[pragma] = SQLITE_AUTOVACUUM[res[pragma]] };
- if (pragma == 'synchronous') { res[pragma] = SQLITE_SYNCHRONOUS[res[pragma]] };
- if (func) { func (pragma, res[pragma]); }
- });
- });
- }
- // Create the PostgreSQL tables
- function postgreSqlCreateTables(func) {
- // Database was created, create the tables
- parent.debug('db', 'Creating tables...');
- sqlDbBatchExec([
- 'CREATE TABLE IF NOT EXISTS main (id VARCHAR(256) PRIMARY KEY NOT NULL, type CHAR(32), domain CHAR(64), extra CHAR(255), extraex CHAR(255), doc JSON)',
- 'CREATE TABLE IF NOT EXISTS events(id SERIAL PRIMARY KEY, time TIMESTAMP, domain CHAR(64), action CHAR(255), nodeid CHAR(255), userid CHAR(255), doc JSON)',
- 'CREATE TABLE IF NOT EXISTS eventids(fkid INT NOT NULL, target CHAR(255), CONSTRAINT fk_eventid FOREIGN KEY (fkid) REFERENCES events (id) ON DELETE CASCADE ON UPDATE RESTRICT)',
- 'CREATE TABLE IF NOT EXISTS serverstats (time TIMESTAMP PRIMARY KEY, expire TIMESTAMP, doc JSON)',
- 'CREATE TABLE IF NOT EXISTS power (id SERIAL PRIMARY KEY, time TIMESTAMP, nodeid CHAR(255), doc JSON)',
- 'CREATE TABLE IF NOT EXISTS smbios (id CHAR(255) PRIMARY KEY, time TIMESTAMP, expire TIMESTAMP, doc JSON)',
- 'CREATE TABLE IF NOT EXISTS plugin (id SERIAL PRIMARY KEY, doc JSON)'
- ], function (results) {
- parent.debug('db', 'Creating indexes...');
- sqlDbExec('CREATE INDEX ndxtypedomainextra ON main (type, domain, extra)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxextra ON main (extra)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxextraex ON main (extraex)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxeventstime ON events(time)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxeventsusername ON events(domain, userid, time)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxeventsdomainnodeidtime ON events(domain, nodeid, time)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxeventids ON eventids(target)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxserverstattime ON serverstats (time)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxserverstatexpire ON serverstats (expire)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxpowernodeidtime ON power (nodeid, time)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxsmbiostime ON smbios (time)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxsmbiosexpire ON smbios (expire)', null, function (err, response) { });
- setupFunctions(func);
- });
- }
-
- // Check the object names for a "."
- function checkObjectNames(r, tag) {
- if (typeof r != 'object') return;
- for (var i in r) {
- if (i.indexOf('.') >= 0) { throw ('BadDbName (' + tag + '): ' + JSON.stringify(r)); }
- checkObjectNames(r[i], tag);
- }
- }
-
- // Query the database
- function sqlDbQuery(query, args, func, debug) {
- if (obj.databaseType == DB_SQLITE) { // SQLite
- if (args == null) { args = []; }
- obj.file.all(query, args, function (err, docs) {
- if (err != null) { console.log(query, args, err, docs); }
- if (docs != null) {
- for (var i in docs) {
- if (typeof docs[i].doc == 'string') {
- try { docs[i] = JSON.parse(docs[i].doc); } catch (ex) {
- console.log(query, args, docs[i]);
- }
- }
- }
- }
- if (func) { func(err, docs); }
- });
- } else if (obj.databaseType == DB_MARIADB) { // MariaDB
- Datastore.getConnection()
- .then(function (conn) {
- conn.query(query, args)
- .then(function (rows) {
- conn.release();
- var docs = [];
- for (var i in rows) {
- if (rows[i].doc) {
- docs.push(performTypedRecordDecrypt((typeof rows[i].doc == 'object') ? rows[i].doc : JSON.parse(rows[i].doc)));
- } else if ((rows.length == 1) && (rows[i]['COUNT(doc)'] != null)) {
- // This is a SELECT COUNT() operation
- docs = parseInt(rows[i]['COUNT(doc)']);
- }
- }
- if (func) try { func(null, docs); } catch (ex) { console.log('SQLERR1', ex); }
- })
- .catch(function (err) { conn.release(); if (func) try { func(err); } catch (ex) { console.log('SQLERR2', ex); } });
- }).catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log('SQLERR3', ex); } } });
- } else if (obj.databaseType == DB_MYSQL) { // MySQL
- Datastore.query(query, args, function (error, results, fields) {
- if (error != null) {
- if (func) try { func(error); } catch (ex) { console.log('SQLERR4', ex); }
- } else {
- var docs = [];
- for (var i in results) {
- if (results[i].doc) {
- if (typeof results[i].doc == 'string') {
- docs.push(JSON.parse(results[i].doc));
- } else {
- docs.push(results[i].doc);
- }
- } else if ((results.length == 1) && (results[i]['COUNT(doc)'] != null)) {
- // This is a SELECT COUNT() operation
- docs = results[i]['COUNT(doc)'];
- }
- }
- if (func) { try { func(null, docs); } catch (ex) { console.log('SQLERR5', ex); } }
- }
- });
- } else if (obj.databaseType == DB_POSTGRESQL) { // Postgres SQL
- Datastore.query(query, args, function (error, results) {
- if (error != null) {
- if (func) try { func(error); } catch (ex) { console.log('SQLERR4', ex); }
- } else {
- var docs = [];
- if ((results.command == 'INSERT') && (results.rows != null) && (results.rows.length == 1)) { docs = results.rows[0]; }
- else if (results.command == 'SELECT') {
- for (var i in results.rows) {
- if (results.rows[i].doc) {
- if (typeof results.rows[i].doc == 'string') {
- docs.push(JSON.parse(results.rows[i].doc));
- } else {
- docs.push(results.rows[i].doc);
- }
- } else if (results.rows[i].count && (results.rows.length == 1)) {
- // This is a SELECT COUNT() operation
- docs = parseInt(results.rows[i].count);
- }
- }
- }
- if (func) { try { func(null, docs, results); } catch (ex) { console.log('SQLERR5', ex); } }
- }
- });
- }
- }
-
- // Exec on the database
- function sqlDbExec(query, args, func) {
- if (obj.databaseType == DB_MARIADB) { // MariaDB
- Datastore.getConnection()
- .then(function (conn) {
- conn.query(query, args)
- .then(function (rows) {
- conn.release();
- if (func) try { func(null, rows[0]); } catch (ex) { console.log(ex); }
- })
- .catch(function (err) { conn.release(); if (func) try { func(err); } catch (ex) { console.log(ex); } });
- }).catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } });
- } else if ((obj.databaseType == DB_MYSQL) || (obj.databaseType == DB_POSTGRESQL)) { // MySQL or Postgres SQL
- Datastore.query(query, args, function (error, results, fields) {
- if (func) try { func(error, results ? results[0] : null); } catch (ex) { console.log(ex); }
- });
- }
- }
-
- // Execute a batch of commands on the database
- function sqlDbBatchExec(queries, func) {
- if (obj.databaseType == DB_MARIADB) { // MariaDB
- Datastore.getConnection()
- .then(function (conn) {
- var Promises = [];
- for (var i in queries) { if (typeof queries[i] == 'string') { Promises.push(conn.query(queries[i])); } else { Promises.push(conn.query(queries[i][0], queries[i][1])); } }
- Promise.all(Promises)
- .then(function (rows) { conn.release(); if (func) { try { func(null); } catch (ex) { console.log(ex); } } })
- .catch(function (err) { conn.release(); if (func) { try { func(err); } catch (ex) { console.log(ex); } } });
- })
- .catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } });
- } else if (obj.databaseType == DB_MYSQL) { // MySQL
- Datastore.getConnection(function(err, connection) {
- if (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } return; }
- var Promises = [];
- for (var i in queries) { if (typeof queries[i] == 'string') { Promises.push(connection.promise().query(queries[i])); } else { Promises.push(connection.promise().query(queries[i][0], queries[i][1])); } }
- Promise.all(Promises)
- .then(function (error, results, fields) { connection.release(); if (func) { try { func(error, results); } catch (ex) { console.log(ex); } } })
- .catch(function (error, results, fields) { connection.release(); if (func) { try { func(error); } catch (ex) { console.log(ex); } } });
- });
- } else if (obj.databaseType == DB_POSTGRESQL) { // Postgres
- var Promises = [];
- for (var i in queries) { if (typeof queries[i] == 'string') { Promises.push(Datastore.query(queries[i])); } else { Promises.push(Datastore.query(queries[i][0], queries[i][1])); } }
- Promise.all(Promises)
- .then(function (error, results, fields) { if (func) { try { func(error, results); } catch (ex) { console.log(ex); } } })
- .catch(function (error, results, fields) { if (func) { try { func(error); } catch (ex) { console.log(ex); } } });
- }
- }
-
- function setupFunctions(func) {
- if (obj.databaseType == DB_SQLITE) {
- // Database actions on the main collection. SQLite3: https://www.linode.com/docs/guides/getting-started-with-nodejs-sqlite/
- obj.Set = function (value, func) {
- obj.dbCounters.fileSet++;
- var extra = null, extraex = null;
- value = common.escapeLinksFieldNameEx(value);
- if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
- if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
- if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
- sqlDbQuery('INSERT INTO main VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET type = $2, domain = $3, extra = $4, extraex = $5, doc = $6;', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, JSON.stringify(performTypedRecordEncrypt(value))], func);
- }
- obj.SetRaw = function (value, func) {
- obj.dbCounters.fileSet++;
- var extra = null, extraex = null;
- if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
- if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
- if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
- sqlDbQuery('INSERT INTO main VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET type = $2, domain = $3, extra = $4, extraex = $5, doc = $6;', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, JSON.stringify(performTypedRecordEncrypt(value))], func);
- }
- obj.Get = function (_id, func) {
- sqlDbQuery('SELECT doc FROM main WHERE id = $1', [_id], function (err, docs) {
- if ((docs != null) && (docs.length > 0)) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- }
- obj.GetAll = function (func) {
- sqlDbQuery('SELECT domain, doc FROM main', null, function (err, docs) {
- if ((docs != null) && (docs.length > 0)) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- }
- obj.GetHash = function (id, func) {
- sqlDbQuery('SELECT doc FROM main WHERE id = $1', [id], function (err, docs) {
- if ((docs != null) && (docs.length > 0)) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- }
- obj.GetAllTypeNoTypeField = function (type, domain, func) {
- sqlDbQuery('SELECT doc FROM main WHERE type = $1 AND domain = $2', [type, domain], function (err, docs) {
- if ((docs != null) && (docs.length > 0)) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- };
- obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
- if (limit == 0) { limit = -1; } // In SQLite, no limit is -1
- if (id && (id != '')) {
- sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra IN (' + dbMergeSqlArray(meshes) + ')) LIMIT $4 OFFSET $5', [id, type, domain, limit, skip], function (err, docs) {
- if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- } else {
- if (extrasids == null) {
- sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra IN (' + dbMergeSqlArray(meshes) + ')) LIMIT $3 OFFSET $4', [type, domain, limit, skip], function (err, docs) {
- if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- } else {
- sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra IN (' + dbMergeSqlArray(meshes) + ')) OR (id IN (' + dbMergeSqlArray(extrasids) + '))) LIMIT $3 OFFSET $4', [type, domain, limit, skip], function (err, docs) {
- if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- }
- }
- };
- obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
- if (id && (id != '')) {
- sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra IN (' + dbMergeSqlArray(meshes) + '))', [id, type, domain], function (err, docs) {
- func(err, (err == null) ? docs[0]['COUNT(doc)'] : null);
- });
- } else {
- if (extrasids == null) {
- sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (type = $1) AND (domain = $2) AND (extra IN (' + dbMergeSqlArray(meshes) + '))', [type, domain], function (err, docs) {
- func(err, (err == null) ? docs[0]['COUNT(doc)'] : null);
- });
- } else {
- sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (type = $1) AND (domain = $2) AND ((extra IN (' + dbMergeSqlArray(meshes) + ')) OR (id IN (' + dbMergeSqlArray(extrasids) + ')))', [type, domain], function (err, docs) {
- func(err, (err == null) ? docs[0]['COUNT(doc)'] : null);
- });
- }
- }
- };
- obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
- if (id && (id != '')) {
- sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra IN (' + dbMergeSqlArray(nodes) + '))', [id, type, domain], function (err, docs) {
- if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- } else {
- sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra IN (' + dbMergeSqlArray(nodes) + '))', [type, domain], function (err, docs) {
- if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- }
- };
- obj.GetAllType = function (type, func) {
- sqlDbQuery('SELECT doc FROM main WHERE type = $1', [type], function (err, docs) {
- if (docs != null) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- }
- obj.GetAllIdsOfType = function (ids, domain, type, func) {
- sqlDbQuery('SELECT doc FROM main WHERE (id IN (' + dbMergeSqlArray(ids) + ')) AND domain = $1 AND type = $2', [domain, type], function (err, docs) {
- if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- }
- obj.GetUserWithEmail = function (domain, email, func) {
- sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extra = $2', [domain, 'email/' + email], function (err, docs) {
- if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- }
- obj.GetUserWithVerifiedEmail = function (domain, email, func) {
- sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extra = $2', [domain, 'email/' + email], function (err, docs) {
- if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- }
- obj.Remove = function (id, func) { sqlDbQuery('DELETE FROM main WHERE id = $1', [id], func); };
- obj.RemoveAll = function (func) { sqlDbQuery('DELETE FROM main', null, func); };
- obj.RemoveAllOfType = function (type, func) { sqlDbQuery('DELETE FROM main WHERE type = $1', [type], func); };
- obj.InsertMany = function (data, func) { var pendingOps = 0; for (var i in data) { pendingOps++; obj.SetRaw(data[i], function () { if (--pendingOps == 0) { func(); } }); } }; // Insert records directly, no link escaping
- obj.RemoveMeshDocuments = function (id, func) { sqlDbQuery('DELETE FROM main WHERE extra = $1', [id], function () { sqlDbQuery('DELETE FROM main WHERE id = $1', ['nt' + id], func); }); };
- obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
- obj.DeleteDomain = function (domain, func) { sqlDbQuery('DELETE FROM main WHERE domain = $1', [domain], func); };
- obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
- obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
- obj.getLocalAmtNodes = function (func) {
- sqlDbQuery('SELECT doc FROM main WHERE (type = \'node\') AND (extraex IS NULL)', null, function (err, docs) {
- if (docs != null) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- var r = []; if (err == null) { for (var i in docs) { if (docs[i].host != null && docs[i].intelamt != null) { r.push(docs[i]); } } } func(err, r);
- });
- };
- obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) {
- sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extraex = $2', [domainid, 'uuid/' + uuid], function (err, docs) {
- if (docs != null) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, docs);
- });
- };
- obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { sqlDbExec('SELECT COUNT(id) FROM main WHERE domain = $1 AND type = $2', [domainid, type], function (err, response) { func((response['COUNT(id)'] == null) || (response['COUNT(id)'] > max), response['COUNT(id)']) }); } }
-
- // Database actions on the events collection
- obj.GetAllEvents = function (func) {
- sqlDbQuery('SELECT doc FROM events', null, func);
- };
- obj.StoreEvent = function (event, func) {
- obj.dbCounters.eventsSet++;
- sqlDbQuery('INSERT INTO events VALUES (NULL, $1, $2, $3, $4, $5, $6) RETURNING id', [event.time, ((typeof event.domain == 'string') ? event.domain : null), event.action, event.nodeid ? event.nodeid : null, event.userid ? event.userid : null, JSON.stringify(event)], function (err, docs) {
- if(func){ func(); }
- if ((err == null) && (docs[0].id)) {
- for (var i in event.ids) {
- if (event.ids[i] != '*') {
- obj.pendingTransfer++;
- sqlDbQuery('INSERT INTO eventids VALUES ($1, $2)', [docs[0].id, event.ids[i]], function(){ if(func){ func(); } });
- }
- }
- }
- });
- };
- obj.GetEvents = function (ids, domain, filter, func) {
- var query = "SELECT doc FROM events ";
- var dataarray = [domain];
- if (ids.indexOf('*') >= 0) {
- query = query + "WHERE (domain = $1";
- if (filter != null) {
- query = query + " AND action = $2";
- dataarray.push(filter);
- }
- query = query + ") ORDER BY time DESC";
- } else {
- query = query + 'JOIN eventids ON id = fkid WHERE (domain = $1 AND (target IN (' + dbMergeSqlArray(ids) + '))';
- if (filter != null) {
- query = query + " AND action = $2";
- dataarray.push(filter);
- }
- query = query + ") GROUP BY id ORDER BY time DESC ";
- }
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
- var query = "SELECT doc FROM events ";
- var dataarray = [domain];
- if (ids.indexOf('*') >= 0) {
- query = query + "WHERE (domain = $1";
- if (filter != null) {
- query = query + " AND action = $2) ORDER BY time DESC LIMIT $3";
- dataarray.push(filter);
- } else {
- query = query + ") ORDER BY time DESC LIMIT $2";
- }
- } else {
- query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND (target IN (" + dbMergeSqlArray(ids) + "))";
- if (filter != null) {
- query = query + " AND action = $2) GROUP BY id ORDER BY time DESC LIMIT $3";
- dataarray.push(filter);
- } else {
- query = query + ") GROUP BY id ORDER BY time DESC LIMIT $2";
- }
- }
- dataarray.push(limit);
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetUserEvents = function (ids, domain, userid, filter, func) {
- var query = "SELECT doc FROM events ";
- var dataarray = [domain, userid];
- if (ids.indexOf('*') >= 0) {
- query = query + "WHERE (domain = $1 AND userid = $2";
- if (filter != null) {
- query = query + " AND action = $3";
- dataarray.push(filter);
- }
- query = query + ") ORDER BY time DESC";
- } else {
- query = query + 'JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target IN (' + dbMergeSqlArray(ids) + '))';
- if (filter != null) {
- query = query + " AND action = $3";
- dataarray.push(filter);
- }
- query = query + ") GROUP BY id ORDER BY time DESC";
- }
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
- var query = "SELECT doc FROM events ";
- var dataarray = [domain, userid];
- if (ids.indexOf('*') >= 0) {
- query = query + "WHERE (domain = $1 AND userid = $2";
- if (filter != null) {
- query = query + " AND action = $3) ORDER BY time DESC LIMIT $4";
- dataarray.push(filter);
- } else {
- query = query + ") ORDER BY time DESC LIMIT $3";
- }
- } else {
- query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target IN (" + dbMergeSqlArray(ids) + "))";
- if (filter != null) {
- query = query + " AND action = $3) GROUP BY id ORDER BY time DESC LIMIT $4";
- dataarray.push(filter);
- } else {
- query = query + ") GROUP BY id ORDER BY time DESC LIMIT $3";
- }
- }
- dataarray.push(limit);
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
- if (ids.indexOf('*') >= 0) {
- sqlDbQuery('SELECT doc FROM events WHERE ((domain = $1) AND (time BETWEEN $2 AND $3)) ORDER BY time', [domain, start, end], func);
- } else {
- sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE ((domain = $1) AND (target IN (' + dbMergeSqlArray(ids) + ')) AND (time BETWEEN $2 AND $3)) GROUP BY id ORDER BY time', [domain, start, end], func);
- }
- };
- //obj.GetUserLoginEvents = function (domain, userid, func) { } // TODO
- obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
- var query = "SELECT doc FROM events WHERE (nodeid = $1 AND domain = $2";
- var dataarray = [nodeid, domain];
- if (filter != null) {
- query = query + " AND action = $3) ORDER BY time DESC LIMIT $4";
- dataarray.push(filter);
- } else {
- query = query + ") ORDER BY time DESC LIMIT $3";
- }
- dataarray.push(limit);
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
- var query = "SELECT doc FROM events WHERE (nodeid = $1) AND (domain = $2) AND ((userid = $3) OR (userid IS NULL)) ";
- var dataarray = [nodeid, domain, userid];
- if (filter != null) {
- query = query + "AND (action = $4) ORDER BY time DESC LIMIT $5";
- dataarray.push(filter);
- } else {
- query = query + "ORDER BY time DESC LIMIT $4";
- }
- dataarray.push(limit);
- sqlDbQuery(query, dataarray, func);
- };
- obj.RemoveAllEvents = function (domain) { sqlDbQuery('DELETE FROM events', null, function (err, docs) { }); };
- obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = $1 AND nodeid = $2', [domain, nodeid], function (err, docs) { }); };
- obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = $1 AND userid = $2', [domain, userid], function (err, docs) { }); };
- obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) { sqlDbQuery('SELECT COUNT(*) FROM events WHERE action = \'authfail\' AND domain = $1 AND userid = $2 AND time > $3', [domainid, userid, lastlogin], function (err, response) { func(err == null ? response[0]['COUNT(*)'] : 0); }); }
-
- // Database actions on the power collection
- obj.getAllPower = function (func) { sqlDbQuery('SELECT doc FROM power', null, func); };
- obj.storePowerEvent = function (event, multiServer, func) { obj.dbCounters.powerSet++; if (multiServer != null) { event.server = multiServer.serverid; } sqlDbQuery('INSERT INTO power VALUES (NULL, $1, $2, $3)', [event.time, event.nodeid ? event.nodeid : null, JSON.stringify(event)], func); };
- obj.getPowerTimeline = function (nodeid, func) { sqlDbQuery('SELECT doc FROM power WHERE ((nodeid = $1) OR (nodeid = \'*\')) ORDER BY time ASC', [nodeid], func); };
- obj.removeAllPowerEvents = function () { sqlDbQuery('DELETE FROM power', null, function (err, docs) { }); };
- obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; sqlDbQuery('DELETE FROM power WHERE nodeid = $1', [nodeid], function (err, docs) { }); };
-
- // Database actions on the SMBIOS collection
- obj.GetAllSMBIOS = function (func) { sqlDbQuery('SELECT doc FROM smbios', null, func); };
- obj.SetSMBIOS = function (smbios, func) { var expire = new Date(smbios.time); expire.setMonth(expire.getMonth() + 6); sqlDbQuery('INSERT INTO smbios VALUES ($1, $2, $3, $4) ON CONFLICT (id) DO UPDATE SET time = $2, expire = $3, doc = $4', [smbios._id, smbios.time, expire, JSON.stringify(smbios)], func); };
- obj.RemoveSMBIOS = function (id) { sqlDbQuery('DELETE FROM smbios WHERE id = $1', [id], function (err, docs) { }); };
- obj.GetSMBIOS = function (id, func) { sqlDbQuery('SELECT doc FROM smbios WHERE id = $1', [id], func); };
-
- // Database actions on the Server Stats collection
- obj.SetServerStats = function (data, func) { sqlDbQuery('INSERT INTO serverstats VALUES ($1, $2, $3) ON CONFLICT (time) DO UPDATE SET expire = $2, doc = $3', [data.time, data.expire, JSON.stringify(data)], func); };
- obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); sqlDbQuery('SELECT doc FROM serverstats WHERE time > $1', [t], func); }; // TODO: Expire old entries
-
- // Read a configuration file from the database
- obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
-
- // Write a configuration file to the database
- obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
-
- // List all configuration files
- obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM main WHERE type = "cfile" ORDER BY id', func); }
-
- // Get database information (TODO: Complete this)
- obj.getDbStats = function (func) {
- obj.stats = { c: 4 };
- sqlDbQuery('SELECT COUNT(*) FROM main', null, function (err, response) { obj.stats.meshcentral = (err == null ? response[0]['COUNT(*)'] : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- sqlDbQuery('SELECT COUNT(*) FROM serverstats', null, function (err, response) { obj.stats.serverstats = (err == null ? response[0]['COUNT(*)'] : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- sqlDbQuery('SELECT COUNT(*) FROM power', null, function (err, response) { obj.stats.power = (err == null ? response[0]['COUNT(*)'] : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- sqlDbQuery('SELECT COUNT(*) FROM smbios', null, function (err, response) { obj.stats.smbios = (err == null ? response[0]['COUNT(*)'] : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- }
-
- // Plugin operations
- if (obj.pluginsActive) {
- obj.addPlugin = function (plugin, func) { sqlDbQuery('INSERT INTO plugin VALUES (NULL, $1)', [JSON.stringify(plugin)], func); }; // Add a plugin
- obj.getPlugins = function (func) { sqlDbQuery('SELECT JSON_INSERT(doc, "$._id", id) as doc FROM plugin', null, func); }; // Get all plugins
- obj.getPlugin = function (id, func) { sqlDbQuery('SELECT JSON_INSERT(doc, "$._id", id) as doc FROM plugin WHERE id = $1', [id], func); }; // Get plugin
- obj.deletePlugin = function (id, func) { sqlDbQuery('DELETE FROM plugin WHERE id = $1', [id], func); }; // Delete plugin
- obj.setPluginStatus = function (id, status, func) { sqlDbQuery('UPDATE plugin SET doc=JSON_SET(doc,"$.status",$1) WHERE id=$2', [status,id], func); };
- obj.updatePlugin = function (id, args, func) { delete args._id; sqlDbQuery('UPDATE plugin SET doc=json_patch(doc,$1) WHERE id=$2', [JSON.stringify(args),id], func); };
- }
- } else if (obj.databaseType == DB_ACEBASE) {
- // Database actions on the main collection. AceBase: https://github.com/appy-one/acebase
- obj.Set = function (data, func) {
- data = common.escapeLinksFieldNameEx(data);
- var xdata = performTypedRecordEncrypt(data);
- obj.dbCounters.fileSet++;
- obj.file.ref('meshcentral').child(encodeURIComponent(xdata._id)).set(common.aceEscapeFieldNames(xdata)).then(function (ref) { if (func) { func(); } })
- };
- obj.Get = function (id, func) {
- obj.file.ref('meshcentral').child(encodeURIComponent(id)).get(function (snapshot) {
- if (snapshot.exists()) { func(null, performTypedRecordDecrypt([common.aceUnEscapeFieldNames(snapshot.val())])); } else { func(null, []); }
- });
- };
- obj.GetAll = function (func) {
- obj.file.ref('meshcentral').get(function(snapshot) {
- const val = snapshot.val();
- const docs = Object.keys(val).map(function(key) { return val[key]; });
- func(null, common.aceUnEscapeAllFieldNames(docs));
- });
- };
- obj.GetHash = function (id, func) {
- obj.file.ref('meshcentral').child(encodeURIComponent(id)).get({ include: ['hash'] }, function (snapshot) {
- if (snapshot.exists()) { func(null, snapshot.val()); } else { func(null, null); }
- });
- };
- obj.GetAllTypeNoTypeField = function (type, domain, func) {
- obj.file.query('meshcentral').filter('type', '==', type).filter('domain', '==', domain).get({ exclude: ['type'] }, function (snapshots) {
- const docs = [];
- for (var i in snapshots) { const x = snapshots[i].val(); docs.push(x); }
- func(null, common.aceUnEscapeAllFieldNames(docs));
- });
- }
- obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
- if (meshes.length == 0) { func(null, []); return; }
- var query = obj.file.query('meshcentral').skip(skip).take(limit).filter('type', '==', type).filter('domain', '==', domain);
- if (id) { query = query.filter('_id', '==', id); }
- if (extrasids == null) {
- query = query.filter('meshid', 'in', meshes);
- query.get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); });
- } else {
- // TODO: This is a slow query as we did not find a filter-or-filter, so we query everything and filter manualy.
- query.get(function (snapshots) {
- const docs = [];
- for (var i in snapshots) { const x = snapshots[i].val(); if ((extrasids.indexOf(x._id) >= 0) || (meshes.indexOf(x.meshid) >= 0)) { docs.push(x); } }
- func(null, performTypedRecordDecrypt(docs));
- });
- }
- };
- obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
- var query = obj.file.query('meshcentral').filter('type', '==', type).filter('domain', '==', domain).filter('nodeid', 'in', nodes);
- if (id) { query = query.filter('_id', '==', id); }
- query.get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); });
- };
- obj.GetAllType = function (type, func) {
- obj.file.query('meshcentral').filter('type', '==', type).get(function (snapshots) {
- const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); }
- func(null, common.aceUnEscapeAllFieldNames(performTypedRecordDecrypt(docs)));
- });
- };
- obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.query('meshcentral').filter('_id', 'in', ids).filter('domain', '==', domain).filter('type', '==', type).get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
- obj.GetUserWithEmail = function (domain, email, func) { obj.file.query('meshcentral').filter('type', '==', 'user').filter('domain', '==', domain).filter('email', '==', email).get({ exclude: ['type'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
- obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.query('meshcentral').filter('type', '==', 'user').filter('domain', '==', domain).filter('email', '==', email).filter('emailVerified', '==', true).get({ exclude: ['type'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
- obj.Remove = function (id, func) { obj.file.ref('meshcentral').child(encodeURIComponent(id)).remove().then(function () { if (func) { func(); } }); };
- obj.RemoveAll = function (func) { obj.file.query('meshcentral').remove().then(function () { if (func) { func(); } }); };
- obj.RemoveAllOfType = function (type, func) { obj.file.query('meshcentral').filter('type', '==', type).remove().then(function () { if (func) { func(); } }); };
- obj.InsertMany = function (data, func) { var r = {}; for (var i in data) { const ref = obj.file.ref('meshcentral').child(encodeURIComponent(data[i]._id)); r[ref.key] = common.aceEscapeFieldNames(data[i]); } obj.file.ref('meshcentral').set(r).then(function (ref) { func(); }); }; // Insert records directly, no link escaping
- obj.RemoveMeshDocuments = function (id) { obj.file.query('meshcentral').filter('meshid', '==', id).remove(); obj.file.ref('meshcentral').child(encodeURIComponent('nt' + id)).remove(); };
- obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
- obj.DeleteDomain = function (domain, func) { obj.file.query('meshcentral').filter('domain', '==', domain).remove().then(function () { if (func) { func(); } }); };
- obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
- obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
- obj.getLocalAmtNodes = function (func) { obj.file.query('meshcentral').filter('type', '==', 'node').filter('host', 'exists').filter('host', '!=', null).filter('intelamt', 'exists').get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
- obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { obj.file.query('meshcentral').filter('type', '==', 'node').filter('domain', '==', domainid).filter('mtype', '!=', mtype).filter('intelamt.uuid', '==', uuid).get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
- obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { obj.file.query('meshcentral').filter('type', '==', type).filter('domain', '==', domainid).get({ snapshots: false }, function (snapshots) { func((snapshots.length > max), snapshots.length); }); } }
-
- // Database actions on the events collection
- obj.GetAllEvents = function (func) {
- obj.file.ref('events').get(function (snapshot) {
- const val = snapshot.val();
- const docs = Object.keys(val).map(function(key) { return val[key]; });
- func(null, docs);
- })
- };
- obj.StoreEvent = function (event, func) {
- if (typeof event.account == 'object') { event = Object.assign({}, event); event.account = common.aceEscapeFieldNames(event.account); }
- obj.dbCounters.eventsSet++;
- obj.file.ref('events').push(event).then(function (userRef) { if (func) { func(); } });
- };
- obj.GetEvents = function (ids, domain, filter, func) {
- // This request is slow since we have not found a .filter() that will take two arrays and match a single item.
- if (filter != null) {
- obj.file.query('events').filter('domain', '==', domain).filter('action', '==', filter).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) {
- const docs = [];
- for (var i in snapshots) {
- const doc = snapshots[i].val();
- if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue;
- var found = false;
- for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches
- if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); }
- }
- func(null, docs);
- });
- } else {
- obj.file.query('events').filter('domain', '==', domain).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) {
- const docs = [];
- for (var i in snapshots) {
- const doc = snapshots[i].val();
- if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue;
- var found = false;
- for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches
- if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); }
- }
- func(null, docs);
- });
- }
- };
- obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
- // This request is slow since we have not found a .filter() that will take two arrays and match a single item.
- // TODO: Request a new AceBase feature for a 'array:contains-one-of' filter:
- // obj.file.indexes.create('events', 'ids', { type: 'array' });
- // db.query('events').filter('ids', 'array:contains-one-of', ids)
- if (filter != null) {
- obj.file.query('events').filter('domain', '==', domain).filter('action', '==', filter).take(limit).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) {
- const docs = [];
- for (var i in snapshots) {
- const doc = snapshots[i].val();
- if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue;
- var found = false;
- for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches
- if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); }
- }
- func(null, docs);
- });
- } else {
- obj.file.query('events').filter('domain', '==', domain).take(limit).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) {
- const docs = [];
- for (var i in snapshots) {
- const doc = snapshots[i].val();
- if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue;
- var found = false;
- for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches
- if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); }
- }
- func(null, docs);
- });
- }
- };
- obj.GetUserEvents = function (ids, domain, userid, filter, func) {
- if (filter != null) {
- obj.file.query('events').filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).filter('action', '==', filter).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
- } else {
- obj.file.query('events').filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
- }
- };
- obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
- if (filter != null) {
- obj.file.query('events').take(limit).filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).filter('action', '==', filter).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
- } else {
- obj.file.query('events').take(limit).filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
- }
- };
- obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
- obj.file.query('events').filter('domain', '==', domain).filter('ids', 'in', ids).filter('msgid', 'in', msgids).filter('time', 'between', [start, end]).sort('time', false).get({ exclude: ['type', '_id', 'domain', 'node'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
- };
- obj.GetUserLoginEvents = function (domain, userid, func) {
- obj.file.query('events').filter('domain', '==', domain).filter('action', 'in', ['authfail', 'login']).filter('userid', '==', userid).filter('msgArgs', 'exists').sort('time', false).get({ include: ['action', 'time', 'msgid', 'msgArgs', 'tokenName'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
- };
- obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
- if (filter != null) {
- obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).filter('action', '==', filter).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
- } else {
- obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
- }
- };
- obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
- if (filter != null) {
- obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).filter('userid', '==', userid).filter('action', '==', filter).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
- } else {
- obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).filter('userid', '==', userid).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
- }
- obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).filter('userid', '==', userid).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
- };
- obj.RemoveAllEvents = function (domain) {
- obj.file.query('events').filter('domain', '==', domain).remove().then(function () { if (func) { func(); } });;
- };
- obj.RemoveAllNodeEvents = function (domain, nodeid) {
- if ((domain == null) || (nodeid == null)) return;
- obj.file.query('events').filter('domain', '==', domain).filter('nodeid', '==', nodeid).remove().then(function () { if (func) { func(); } });;
- };
- obj.RemoveAllUserEvents = function (domain, userid) {
- if ((domain == null) || (userid == null)) return;
- obj.file.query('events').filter('domain', '==', domain).filter('userid', '==', userid).remove().then(function () { if (func) { func(); } });;
- };
- obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) {
- obj.file.query('events').filter('domain', '==', domainid).filter('userid', '==', userid).filter('time', '>', lastlogin).sort('time', false).get({ snapshots: false }, function (snapshots) { func(null, snapshots.length); });
- }
-
- // Database actions on the power collection
- obj.getAllPower = function (func) {
- obj.file.ref('power').get(function (snapshot) {
- const val = snapshot.val();
- const docs = Object.keys(val).map(function(key) { return val[key]; });
- func(null, docs);
- });
- };
- obj.storePowerEvent = function (event, multiServer, func) {
- if (multiServer != null) { event.server = multiServer.serverid; }
- obj.file.ref('power').push(event).then(function (userRef) { if (func) { func(); } });
- };
- obj.getPowerTimeline = function (nodeid, func) {
- obj.file.query('power').filter('nodeid', 'in', ['*', nodeid]).sort('time').get({ exclude: ['_id', 'nodeid', 's'] }, function (snapshots) {
- const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs);
- });
- };
- obj.removeAllPowerEvents = function () {
- obj.file.ref('power').remove().then(function () { if (func) { func(); } });
- };
- obj.removeAllPowerEventsForNode = function (nodeid) {
- if (nodeid == null) return;
- obj.file.query('power').filter('nodeid', '==', nodeid).remove().then(function () { if (func) { func(); } });
- };
-
- // Database actions on the SMBIOS collection
- if (obj.smbiosfile != null) {
- obj.GetAllSMBIOS = function (func) {
- obj.file.ref('smbios').get(function (snapshot) {
- const val = snapshot.val();
- const docs = Object.keys(val).map(function(key) { return val[key]; });
- func(null, docs);
- });
- };
- obj.SetSMBIOS = function (smbios, func) {
- obj.file.ref('meshcentral/' + encodeURIComponent(smbios._id)).set(smbios).then(function (ref) { if (func) { func(); } })
- };
- obj.RemoveSMBIOS = function (id) {
- obj.file.query('smbios').filter('_id', '==', id).remove().then(function () { if (func) { func(); } });
- };
- obj.GetSMBIOS = function (id, func) {
- obj.file.query('smbios').filter('_id', '==', id).get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
- };
- }
-
- // Database actions on the Server Stats collection
- obj.SetServerStats = function (data, func) {
- obj.file.ref('stats').push(data).then(function (userRef) { if (func) { func(); } });
- };
- obj.GetServerStats = function (hours, func) {
- var t = new Date();
- t.setTime(t.getTime() - (60 * 60 * 1000 * hours));
- obj.file.query('stats').filter('time', '>', t).get({ exclude: ['_id', 'cpu'] }, function (snapshots) {
- const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs);
- });
- };
-
- // Read a configuration file from the database
- obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
-
- // Write a configuration file to the database
- obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
-
- // List all configuration files
- obj.listConfigFiles = function (func) {
- obj.file.query('meshcentral').filter('type', '==', 'cfile').sort('_id').get(function (snapshots) {
- const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs);
- });
- }
-
- // Get database information
- obj.getDbStats = function (func) {
- obj.stats = { c: 5 };
- obj.file.ref('meshcentral').count().then(function (count) { obj.stats.meshcentral = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- obj.file.ref('events').count().then(function (count) { obj.stats.events = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- obj.file.ref('power').count().then(function (count) { obj.stats.power = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- obj.file.ref('smbios').count().then(function (count) { obj.stats.smbios = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- obj.file.ref('stats').count().then(function (count) { obj.stats.serverstats = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- }
-
- // Plugin operations
- if (obj.pluginsActive) {
- obj.addPlugin = function (plugin, func) { plugin.type = 'plugin'; obj.file.ref('plugin').child(encodeURIComponent(plugin._id)).set(plugin).then(function (ref) { if (func) { func(); } }) }; // Add a plugin
- obj.getPlugins = function (func) {
- obj.file.ref('plugin').get({ exclude: ['type'] }, function (snapshot) {
- const val = snapshot.val();
- const docs = Object.keys(val).map(function(key) { return val[key]; }).sort(function(a, b) { return a.name < b.name ? -1 : 1 });
- func(null, docs);
- });
- }; // Get all plugins
- obj.getPlugin = function (id, func) { obj.file.query('plugin').filter('_id', '==', id).get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); }); }; // Get plugin
- obj.deletePlugin = function (id, func) { obj.file.ref('plugin').child(encodeURIComponent(id)).remove().then(function () { if (func) { func(); } }); }; // Delete plugin
- obj.setPluginStatus = function (id, status, func) { obj.file.ref('plugin').child(encodeURIComponent(id)).update({ status: status }).then(function (ref) { if (func) { func(); } }) };
- obj.updatePlugin = function (id, args, func) { delete args._id; obj.file.ref('plugin').child(encodeURIComponent(id)).set(args).then(function (ref) { if (func) { func(); } }) };
- }
- } else if (obj.databaseType == DB_POSTGRESQL) {
- // Database actions on the main collection (Postgres)
- obj.Set = function (value, func) {
- obj.dbCounters.fileSet++;
- var extra = null, extraex = null;
- value = common.escapeLinksFieldNameEx(value);
- if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
- if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
- if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
- sqlDbQuery('INSERT INTO main VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET type = $2, domain = $3, extra = $4, extraex = $5, doc = $6;', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, performTypedRecordEncrypt(value)], func);
- }
- obj.SetRaw = function (value, func) {
- obj.dbCounters.fileSet++;
- var extra = null, extraex = null;
- if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
- if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
- if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
- sqlDbQuery('INSERT INTO main VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET type = $2, domain = $3, extra = $4, extraex = $5, doc = $6;', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, performTypedRecordEncrypt(value)], func);
- }
- obj.Get = function (_id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = $1', [_id], function (err, docs) { if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); } func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetAll = function (func) { sqlDbQuery('SELECT domain, doc FROM main', null, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetHash = function (id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = $1', [id], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetAllTypeNoTypeField = function (type, domain, func) { sqlDbQuery('SELECT doc FROM main WHERE type = $1 AND domain = $2', [type, domain], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
- if (limit == 0) { limit = 0xFFFFFFFF; }
- if (id && (id != '')) {
- sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra = ANY ($4)) LIMIT $5 OFFSET $6', [id, type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
- } else {
- if (extrasids == null) {
- sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3)) LIMIT $4 OFFSET $5', [type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }, true);
- } else {
- sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra = ANY ($3)) OR (id = ANY ($4))) LIMIT $5 OFFSET $6', [type, domain, meshes, extrasids, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
- }
- }
- };
- obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
- if (id && (id != '')) {
- sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra = ANY ($4))', [id, type, domain, meshes], function (err, docs) { func(err, docs); });
- } else {
- if (extrasids == null) {
- sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3))', [type, domain, meshes], function (err, docs) { func(err, docs); }, true);
- } else {
- sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (type = $1) AND (domain = $2) AND ((extra = ANY ($3)) OR (id = ANY ($4)))', [type, domain, meshes, extrasids], function (err, docs) { func(err, docs); });
- }
- }
- };
- obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
- if (id && (id != '')) {
- sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra = ANY ($4))', [id, type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
- } else {
- sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3))', [type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
- }
- };
- obj.GetAllType = function (type, func) { sqlDbQuery('SELECT doc FROM main WHERE type = $1', [type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetAllIdsOfType = function (ids, domain, type, func) { sqlDbQuery('SELECT doc FROM main WHERE (id = ANY ($1)) AND domain = $2 AND type = $3', [ids, domain, type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetUserWithEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extra = $2', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetUserWithVerifiedEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extra = $2', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.Remove = function (id, func) { sqlDbQuery('DELETE FROM main WHERE id = $1', [id], func); };
- obj.RemoveAll = function (func) { sqlDbQuery('DELETE FROM main', null, func); };
- obj.RemoveAllOfType = function (type, func) { sqlDbQuery('DELETE FROM main WHERE type = $1', [type], func); };
- obj.InsertMany = function (data, func) { var pendingOps = 0; for (var i in data) { pendingOps++; obj.SetRaw(data[i], function () { if (--pendingOps == 0) { func(); } }); } }; // Insert records directly, no link escaping
- obj.RemoveMeshDocuments = function (id, func) { sqlDbQuery('DELETE FROM main WHERE extra = $1', [id], function () { sqlDbQuery('DELETE FROM main WHERE id = $1', ['nt' + id], func); }); };
- obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
- obj.DeleteDomain = function (domain, func) { sqlDbQuery('DELETE FROM main WHERE domain = $1', [domain], func); };
- obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
- obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
- obj.getLocalAmtNodes = function (func) { sqlDbQuery('SELECT doc FROM main WHERE (type = \'node\') AND (extraex IS NULL)', null, function (err, docs) { var r = []; if (err == null) { for (var i in docs) { if (docs[i].host != null && docs[i].intelamt != null) { r.push(docs[i]); } } } func(err, r); }); };
- obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extraex = $2', [domainid, 'uuid/' + uuid], func); };
- obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { sqlDbExec('SELECT COUNT(id) FROM main WHERE domain = $1 AND type = $2', [domainid, type], function (err, response) { func((response['COUNT(id)'] == null) || (response['COUNT(id)'] > max), response['COUNT(id)']) }); } }
-
- // Database actions on the events collection
- obj.GetAllEvents = function (func) { sqlDbQuery('SELECT doc FROM events', null, func); };
- obj.StoreEvent = function (event, func) {
- obj.dbCounters.eventsSet++;
- sqlDbQuery('INSERT INTO events VALUES (DEFAULT, $1, $2, $3, $4, $5, $6) RETURNING id', [event.time, ((typeof event.domain == 'string') ? event.domain : null), event.action, event.nodeid ? event.nodeid : null, event.userid ? event.userid : null, event], function (err, docs) {
- if(func){ func(); }
- if (docs.id) {
- for (var i in event.ids) {
- if (event.ids[i] != '*') {
- obj.pendingTransfer++;
- sqlDbQuery('INSERT INTO eventids VALUES ($1, $2)', [docs.id, event.ids[i]], function(){ if(func){ func(); } });
- }
- }
- }
- });
- };
- obj.GetEvents = function (ids, domain, filter, func) {
- var query = "SELECT doc FROM events ";
- var dataarray = [domain];
- if (ids.indexOf('*') >= 0) {
- query = query + "WHERE (domain = $1";
- if (filter != null) {
- query = query + " AND action = $2";
- dataarray.push(filter);
- }
- query = query + ") ORDER BY time DESC";
- } else {
- query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND (target = ANY ($2))";
- dataarray.push(ids);
- if (filter != null) {
- query = query + " AND action = $3";
- dataarray.push(filter);
- }
- query = query + ") GROUP BY id ORDER BY time DESC";
- }
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
- var query = "SELECT doc FROM events ";
- var dataarray = [domain];
- if (ids.indexOf('*') >= 0) {
- query = query + "WHERE (domain = $1";
- if (filter != null) {
- query = query + " AND action = $2) ORDER BY time DESC LIMIT $3";
- dataarray.push(filter);
- } else {
- query = query + ") ORDER BY time DESC LIMIT $2";
- }
- } else {
- if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND (target = ANY ($2))";
- dataarray.push(ids);
- if (filter != null) {
- query = query + " AND action = $3) ORDER BY time DESC LIMIT $4";
- dataarray.push(filter);
- } else {
- query = query + ") ORDER BY time DESC LIMIT $3";
- }
- }
- dataarray.push(limit);
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetUserEvents = function (ids, domain, userid, filter, func) {
- var query = "SELECT doc FROM events ";
- var dataarray = [domain, userid];
- if (ids.indexOf('*') >= 0) {
- query = query + "WHERE (domain = $1 AND userid = $2";
- if (filter != null) {
- query = query + " AND action = $3";
- dataarray.push(filter);
- }
- query = query + ") ORDER BY time DESC";
- } else {
- if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target = ANY ($3))";
- dataarray.push(ids);
- if (filter != null) {
- query = query + " AND action = $4";
- dataarray.push(filter);
- }
- query = query + ") GROUP BY id ORDER BY time DESC";
- }
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
- var query = "SELECT doc FROM events ";
- var dataarray = [domain, userid];
- if (ids.indexOf('*') >= 0) {
- query = query + "WHERE (domain = $1 AND userid = $2";
- if (filter != null) {
- query = query + " AND action = $3) ORDER BY time DESC LIMIT $4 ";
- dataarray.push(filter);
- } else {
- query = query + ") ORDER BY time DESC LIMIT $3";
- }
- } else {
- if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target = ANY ($3))";
- dataarray.push(ids);
- if (filter != null) {
- query = query + " AND action = $4) GROUP BY id ORDER BY time DESC LIMIT $5";
- dataarray.push(filter);
- } else {
- query = query + ") GROUP BY id ORDER BY time DESC LIMIT $4";
- }
- }
- dataarray.push(limit);
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
- if (ids.indexOf('*') >= 0) {
- sqlDbQuery('SELECT doc FROM events WHERE ((domain = $1) AND (time BETWEEN $2 AND $3)) ORDER BY time', [domain, start, end], func);
- } else {
- sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE ((domain = $1) AND (target = ANY ($2)) AND (time BETWEEN $3 AND $4)) GROUP BY id ORDER BY time', [domain, ids, start, end], func);
- }
- };
- //obj.GetUserLoginEvents = function (domain, userid, func) { } // TODO
- obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
- var query = "SELECT doc FROM events WHERE (nodeid = $1 AND domain = $2";
- var dataarray = [nodeid, domain];
- if (filter != null) {
- query = query + " AND action = $3) ORDER BY time DESC LIMIT $4";
- dataarray.push(filter);
- } else {
- query = query + ") ORDER BY time DESC LIMIT $3";
- }
- dataarray.push(limit);
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
- var query = "SELECT doc FROM events WHERE (nodeid = $1 AND domain = $2 AND ((userid = $3) OR (userid IS NULL))";
- var dataarray = [nodeid, domain, userid];
- if (filter != null) {
- query = query + " AND action = $4) ORDER BY time DESC LIMIT $5";
- dataarray.push(filter);
- } else {
- query = query + ") ORDER BY time DESC LIMIT $4";
- }
- dataarray.push(limit);
- sqlDbQuery(query, dataarray, func);
- };
- obj.RemoveAllEvents = function (domain) { sqlDbQuery('DELETE FROM events', null, function (err, docs) { }); };
- obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = $1 AND nodeid = $2', [domain, nodeid], function (err, docs) { }); };
- obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = $1 AND userid = $2', [domain, userid], function (err, docs) { }); };
- obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) { sqlDbQuery('SELECT COUNT(*) FROM events WHERE action = \'authfail\' AND domain = $1 AND userid = $2 AND time > $3', [domainid, userid, lastlogin], function (err, response, raw) { func(err == null ? parseInt(raw.rows[0].count) : 0); }); }
-
- // Database actions on the power collection
- obj.getAllPower = function (func) { sqlDbQuery('SELECT doc FROM power', null, func); };
- obj.storePowerEvent = function (event, multiServer, func) { obj.dbCounters.powerSet++; if (multiServer != null) { event.server = multiServer.serverid; } sqlDbQuery('INSERT INTO power VALUES (DEFAULT, $1, $2, $3)', [event.time, event.nodeid ? event.nodeid : null, event], func); };
- obj.getPowerTimeline = function (nodeid, func) { sqlDbQuery('SELECT doc FROM power WHERE ((nodeid = $1) OR (nodeid = \'*\')) ORDER BY time ASC', [nodeid], func); };
- obj.removeAllPowerEvents = function () { sqlDbQuery('DELETE FROM power', null, function (err, docs) { }); };
- obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; sqlDbQuery('DELETE FROM power WHERE nodeid = $1', [nodeid], function (err, docs) { }); };
-
- // Database actions on the SMBIOS collection
- obj.GetAllSMBIOS = function (func) { sqlDbQuery('SELECT doc FROM smbios', null, func); };
- obj.SetSMBIOS = function (smbios, func) { var expire = new Date(smbios.time); expire.setMonth(expire.getMonth() + 6); sqlDbQuery('INSERT INTO smbios VALUES ($1, $2, $3, $4) ON CONFLICT (id) DO UPDATE SET time = $2, expire = $3, doc = $4', [smbios._id, smbios.time, expire, smbios], func); };
- obj.RemoveSMBIOS = function (id) { sqlDbQuery('DELETE FROM smbios WHERE id = $1', [id], function (err, docs) { }); };
- obj.GetSMBIOS = function (id, func) { sqlDbQuery('SELECT doc FROM smbios WHERE id = $1', [id], func); };
-
- // Database actions on the Server Stats collection
- obj.SetServerStats = function (data, func) { sqlDbQuery('INSERT INTO serverstats VALUES ($1, $2, $3) ON CONFLICT (time) DO UPDATE SET expire = $2, doc = $3', [data.time, data.expire, data], func); };
- obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); sqlDbQuery('SELECT doc FROM serverstats WHERE time > $1', [t], func); }; // TODO: Expire old entries
-
- // Read a configuration file from the database
- obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
-
- // Write a configuration file to the database
- obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
-
- // List all configuration files
- obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM main WHERE type = "cfile" ORDER BY id', func); }
-
- // Get database information (TODO: Complete this)
- obj.getDbStats = function (func) {
- obj.stats = { c: 4 };
- sqlDbQuery('SELECT COUNT(*) FROM main', null, function (err, response, raw) { obj.stats.meshcentral = (err == null ? parseInt(raw.rows[0].count) : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- sqlDbQuery('SELECT COUNT(*) FROM serverstats', null, function (err, response, raw) { obj.stats.serverstats = (err == null ? parseInt(raw.rows[0].count) : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- sqlDbQuery('SELECT COUNT(*) FROM power', null, function (err, response, raw) { obj.stats.power = (err == null ? parseInt(raw.rows[0].count) : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- sqlDbQuery('SELECT COUNT(*) FROM smbios', null, function (err, response, raw) { obj.stats.smbios = (err == null ? parseInt(raw.rows[0].count) : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- }
-
- // Plugin operations
- if (obj.pluginsActive) {
- obj.addPlugin = function (plugin, func) { sqlDbQuery('INSERT INTO plugin VALUES (DEFAULT, $1)', [plugin], func); }; // Add a plugin
- obj.getPlugins = function (func) { sqlDbQuery("SELECT doc::jsonb || ('{\"_id\":' || plugin.id || '}')::jsonb as doc FROM plugin", null, func); }; // Get all plugins
- obj.getPlugin = function (id, func) { sqlDbQuery("SELECT doc::jsonb || ('{\"_id\":' || plugin.id || '}')::jsonb as doc FROM plugin WHERE id = $1", [id], func); }; // Get plugin
- obj.deletePlugin = function (id, func) { sqlDbQuery('DELETE FROM plugin WHERE id = $1', [id], func); }; // Delete plugin
- obj.setPluginStatus = function (id, status, func) { sqlDbQuery("UPDATE plugin SET doc= jsonb_set(doc::jsonb,'{status}',$1) WHERE id=$2", [status,id], func); };
- obj.updatePlugin = function (id, args, func) { delete args._id; sqlDbQuery('UPDATE plugin SET doc= doc::jsonb || ($1) WHERE id=$2', [args,id], func); };
- }
- } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
- // Database actions on the main collection (MariaDB or MySQL)
- obj.Set = function (value, func) {
- obj.dbCounters.fileSet++;
- var extra = null, extraex = null;
- value = common.escapeLinksFieldNameEx(value);
- if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
- if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
- if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
- sqlDbQuery('REPLACE INTO main VALUE (?, ?, ?, ?, ?, ?)', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, JSON.stringify(performTypedRecordEncrypt(value))], func);
- }
- obj.SetRaw = function (value, func) {
- obj.dbCounters.fileSet++;
- var extra = null, extraex = null;
- if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
- if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
- if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
- sqlDbQuery('REPLACE INTO main VALUE (?, ?, ?, ?, ?, ?)', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, JSON.stringify(performTypedRecordEncrypt(value))], func);
- }
- obj.Get = function (_id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = ?', [_id], function (err, docs) { if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); } func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetAll = function (func) { sqlDbQuery('SELECT domain, doc FROM main', null, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetHash = function (id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = ?', [id], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetAllTypeNoTypeField = function (type, domain, func) { sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ?', [type, domain], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
- if (limit == 0) { limit = 0xFFFFFFFF; }
- if ((meshes == null) || (meshes.length == 0)) { meshes = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- if ((extrasids == null) || (extrasids.length == 0)) { extrasids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- if (id && (id != '')) {
- sqlDbQuery('SELECT doc FROM main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?) LIMIT ? OFFSET ?', [id, type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
- } else {
- sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND (extra IN (?) OR id IN (?)) LIMIT ? OFFSET ?', [type, domain, meshes, extrasids, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
- }
- };
- obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
- if ((meshes == null) || (meshes.length == 0)) { meshes = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- if ((extrasids == null) || (extrasids.length == 0)) { extrasids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- if (id && (id != '')) {
- sqlDbQuery('SELECT COUNT(doc) FROM main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?)', [id, type, domain, meshes], function (err, docs) { func(err, docs); });
- } else {
- sqlDbQuery('SELECT COUNT(doc) FROM main WHERE type = ? AND domain = ? AND (extra IN (?) OR id IN (?))', [type, domain, meshes, extrasids], function (err, docs) { func(err, docs); });
- }
- };
- obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
- if ((nodes == null) || (nodes.length == 0)) { nodes = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- if (id && (id != '')) {
- sqlDbQuery('SELECT doc FROM main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?)', [id, type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
- } else {
- sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND extra IN (?)', [type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
- }
- };
- obj.GetAllType = function (type, func) { sqlDbQuery('SELECT doc FROM main WHERE type = ?', [type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetAllIdsOfType = function (ids, domain, type, func) {
- if ((ids == null) || (ids.length == 0)) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- sqlDbQuery('SELECT doc FROM main WHERE id IN (?) AND domain = ? AND type = ?', [ids, domain, type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
- }
- obj.GetUserWithEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = ? AND extra = ?', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetUserWithVerifiedEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = ? AND extra = ?', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.Remove = function (id, func) { sqlDbQuery('DELETE FROM main WHERE id = ?', [id], func); };
- obj.RemoveAll = function (func) { sqlDbQuery('DELETE FROM main', null, func); };
- obj.RemoveAllOfType = function (type, func) { sqlDbQuery('DELETE FROM main WHERE type = ?', [type], func); };
- obj.InsertMany = function (data, func) { var pendingOps = 0; for (var i in data) { pendingOps++; obj.SetRaw(data[i], function () { if (--pendingOps == 0) { func(); } }); } }; // Insert records directly, no link escaping
- obj.RemoveMeshDocuments = function (id, func) { sqlDbQuery('DELETE FROM main WHERE extra = ?', [id], function () { sqlDbQuery('DELETE FROM main WHERE id = ?', ['nt' + id], func); } ); };
- obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
- obj.DeleteDomain = function (domain, func) { sqlDbQuery('DELETE FROM main WHERE domain = ?', [domain], func); };
- obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
- obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
- obj.getLocalAmtNodes = function (func) { sqlDbQuery('SELECT doc FROM main WHERE (type = "node") AND (extraex IS NULL)', null, function (err, docs) { var r = []; if (err == null) { for (var i in docs) { if (docs[i].host != null && docs[i].intelamt != null) { r.push(docs[i]); } } } func(err, r); }); };
- obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = ? AND extraex = ?', [domainid, 'uuid/' + uuid], func); };
- obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { sqlDbExec('SELECT COUNT(id) FROM main WHERE domain = ? AND type = ?', [domainid, type], function (err, response) { func((response['COUNT(id)'] == null) || (response['COUNT(id)'] > max), response['COUNT(id)']) }); } }
-
- // Database actions on the events collection
- obj.GetAllEvents = function (func) { sqlDbQuery('SELECT doc FROM events', null, func); };
- obj.StoreEvent = function (event, func) {
- obj.dbCounters.eventsSet++;
- var batchQuery = [['INSERT INTO events VALUE (?, ?, ?, ?, ?, ?, ?)', [null, event.time, ((typeof event.domain == 'string') ? event.domain : null), event.action, event.nodeid ? event.nodeid : null, event.userid ? event.userid : null, JSON.stringify(event)]]];
- for (var i in event.ids) { if (event.ids[i] != '*') { batchQuery.push(['INSERT INTO eventids VALUE (LAST_INSERT_ID(), ?)', [event.ids[i]]]); } }
- sqlDbBatchExec(batchQuery, function (err, docs) { if (func != null) { func(err, docs); } });
- };
- obj.GetEvents = function (ids, domain, filter, func) {
- var query = "SELECT doc FROM events ";
- var dataarray = [domain];
- if (ids.indexOf('*') >= 0) {
- query = query + "WHERE (domain = ?";
- if (filter != null) {
- query = query + " AND action = ?";
- dataarray.push(filter);
- }
- query = query + ") ORDER BY time DESC";
- } else {
- if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND target IN (?)";
- dataarray.push(ids);
- if (filter != null) {
- query = query + " AND action = ?";
- dataarray.push(filter);
- }
- query = query + ") GROUP BY id ORDER BY time DESC";
- }
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
- var query = "SELECT doc FROM events ";
- var dataarray = [domain];
- if (ids.indexOf('*') >= 0) {
- query = query + "WHERE (domain = ?";
- if (filter != null) {
- query = query + " AND action = ? ";
- dataarray.push(filter);
- }
- query = query + ") ORDER BY time DESC LIMIT ?";
- } else {
- if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND target IN (?)";
- dataarray.push(ids);
- if (filter != null) {
- query = query + " AND action = ?";
- dataarray.push(filter);
- }
- query = query + ") GROUP BY id ORDER BY time DESC LIMIT ?";
- }
- dataarray.push(limit);
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetUserEvents = function (ids, domain, userid, filter, func) {
- var query = "SELECT doc FROM events ";
- var dataarray = [domain, userid];
- if (ids.indexOf('*') >= 0) {
- query = query + "WHERE (domain = ? AND userid = ?";
- if (filter != null) {
- query = query + " AND action = ?";
- dataarray.push(filter);
- }
- query = query + ") ORDER BY time DESC";
- } else {
- if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND userid = ? AND target IN (?)";
- dataarray.push(ids);
- if (filter != null) {
- query = query + " AND action = ?";
- dataarray.push(filter);
- }
- query = query + ") GROUP BY id ORDER BY time DESC";
- }
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
- var query = "SELECT doc FROM events ";
- var dataarray = [domain, userid];
- if (ids.indexOf('*') >= 0) {
- query = query + "WHERE (domain = ? AND userid = ?";
- if (filter != null) {
- query = query + " AND action = ?";
- dataarray.push(filter);
- }
- query = query + ") ORDER BY time DESC LIMIT ?";
- } else {
- if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND userid = ? AND target IN (?)";
- dataarray.push(ids);
- if (filter != null) {
- query = query + " AND action = ?";
- dataarray.push(filter);
- }
- query = query + ") GROUP BY id ORDER BY time DESC LIMIT ?";
- }
- dataarray.push(limit);
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
- if (ids.indexOf('*') >= 0) {
- sqlDbQuery('SELECT doc FROM events WHERE ((domain = ?) AND (time BETWEEN ? AND ?)) ORDER BY time', [domain, start, end], func);
- } else {
- if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE ((domain = ?) AND (target IN (?)) AND (time BETWEEN ? AND ?)) GROUP BY id ORDER BY time', [domain, ids, start, end], func);
- }
- };
- //obj.GetUserLoginEvents = function (domain, userid, func) { } // TODO
- obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
- var query = "SELECT doc FROM events WHERE (nodeid = ? AND domain = ?";
- var dataarray = [nodeid, domain];
- if (filter != null) {
- query = query + " AND action = ?) ORDER BY time DESC LIMIT ?";
- dataarray.push(filter);
- } else {
- query = query + ") ORDER BY time DESC LIMIT ?";
- }
- dataarray.push(limit);
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
- var query = "SELECT doc FROM events WHERE (nodeid = ? AND domain = ? AND ((userid = ?) OR (userid IS NULL))";
- var dataarray = [nodeid, domain, userid];
- if (filter != null) {
- query = query + " AND action = ?) ORDER BY time DESC LIMIT ?";
- dataarray.push(filter);
- } else {
- query = query + ") ORDER BY time DESC LIMIT ?";
- }
- dataarray.push(limit);
- sqlDbQuery(query, dataarray, func);
- };
- obj.RemoveAllEvents = function (domain) { sqlDbQuery('DELETE FROM events', null, function (err, docs) { }); };
- obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = ? AND nodeid = ?', [domain, nodeid], function (err, docs) { }); };
- obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = ? AND userid = ?', [domain, userid], function (err, docs) { }); };
- obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) { sqlDbExec('SELECT COUNT(id) FROM events WHERE action = "authfail" AND domain = ? AND userid = ? AND time > ?', [domainid, userid, lastlogin], function (err, response) { func(err == null ? response['COUNT(id)'] : 0); }); }
-
- // Database actions on the power collection
- obj.getAllPower = function (func) { sqlDbQuery('SELECT doc FROM power', null, func); };
- obj.storePowerEvent = function (event, multiServer, func) { obj.dbCounters.powerSet++; if (multiServer != null) { event.server = multiServer.serverid; } sqlDbQuery('INSERT INTO power VALUE (?, ?, ?, ?)', [null, event.time, event.nodeid ? event.nodeid : null, JSON.stringify(event)], func); };
- obj.getPowerTimeline = function (nodeid, func) { sqlDbQuery('SELECT doc FROM power WHERE ((nodeid = ?) OR (nodeid = "*")) ORDER BY time ASC', [nodeid], func); };
- obj.removeAllPowerEvents = function () { sqlDbQuery('DELETE FROM power', null, function (err, docs) { }); };
- obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; sqlDbQuery('DELETE FROM power WHERE nodeid = ?', [nodeid], function (err, docs) { }); };
-
- // Database actions on the SMBIOS collection
- obj.GetAllSMBIOS = function (func) { sqlDbQuery('SELECT doc FROM smbios', null, func); };
- obj.SetSMBIOS = function (smbios, func) { var expire = new Date(smbios.time); expire.setMonth(expire.getMonth() + 6); sqlDbQuery('REPLACE INTO smbios VALUE (?, ?, ?, ?)', [smbios._id, smbios.time, expire, JSON.stringify(smbios)], func); };
- obj.RemoveSMBIOS = function (id) { sqlDbQuery('DELETE FROM smbios WHERE id = ?', [id], function (err, docs) { }); };
- obj.GetSMBIOS = function (id, func) { sqlDbQuery('SELECT doc FROM smbios WHERE id = ?', [id], func); };
-
- // Database actions on the Server Stats collection
- obj.SetServerStats = function (data, func) { sqlDbQuery('REPLACE INTO serverstats VALUE (?, ?, ?)', [data.time, data.expire, JSON.stringify(data)], func); };
- obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); sqlDbQuery('SELECT doc FROM serverstats WHERE time > ?', [t], func); };
-
- // Read a configuration file from the database
- obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
-
- // Write a configuration file to the database
- obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
-
- // List all configuration files
- obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM main WHERE type = "cfile" ORDER BY id', func); }
-
- // Get database information (TODO: Complete this)
- obj.getDbStats = function (func) {
- obj.stats = { c: 4 };
- sqlDbExec('SELECT COUNT(id) FROM main', null, function (err, response) { obj.stats.meshcentral = Number(response['COUNT(id)']); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- sqlDbExec('SELECT COUNT(time) FROM serverstats', null, function (err, response) { obj.stats.serverstats = Number(response['COUNT(time)']); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- sqlDbExec('SELECT COUNT(id) FROM power', null, function (err, response) { obj.stats.power = Number(response['COUNT(id)']); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- sqlDbExec('SELECT COUNT(id) FROM smbios', null, function (err, response) { obj.stats.smbios = Number(response['COUNT(id)']); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- }
-
- // Plugin operations
- if (obj.pluginsActive) {
- obj.addPlugin = function (plugin, func) { sqlDbQuery('INSERT INTO plugin VALUE (?, ?)', [null, JSON.stringify(plugin)], func); }; // Add a plugin
- obj.getPlugins = function (func) { sqlDbQuery('SELECT JSON_INSERT(doc, "$._id", id) as doc FROM plugin', null, func); }; // Get all plugins
- obj.getPlugin = function (id, func) { sqlDbQuery('SELECT JSON_INSERT(doc, "$._id", id) as doc FROM plugin WHERE id = ?', [id], func); }; // Get plugin
- obj.deletePlugin = function (id, func) { sqlDbQuery('DELETE FROM plugin WHERE id = ?', [id], func); }; // Delete plugin
- obj.setPluginStatus = function (id, status, func) { sqlDbQuery('UPDATE meshcentral.plugin SET doc=JSON_SET(doc,"$.status",?) WHERE id=?', [status,id], func); };
- obj.updatePlugin = function (id, args, func) { delete args._id; sqlDbQuery('UPDATE meshcentral.plugin SET doc=JSON_MERGE_PATCH(doc,?) WHERE id=?', [JSON.stringify(args),id], func); };
- }
- } else if (obj.databaseType == DB_MONGODB) {
- // Database actions on the main collection (MongoDB)
-
- // Bulk operations
- if (parent.config.settings.mongodbbulkoperations) {
- obj.Set = function (data, func) { // Fast Set operation using bulkWrite(), this is much faster then using replaceOne()
- if (obj.filePendingSet == false) {
- // Perform the operation now
- obj.dbCounters.fileSet++;
- obj.filePendingSet = true; obj.filePendingSets = null;
- if (func != null) { obj.filePendingCbs = [func]; }
- obj.file.bulkWrite([{ replaceOne: { filter: { _id: data._id }, replacement: performTypedRecordEncrypt(common.escapeLinksFieldNameEx(data)), upsert: true } }], fileBulkWriteCompleted);
- } else {
- // Add this operation to the pending list
- obj.dbCounters.fileSetPending++;
- if (obj.filePendingSets == null) { obj.filePendingSets = {} }
- obj.filePendingSets[data._id] = data;
- if (func != null) { if (obj.filePendingCb == null) { obj.filePendingCb = [func]; } else { obj.filePendingCb.push(func); } }
- }
- };
-
- obj.Get = function (id, func) { // Fast Get operation using a bulk find() to reduce round trips to the database.
- // Encode arguments into return function if any are present.
- var func2 = func;
- if (arguments.length > 2) {
- var parms = [func];
- for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
- var func2 = function _func2(arg1, arg2) {
- var userCallback = _func2.userArgs.shift();
- _func2.userArgs.unshift(arg2);
- _func2.userArgs.unshift(arg1);
- userCallback.apply(obj, _func2.userArgs);
- };
- func2.userArgs = parms;
- }
-
- if (obj.filePendingGets == null) {
- // No pending gets, perform the operation now.
- obj.filePendingGets = {};
- obj.filePendingGets[id] = [func2];
- obj.file.find({ _id: id }).toArray(fileBulkReadCompleted);
- } else {
- // Add get to pending list.
- if (obj.filePendingGet == null) { obj.filePendingGet = {}; }
- if (obj.filePendingGet[id] == null) { obj.filePendingGet[id] = [func2]; } else { obj.filePendingGet[id].push(func2); }
- }
- };
- } else {
- obj.Set = function (data, func) {
- obj.dbCounters.fileSet++;
- data = common.escapeLinksFieldNameEx(data);
- obj.file.replaceOne({ _id: data._id }, performTypedRecordEncrypt(data), { upsert: true }, func);
- };
- obj.Get = function (id, func) {
- if (arguments.length > 2) {
- var parms = [func];
- for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
- var func2 = function _func2(arg1, arg2) {
- var userCallback = _func2.userArgs.shift();
- _func2.userArgs.unshift(arg2);
- _func2.userArgs.unshift(arg1);
- userCallback.apply(obj, _func2.userArgs);
- };
- func2.userArgs = parms;
- obj.file.find({ _id: id }).toArray(function (err, docs) {
- if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
- func2(err, performTypedRecordDecrypt(docs));
- });
- } else {
- obj.file.find({ _id: id }).toArray(function (err, docs) {
- if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
- func(err, performTypedRecordDecrypt(docs));
- });
- }
- };
- }
- obj.GetAll = function (func) { obj.file.find({}).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetHash = function (id, func) { obj.file.find({ _id: id }).project({ _id: 0, hash: 1 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }).project({ type: 0 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
- if (extrasids == null) {
- const x = { type: type, domain: domain, meshid: { $in: meshes } };
- if (id) { x._id = id; }
- var f = obj.file.find(x, { type: 0 });
- if (skip > 0) f = f.skip(skip); // Skip records
- if (limit > 0) f = f.limit(limit); // Limit records
- f.toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
- } else {
- const x = { type: type, domain: domain, $or: [ { meshid: { $in: meshes } }, { _id: { $in: extrasids } } ] };
- if (id) { x._id = id; }
- var f = obj.file.find(x, { type: 0 });
- if (skip > 0) f = f.skip(skip); // Skip records
- if (limit > 0) f = f.limit(limit); // Limit records
- f.toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
- }
- };
- obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
- if (extrasids == null) {
- const x = { type: type, domain: domain, meshid: { $in: meshes } };
- if (id) { x._id = id; }
- var f = obj.file.find(x, { type: 0 });
- f.count(function (err, count) { func(err, count); });
- } else {
- const x = { type: type, domain: domain, $or: [{ meshid: { $in: meshes } }, { _id: { $in: extrasids } }] };
- if (id) { x._id = id; }
- var f = obj.file.find(x, { type: 0 });
- f.count(function (err, count) { func(err, count); });
- }
- };
- obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
- var x = { type: type, domain: domain, nodeid: { $in: nodes } };
- if (id) { x._id = id; }
- obj.file.find(x, { type: 0 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
- };
- obj.GetAllType = function (type, func) { obj.file.find({ type: type }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.find({ type: type, domain: domain, _id: { $in: ids } }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetUserWithEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email, emailVerified: true }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
-
- // Bulk operations
- if (parent.config.settings.mongodbbulkoperations) {
- obj.Remove = function (id, func) { // Fast remove operation using a bulk find() to reduce round trips to the database.
- if (obj.filePendingRemoves == null) {
- // No pending removes, perform the operation now.
- obj.dbCounters.fileRemove++;
- obj.filePendingRemoves = {};
- obj.filePendingRemoves[id] = [func];
- obj.file.deleteOne({ _id: id }, fileBulkRemoveCompleted);
- } else {
- // Add remove to pending list.
- obj.dbCounters.fileRemovePending++;
- if (obj.filePendingRemove == null) { obj.filePendingRemove = {}; }
- if (obj.filePendingRemove[id] == null) { obj.filePendingRemove[id] = [func]; } else { obj.filePendingRemove[id].push(func); }
- }
- };
- } else {
- obj.Remove = function (id, func) { obj.dbCounters.fileRemove++; obj.file.deleteOne({ _id: id }, func); };
- }
-
- obj.RemoveAll = function (func) { obj.file.deleteMany({}, { multi: true }, func); };
- obj.RemoveAllOfType = function (type, func) { obj.file.deleteMany({ type: type }, { multi: true }, func); };
- obj.InsertMany = function (data, func) { obj.file.insertMany(data, func); }; // Insert records directly, no link escaping
- obj.RemoveMeshDocuments = function (id) { obj.file.deleteMany({ meshid: id }, { multi: true }); obj.file.deleteOne({ _id: 'nt' + id }); };
- obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
- obj.DeleteDomain = function (domain, func) { obj.file.deleteMany({ domain: domain }, { multi: true }, func); };
- obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
- obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
- obj.getLocalAmtNodes = function (func) { obj.file.find({ type: 'node', host: { $exists: true, $ne: null }, intelamt: { $exists: true } }).toArray(func); };
- obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { obj.file.find({ type: 'node', domain: domainid, mtype: mtype, 'intelamt.uuid': uuid }).toArray(func); };
-
- // TODO: Starting in MongoDB 4.0.3, you should use countDocuments() instead of count() that is deprecated. We should detect MongoDB version and switch.
- // https://docs.mongodb.com/manual/reference/method/db.collection.countDocuments/
- //obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { obj.file.countDocuments({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max)); }); } }
- obj.isMaxType = function (max, type, domainid, func) {
- if (obj.file.countDocuments) {
- if (max == null) { func(false); } else { obj.file.countDocuments({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); }
- } else {
- if (max == null) { func(false); } else { obj.file.count({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); }
- }
- }
-
- // Database actions on the events collection
- obj.GetAllEvents = function (func) { obj.eventsfile.find({}).toArray(func); };
-
- // Bulk operations
- if (parent.config.settings.mongodbbulkoperations) {
- obj.StoreEvent = function (event, func) { // Fast MongoDB event store using bulkWrite()
- if (obj.eventsFilePendingSet == false) {
- // Perform the operation now
- obj.dbCounters.eventsSet++;
- obj.eventsFilePendingSet = true; obj.eventsFilePendingSets = null;
- if (func != null) { obj.eventsFilePendingCbs = [func]; }
- obj.eventsfile.bulkWrite([{ insertOne: { document: event } }], eventsFileBulkWriteCompleted);
- } else {
- // Add this operation to the pending list
- obj.dbCounters.eventsSetPending++;
- if (obj.eventsFilePendingSets == null) { obj.eventsFilePendingSets = [] }
- obj.eventsFilePendingSets.push(event);
- if (func != null) { if (obj.eventsFilePendingCb == null) { obj.eventsFilePendingCb = [func]; } else { obj.eventsFilePendingCb.push(func); } }
- }
- };
- } else {
- obj.StoreEvent = function (event, func) { obj.dbCounters.eventsSet++; obj.eventsfile.insertOne(event, func); };
- }
-
- obj.GetEvents = function (ids, domain, filter, func) {
- var finddata = { domain: domain, ids: { $in: ids } };
- if (filter != null) finddata.action = filter;
- obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).toArray(func);
- };
- obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
- var finddata = { domain: domain, ids: { $in: ids } };
- if (filter != null) finddata.action = filter;
- obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).toArray(func);
- };
- obj.GetUserEvents = function (ids, domain, userid, filter, func) {
- var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
- if (filter != null) finddata.action = filter;
- obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).toArray(func);
- };
- obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
- var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
- if (filter != null) finddata.action = filter;
- obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).toArray(func);
- };
- obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) { obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }], msgid: { $in: msgids }, time: { $gte: start, $lte: end } }).project({ type: 0, _id: 0, domain: 0, node: 0 }).sort({ time: 1 }).toArray(func); };
- obj.GetUserLoginEvents = function (domain, userid, func) { obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }).project({ action: 1, time: 1, msgid: 1, msgArgs: 1, tokenName: 1 }).sort({ time: -1 }).toArray(func); };
- obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
- var finddata = { domain: domain, nodeid: nodeid };
- if (filter != null) finddata.action = filter;
- obj.eventsfile.find(finddata).project({ type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).toArray(func);
- };
- obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
- var finddata = { domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } };
- if (filter != null) finddata.action = filter;
- obj.eventsfile.find(finddata).project({ type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).toArray(func);
- };
- obj.RemoveAllEvents = function (domain) { obj.eventsfile.deleteMany({ domain: domain }, { multi: true }); };
- obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; obj.eventsfile.deleteMany({ domain: domain, nodeid: nodeid }, { multi: true }); };
- obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; obj.eventsfile.deleteMany({ domain: domain, userid: userid }, { multi: true }); };
- obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) {
- if (obj.eventsfile.countDocuments) {
- obj.eventsfile.countDocuments({ action: 'authfail', userid: userid, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null) ? count : 0); });
- } else {
- obj.eventsfile.count({ action: 'authfail', userid: userid, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null) ? count : 0); });
- }
- }
-
- // Database actions on the power collection
- obj.getAllPower = function (func) { obj.powerfile.find({}).toArray(func); };
-
- // Bulk operations
- if (parent.config.settings.mongodbbulkoperations) {
- obj.storePowerEvent = function (event, multiServer, func) { // Fast MongoDB event store using bulkWrite()
- if (multiServer != null) { event.server = multiServer.serverid; }
- if (obj.powerFilePendingSet == false) {
- // Perform the operation now
- obj.dbCounters.powerSet++;
- obj.powerFilePendingSet = true; obj.powerFilePendingSets = null;
- if (func != null) { obj.powerFilePendingCbs = [func]; }
- obj.powerfile.bulkWrite([{ insertOne: { document: event } }], powerFileBulkWriteCompleted);
- } else {
- // Add this operation to the pending list
- obj.dbCounters.powerSetPending++;
- if (obj.powerFilePendingSets == null) { obj.powerFilePendingSets = [] }
- obj.powerFilePendingSets.push(event);
- if (func != null) { if (obj.powerFilePendingCb == null) { obj.powerFilePendingCb = [func]; } else { obj.powerFilePendingCb.push(func); } }
- }
- };
- } else {
- obj.storePowerEvent = function (event, multiServer, func) { obj.dbCounters.powerSet++; if (multiServer != null) { event.server = multiServer.serverid; } obj.powerfile.insertOne(event, func); };
- }
-
- obj.getPowerTimeline = function (nodeid, func) { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }).project({ _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }).toArray(func); };
- obj.removeAllPowerEvents = function () { obj.powerfile.deleteMany({}, { multi: true }); };
- obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; obj.powerfile.deleteMany({ nodeid: nodeid }, { multi: true }); };
-
- // Database actions on the SMBIOS collection
- obj.GetAllSMBIOS = function (func) { obj.smbiosfile.find({}).toArray(func); };
- obj.SetSMBIOS = function (smbios, func) { obj.smbiosfile.updateOne({ _id: smbios._id }, { $set: smbios }, { upsert: true }, func); };
- obj.RemoveSMBIOS = function (id) { obj.smbiosfile.deleteOne({ _id: id }); };
- obj.GetSMBIOS = function (id, func) { obj.smbiosfile.find({ _id: id }).toArray(func); };
-
- // Database actions on the Server Stats collection
- obj.SetServerStats = function (data, func) { obj.serverstatsfile.insertOne(data, func); };
- obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); obj.serverstatsfile.find({ time: { $gt: t } }, { _id: 0, cpu: 0 }).toArray(func); };
-
- // Read a configuration file from the database
- obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
-
- // Write a configuration file to the database
- obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
-
- // List all configuration files
- obj.listConfigFiles = function (func) { obj.file.find({ type: 'cfile' }).sort({ _id: 1 }).toArray(func); }
-
- // Get database information
- obj.getDbStats = function (func) {
- obj.stats = { c: 6 };
- obj.getStats(function (r) { obj.stats.recordTypes = r; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } })
- obj.file.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
- obj.eventsfile.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
- obj.powerfile.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
- obj.smbiosfile.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
- obj.serverstatsfile.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
- }
-
- // Correct database information of obj.getDbStats before returning it
- function getDbStatsEx(data) {
- var r = {};
- if (data.recordTypes != null) { r = data.recordTypes; }
- try { r.smbios = data['meshcentral.smbios'].count; } catch (ex) { }
- try { r.power = data['meshcentral.power'].count; } catch (ex) { }
- try { r.events = data['meshcentral.events'].count; } catch (ex) { }
- try { r.serverstats = data['meshcentral.serverstats'].count; } catch (ex) { }
- return r;
- }
-
- // Plugin operations
- if (obj.pluginsActive) {
- obj.addPlugin = function (plugin, func) { plugin.type = 'plugin'; obj.pluginsfile.insertOne(plugin, func); }; // Add a plugin
- obj.getPlugins = function (func) { obj.pluginsfile.find({ type: 'plugin' }).project({ type: 0 }).sort({ name: 1 }).toArray(func); }; // Get all plugins
- obj.getPlugin = function (id, func) { id = require('mongodb').ObjectId(id); obj.pluginsfile.find({ _id: id }).sort({ name: 1 }).toArray(func); }; // Get plugin
- obj.deletePlugin = function (id, func) { id = require('mongodb').ObjectId(id); obj.pluginsfile.deleteOne({ _id: id }, func); }; // Delete plugin
- obj.setPluginStatus = function (id, status, func) { id = require('mongodb').ObjectId(id); obj.pluginsfile.updateOne({ _id: id }, { $set: { status: status } }, func); };
- obj.updatePlugin = function (id, args, func) { delete args._id; id = require('mongodb').ObjectId(id); obj.pluginsfile.updateOne({ _id: id }, { $set: args }, func); };
- }
-
- } else {
- // Database actions on the main collection (NeDB and MongoJS)
- obj.Set = function (data, func) {
- obj.dbCounters.fileSet++;
- data = common.escapeLinksFieldNameEx(data);
- var xdata = performTypedRecordEncrypt(data); obj.file.update({ _id: xdata._id }, xdata, { upsert: true }, func);
- };
- obj.Get = function (id, func) {
- if (arguments.length > 2) {
- var parms = [func];
- for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
- var func2 = function _func2(arg1, arg2) {
- var userCallback = _func2.userArgs.shift();
- _func2.userArgs.unshift(arg2);
- _func2.userArgs.unshift(arg1);
- userCallback.apply(obj, _func2.userArgs);
- };
- func2.userArgs = parms;
- obj.file.find({ _id: id }, function (err, docs) {
- if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
- func2(err, performTypedRecordDecrypt(docs));
- });
- } else {
- obj.file.find({ _id: id }, function (err, docs) {
- if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
- func(err, performTypedRecordDecrypt(docs));
- });
- }
- };
- obj.GetAll = function (func) { obj.file.find({}, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetHash = function (id, func) { obj.file.find({ _id: id }, { _id: 0, hash: 1 }, func); };
- obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- //obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, domain, type, id, skip, limit, func) {
- //var x = { type: type, domain: domain, meshid: { $in: meshes } };
- //if (id) { x._id = id; }
- //obj.file.find(x, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
- //};
- obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
- if (extrasids == null) {
- const x = { type: type, domain: domain, meshid: { $in: meshes } };
- if (id) { x._id = id; }
- obj.file.find(x, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
- } else {
- const x = { type: type, domain: domain, $or: [{ meshid: { $in: meshes } }, { _id: { $in: extrasids } }] };
- if (id) { x._id = id; }
- obj.file.find(x, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
- }
- };
- obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
- var x = { type: type, domain: domain, nodeid: { $in: nodes } };
- if (id) { x._id = id; }
- obj.file.find(x, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
- };
- obj.GetAllType = function (type, func) { obj.file.find({ type: type }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.find({ type: type, domain: domain, _id: { $in: ids } }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetUserWithEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email }, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email, emailVerified: true }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.Remove = function (id, func) { obj.file.remove({ _id: id }, func); };
- obj.RemoveAll = function (func) { obj.file.remove({}, { multi: true }, func); };
- obj.RemoveAllOfType = function (type, func) { obj.file.remove({ type: type }, { multi: true }, func); };
- obj.InsertMany = function (data, func) { obj.file.insert(data, func); }; // Insert records directly, no link escaping
- obj.RemoveMeshDocuments = function (id) { obj.file.remove({ meshid: id }, { multi: true }); obj.file.remove({ _id: 'nt' + id }); };
- obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
- obj.DeleteDomain = function (domain, func) { obj.file.remove({ domain: domain }, { multi: true }, func); };
- obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
- obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
- obj.getLocalAmtNodes = function (func) { obj.file.find({ type: 'node', host: { $exists: true, $ne: null }, intelamt: { $exists: true } }, func); };
- obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { obj.file.find({ type: 'node', domain: domainid, mtype: mtype, 'intelamt.uuid': uuid }, func); };
- obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { obj.file.count({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); } }
-
- // Database actions on the events collection
- obj.GetAllEvents = function (func) { obj.eventsfile.find({}, func); };
- obj.StoreEvent = function (event, func) { obj.eventsfile.insert(event, func); };
- obj.GetEvents = function (ids, domain, filter, func) {
- var finddata = { domain: domain, ids: { $in: ids } };
- if (filter != null) finddata.action = filter;
- if (obj.databaseType == DB_NEDB) {
- obj.eventsfile.find(finddata, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func);
- } else {
- obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func);
- }
- };
- obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
- var finddata = { domain: domain, ids: { $in: ids } };
- if (filter != null) finddata.action = filter;
- if (obj.databaseType == DB_NEDB) {
- obj.eventsfile.find(finddata, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func);
- } else {
- obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func);
- }
- };
- obj.GetUserEvents = function (ids, domain, userid, filter, func) {
- var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
- if (filter != null) finddata.action = filter;
- if (obj.databaseType == DB_NEDB) {
- obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func);
- } else {
- obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func);
- }
- };
- obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
- var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
- if (filter != null) finddata.action = filter;
- if (obj.databaseType == DB_NEDB) {
- obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func);
- } else {
- obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func);
- }
- };
- obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
- if (obj.databaseType == DB_NEDB) {
- obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }], msgid: { $in: msgids }, time: { $gte: start, $lte: end } }, { type: 0, _id: 0, domain: 0, node: 0 }).sort({ time: 1 }).exec(func);
- } else {
- obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }], msgid: { $in: msgids }, time: { $gte: start, $lte: end } }, { type: 0, _id: 0, domain: 0, node: 0 }).sort({ time: 1 }, func);
- }
- };
- obj.GetUserLoginEvents = function (domain, userid, func) {
- if (obj.databaseType == DB_NEDB) {
- obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }, { action: 1, time: 1, msgid: 1, msgArgs: 1, tokenName: 1 }).sort({ time: -1 }).exec(func);
- } else {
- obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }, { action: 1, time: 1, msgid: 1, msgArgs: 1, tokenName: 1 }).sort({ time: -1 }, func);
- }
- };
- obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
- var finddata = { domain: domain, nodeid: nodeid };
- if (filter != null) finddata.action = filter;
- if (obj.databaseType == DB_NEDB) {
- obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).exec(func);
- } else {
- obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit, func);
- }
- };
- obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
- var finddata = { domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } };
- if (filter != null) finddata.action = filter;
- if (obj.databaseType == DB_NEDB) {
- obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).exec(func);
- } else {
- obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit, func);
- }
- };
- obj.RemoveAllEvents = function (domain) { obj.eventsfile.remove({ domain: domain }, { multi: true }); };
- obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; obj.eventsfile.remove({ domain: domain, nodeid: nodeid }, { multi: true }); };
- obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; obj.eventsfile.remove({ domain: domain, userid: userid }, { multi: true }); };
- obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) { obj.eventsfile.count({ action: 'authfail', userid: userid, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null) ? count : 0); }); }
-
- // Database actions on the power collection
- obj.getAllPower = function (func) { obj.powerfile.find({}, func); };
- obj.storePowerEvent = function (event, multiServer, func) { if (multiServer != null) { event.server = multiServer.serverid; } obj.powerfile.insert(event, func); };
- obj.getPowerTimeline = function (nodeid, func) { if (obj.databaseType == DB_NEDB) { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }, { _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }).exec(func); } else { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }, { _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }, func); } };
- obj.removeAllPowerEvents = function () { obj.powerfile.remove({}, { multi: true }); };
- obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; obj.powerfile.remove({ nodeid: nodeid }, { multi: true }); };
-
- // Database actions on the SMBIOS collection
- if (obj.smbiosfile != null) {
- obj.GetAllSMBIOS = function (func) { obj.smbiosfile.find({}, func); };
- obj.SetSMBIOS = function (smbios, func) { obj.smbiosfile.update({ _id: smbios._id }, smbios, { upsert: true }, func); };
- obj.RemoveSMBIOS = function (id) { obj.smbiosfile.remove({ _id: id }); };
- obj.GetSMBIOS = function (id, func) { obj.smbiosfile.find({ _id: id }, func); };
- }
-
- // Database actions on the Server Stats collection
- obj.SetServerStats = function (data, func) { obj.serverstatsfile.insert(data, func); };
- obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); obj.serverstatsfile.find({ time: { $gt: t } }, { _id: 0, cpu: 0 }, func); };
-
- // Read a configuration file from the database
- obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
-
- // Write a configuration file to the database
- obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
-
- // List all configuration files
- obj.listConfigFiles = function (func) { obj.file.find({ type: 'cfile' }).sort({ _id: 1 }).exec(func); }
-
- // Get database information
- obj.getDbStats = function (func) {
- obj.stats = { c: 5 };
- obj.getStats(function (r) { obj.stats.recordTypes = r; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } })
- obj.file.count({}, function (err, count) { obj.stats.meshcentral = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
- obj.eventsfile.count({}, function (err, count) { obj.stats.events = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
- obj.powerfile.count({}, function (err, count) { obj.stats.power = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
- obj.serverstatsfile.count({}, function (err, count) { obj.stats.serverstats = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
- }
-
- // Correct database information of obj.getDbStats before returning it
- function getDbStatsEx(data) {
- var r = {};
- if (data.recordTypes != null) { r = data.recordTypes; }
- try { r.smbios = data['smbios'].count; } catch (ex) { }
- try { r.power = data['power'].count; } catch (ex) { }
- try { r.events = data['events'].count; } catch (ex) { }
- try { r.serverstats = data['serverstats'].count; } catch (ex) { }
- return r;
- }
-
- // Plugin operations
- if (obj.pluginsActive) {
- obj.addPlugin = function (plugin, func) { plugin.type = 'plugin'; obj.pluginsfile.insert(plugin, func); }; // Add a plugin
- obj.getPlugins = function (func) { obj.pluginsfile.find({ 'type': 'plugin' }, { 'type': 0 }).sort({ name: 1 }).exec(func); }; // Get all plugins
- obj.getPlugin = function (id, func) { obj.pluginsfile.find({ _id: id }).sort({ name: 1 }).exec(func); }; // Get plugin
- obj.deletePlugin = function (id, func) { obj.pluginsfile.remove({ _id: id }, func); }; // Delete plugin
- obj.setPluginStatus = function (id, status, func) { obj.pluginsfile.update({ _id: id }, { $set: { status: status } }, func); };
- obj.updatePlugin = function (id, args, func) { delete args._id; obj.pluginsfile.update({ _id: id }, { $set: args }, func); };
- }
-
- }
-
- // Get all configuration files
- obj.getAllConfigFiles = function (password, func) {
- obj.GetAllType('cfile', function (err, docs) {
- if (err != null) { func(null); return; }
- var r = null;
- for (var i = 0; i < docs.length; i++) {
- var name = docs[i]._id.split('/')[1];
- var data = obj.decryptData(password, docs[i].data);
- if (data != null) { if (r == null) { r = {}; } r[name] = data; }
- }
- func(r);
- });
- }
-
- func(obj); // Completed function setup
- }
-
- // Return a human readable string with current backup configuration
- obj.getBackupConfig = function () {
- var r = '', backupPath = parent.backuppath;
-
- let dbname = 'meshcentral';
- if (parent.args.mongodbname) { dbname = parent.args.mongodbname; }
- else if ((typeof parent.args.mariadb == 'object') && (typeof parent.args.mariadb.database == 'string')) { dbname = parent.args.mariadb.database; }
- else if ((typeof parent.args.mysql == 'object') && (typeof parent.args.mysql.database == 'string')) { dbname = parent.args.mysql.database; }
- else if ((typeof parent.args.postgres == 'object') && (typeof parent.args.postgres.database == 'string')) { dbname = parent.args.postgres.database; }
- else if (typeof parent.config.settings.sqlite3 == 'string') {dbname = parent.config.settings.sqlite3 + '.sqlite'};
-
- const currentDate = new Date();
- const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2);
- obj.newAutoBackupFile = parent.config.settings.autobackup.backupname + fileSuffix;
-
- r += 'DB Name: ' + dbname + '\r\n';
- r += 'DB Type: ' + DB_LIST[obj.databaseType] + '\r\n';
-
- if (parent.config.settings.autobackup.backupintervalhours == -1) {
- r += 'Backup disabled\r\n';
- } else {
- r += 'BackupPath: ' + backupPath + '\r\n';
- r += 'BackupFile: ' + obj.newAutoBackupFile + '.zip\r\n';
-
- if (parent.config.settings.autobackup.backuphour != null && parent.config.settings.autobackup.backuphour != -1) {
- r += 'Backup between: ' + parent.config.settings.autobackup.backuphour + 'H-' + (parent.config.settings.autobackup.backuphour + 1) + 'H\r\n';
- }
- r += 'Backup Interval (Hours): ' + parent.config.settings.autobackup.backupintervalhours + '\r\n';
- if (parent.config.settings.autobackup.keeplastdaysbackup != null) {
- r += 'Keep Last Backups (Days): ' + parent.config.settings.autobackup.keeplastdaysbackup + '\r\n';
- }
- if (parent.config.settings.autobackup.zippassword != null) {
- r += 'ZIP Password: ';
- if (typeof parent.config.settings.autobackup.zippassword != 'string') { r += 'Bad zippassword type, Backups will not be encrypted\r\n'; }
- else if (parent.config.settings.autobackup.zippassword == "") { r += 'Blank zippassword, Backups will fail\r\n'; }
- else { r += 'Set\r\n'; }
- }
- if (parent.config.settings.autobackup.mongodumppath != null) {
- r += 'MongoDump Path: ';
- if (typeof parent.config.settings.autobackup.mongodumppath != 'string') { r += 'Bad mongodumppath type\r\n'; }
- else { r += parent.config.settings.autobackup.mongodumppath + '\r\n'; }
- }
- if (parent.config.settings.autobackup.mysqldumppath != null) {
- r += 'MySqlDump Path: ';
- if (typeof parent.config.settings.autobackup.mysqldumppath != 'string') { r += 'Bad mysqldump type\r\n'; }
- else { r += parent.config.settings.autobackup.mysqldumppath + '\r\n'; }
- }
- if (parent.config.settings.autobackup.pgdumppath != null) {
- r += 'pgDump Path: ';
- if (typeof parent.config.settings.autobackup.pgdumppath != 'string') { r += 'Bad pgdump type\r\n'; }
- else { r += parent.config.settings.autobackup.pgdumppath + '\r\n'; }
- }
- if (parent.config.settings.autobackup.backupotherfolders) {
- r += 'Backup other folders: ';
- r += parent.filespath + ', ' + parent.recordpath + '\r\n';
- }
- if (parent.config.settings.autobackup.backupwebfolders) {
- r += 'Backup webfolders: ';
- if (parent.webViewsOverridePath) {r += parent.webViewsOverridePath };
- if (parent.webPublicOverridePath) {r += ', '+ parent.webPublicOverridePath};
- if (parent.webEmailsOverridePath) {r += ',' + parent.webEmailsOverridePath};
- r+= '\r\n';
- }
- if (parent.config.settings.autobackup.backupignorefilesglob != []) {
- r += 'Backup IgnoreFilesGlob: ';
- { r += parent.config.settings.autobackup.backupignorefilesglob + '\r\n'; }
- }
- if (parent.config.settings.autobackup.backupskipfoldersglob != []) {
- r += 'Backup SkipFoldersGlob: ';
- { r += parent.config.settings.autobackup.backupskipfoldersglob + '\r\n'; }
- }
-
- if (typeof parent.config.settings.autobackup.s3 == 'object') {
- r += 'S3 Backups: Enabled\r\n';
- }
- if (typeof parent.config.settings.autobackup.webdav == 'object') {
- r += 'WebDAV Backups: Enabled\r\n';
- r += 'WebDAV backup path: ' + ((typeof parent.config.settings.autobackup.webdav.foldername == 'string') ? parent.config.settings.autobackup.webdav.foldername : 'MeshCentral-Backups') + '\r\n';
- r += 'WebDAV maximum files: '+ ((typeof parent.config.settings.autobackup.webdav.maxfiles == 'number') ? parent.config.settings.autobackup.webdav.maxfiles : 'no limit') + '\r\n';
- }
- if (typeof parent.config.settings.autobackup.googledrive == 'object') {
- r += 'Google Drive Backups: Enabled\r\n';
- }
-
-
- }
-
- return r;
- }
-
- function buildSqlDumpCommand() {
- var props = (obj.databaseType == DB_MARIADB) ? parent.args.mariadb : parent.args.mysql;
-
- var mysqldumpPath = 'mysqldump';
- if (parent.config.settings.autobackup && parent.config.settings.autobackup.mysqldumppath) {
- mysqldumpPath = path.normalize(parent.config.settings.autobackup.mysqldumppath);
- }
-
- var cmd = '\"' + mysqldumpPath + '\" --user=\'' + props.user + '\'';
- // Windows will treat ' as part of the pw. Linux/Unix requires it to escape.
- cmd += (parent.platform == 'win32') ? ' --password=\"' + props.password + '\"' : ' --password=\'' + props.password + '\'';
- if (props.host) { cmd += ' -h ' + props.host; }
- if (props.port) { cmd += ' -P ' + props.port; }
-
- if (props.awsrds) { cmd += ' --single-transaction'; }
-
- // SSL options different on mariadb/mysql
- var sslOptions = '';
- if (obj.databaseType == DB_MARIADB) {
- if (props.ssl) {
- sslOptions = ' --ssl';
- if (props.ssl.cacertpath) sslOptions = ' --ssl-ca=' + props.ssl.cacertpath;
- if (props.ssl.dontcheckserveridentity != true) {sslOptions += ' --ssl-verify-server-cert'} else {sslOptions += ' --ssl-verify-server-cert=false'};
- if (props.ssl.clientcertpath) sslOptions += ' --ssl-cert=' + props.ssl.clientcertpath;
- if (props.ssl.clientkeypath) sslOptions += ' --ssl-key=' + props.ssl.clientkeypath;
- }
- } else {
- if (props.ssl) {
- sslOptions = ' --ssl-mode=required';
- if (props.ssl.cacertpath) sslOptions = ' --ssl-ca=' + props.ssl.cacertpath;
- if (props.ssl.dontcheckserveridentity != true) sslOptions += ' --ssl-mode=verify_identity';
- else sslOptions += ' --ssl-mode=required';
- if (props.ssl.clientcertpath) sslOptions += ' --ssl-cert=' + props.ssl.clientcertpath;
- if (props.ssl.clientkeypath) sslOptions += ' --ssl-key=' + props.ssl.clientkeypath;
- }
- }
- cmd += sslOptions;
-
- var dbname = (props.database) ? props.database : 'meshcentral';
- cmd += ' ' + dbname
-
- return cmd;
- }
-
- function buildMongoDumpCommand() {
- const dburl = parent.args.mongodb;
-
- var mongoDumpPath = 'mongodump';
- if (parent.config.settings.autobackup && parent.config.settings.autobackup.mongodumppath) {
- mongoDumpPath = path.normalize(parent.config.settings.autobackup.mongodumppath);
- }
-
- var cmd = '"' + mongoDumpPath + '"';
- if (dburl) { cmd = '\"' + mongoDumpPath + '\" --uri=\"' + dburl + '\"'; }
- if (parent.config.settings.autobackup?.mongodumpargs) {
- cmd = '\"' + mongoDumpPath + '\" ' + parent.config.settings.autobackup.mongodumpargs;
- if (!parent.config.settings.autobackup.mongodumpargs.includes("--db=")) {cmd += ' --db=' + (parent.config.settings.mongodbname ? parent.config.settings.mongodbname : 'meshcentral')};
- }
- return cmd;
- }
-
- // Check that the server is capable of performing a backup
- // Tries configured custom location with fallback to default location
- // Now runs after autobackup config init in meshcentral.js so config options are checked
- obj.checkBackupCapability = function (func) {
- if (parent.config.settings.autobackup.backupintervalhours == -1) { return; };
- //block backup until validated. Gets put back if all checks are ok.
- let backupInterval = parent.config.settings.autobackup.backupintervalhours;
- parent.config.settings.autobackup.backupintervalhours = -1;
- let backupPath = parent.backuppath;
-
- if (backupPath.startsWith(parent.datapath)) {
- func(1, "Backup path can't be set within meshcentral-data folder. No backups will be made.");
- return;
- }
- // Check create/write backupdir
- try { fs.mkdirSync(backupPath); }
- catch (e) {
- // EEXIST error = dir already exists
- if (e.code != 'EEXIST' ) {
- //Unable to create backuppath
- console.error(e.message);
- func(1, 'Unable to create ' + backupPath + '. No backups will be made. Error: ' + e.message);
- return;
- }
- }
- const currentDate = new Date();
- const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2);
- const testFile = path.join(backupPath, parent.config.settings.autobackup.backupname + fileSuffix + '.zip');
- try { fs.writeFileSync( testFile, "DeleteMe"); }
- catch (e) {
- //Unable to create file
- console.error (e.message);
- func(1, "Backuppath (" + backupPath + ") can't be written to. No backups will be made. Error: " + e.message);
- return;
- }
- try { fs.unlinkSync(testFile); parent.debug('backup', 'Backuppath ' + backupPath + ' accesscheck successful');}
- catch (e) {
- console.error (e.message);
- func(1, "Backuppathtestfile (" + testFile + ") can't be deleted, check filerights. Error: " + e.message);
- // Assume write rights, no delete rights. Continue with warning.
- //return;
- }
-
- // Check database dumptools
- if ((obj.databaseType == DB_MONGOJS) || (obj.databaseType == DB_MONGODB)) {
- // Check that we have access to MongoDump
- var cmd = buildMongoDumpCommand();
- cmd += (parent.platform == 'win32') ? ' --archive=\"nul\"' : ' --archive=\"/dev/null\"';
- const child_process = require('child_process');
- child_process.exec(cmd, { cwd: backupPath }, function (error, stdout, stderr) {
- if ((error != null) && (error != '')) {
- func(1, "Mongodump error, backup will not be performed. Check path or use mongodumppath & mongodumpargs");
- return;
- } else {parent.config.settings.autobackup.backupintervalhours = backupInterval;}
- });
- } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
- // Check that we have access to mysqldump
- var cmd = buildSqlDumpCommand();
- cmd += ' > ' + ((parent.platform == 'win32') ? '\"nul\"' : '\"/dev/null\"');
- const child_process = require('child_process');
- child_process.exec(cmd, { cwd: backupPath, timeout: 1000*30 }, function(error, stdout, stdin) {
- if ((error != null) && (error != '')) {
- func(1, "mysqldump error, backup will not be performed. Check path or use mysqldumppath");
- return;
- } else {parent.config.settings.autobackup.backupintervalhours = backupInterval;}
-
- });
- } else if (obj.databaseType == DB_POSTGRESQL) {
- // Check that we have access to pg_dump
- parent.config.settings.autobackup.pgdumppath = path.normalize(parent.config.settings.autobackup.pgdumppath ? parent.config.settings.autobackup.pgdumppath : 'pg_dump');
- let cmd = '"' + parent.config.settings.autobackup.pgdumppath + '"'
- + ' --dbname=postgresql://' + encodeURIComponent(parent.config.settings.postgres.user) + ":" + encodeURIComponent(parent.config.settings.postgres.password)
- + "@" + parent.config.settings.postgres.host + ":" + parent.config.settings.postgres.port + "/" + encodeURIComponent(databaseName)
- + ' > ' + ((parent.platform == 'win32') ? '\"nul\"' : '\"/dev/null\"');
- const child_process = require('child_process');
- child_process.exec(cmd, { cwd: backupPath }, function(error, stdout, stdin) {
- if ((error != null) && (error != '')) {
- func(1, "pg_dump error, backup will not be performed. Check path or use pgdumppath.");
- return;
- } else {parent.config.settings.autobackup.backupintervalhours = backupInterval;}
- });
- } else {
- //all ok, enable backup
- parent.config.settings.autobackup.backupintervalhours = backupInterval;}
- }
-
- // MongoDB pending bulk read operation, perform fast bulk document reads.
- function fileBulkReadCompleted(err, docs) {
- // Send out callbacks with results
- if (docs != null) {
- for (var i in docs) {
- if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); }
- const id = docs[i]._id;
- if (obj.filePendingGets[id] != null) {
- for (var j in obj.filePendingGets[id]) {
- if (typeof obj.filePendingGets[id][j] == 'function') { obj.filePendingGets[id][j](err, performTypedRecordDecrypt([docs[i]])); }
- }
- delete obj.filePendingGets[id];
- }
- }
- }
-
- // If there are not results, send out a null callback
- for (var i in obj.filePendingGets) { for (var j in obj.filePendingGets[i]) { obj.filePendingGets[i][j](err, []); } }
-
- // Move on to process any more pending get operations
- obj.filePendingGets = obj.filePendingGet;
- obj.filePendingGet = null;
- if (obj.filePendingGets != null) {
- var findlist = [];
- for (var i in obj.filePendingGets) { findlist.push(i); }
- obj.file.find({ _id: { $in: findlist } }).toArray(fileBulkReadCompleted);
- }
- }
-
- // MongoDB pending bulk remove operation, perform fast bulk document removes.
- function fileBulkRemoveCompleted(err) {
- // Send out callbacks
- for (var i in obj.filePendingRemoves) {
- for (var j in obj.filePendingRemoves[i]) {
- if (typeof obj.filePendingRemoves[i][j] == 'function') { obj.filePendingRemoves[i][j](err); }
- }
- }
-
- // Move on to process any more pending get operations
- obj.filePendingRemoves = obj.filePendingRemove;
- obj.filePendingRemove = null;
- if (obj.filePendingRemoves != null) {
- obj.dbCounters.fileRemoveBulk++;
- var findlist = [], count = 0;
- for (var i in obj.filePendingRemoves) { findlist.push(i); count++; }
- obj.file.deleteMany({ _id: { $in: findlist } }, { multi: true }, fileBulkRemoveCompleted);
- }
- }
-
- // MongoDB pending bulk write operation, perform fast bulk document replacement.
- function fileBulkWriteCompleted() {
- // Callbacks
- if (obj.filePendingCbs != null) {
- for (var i in obj.filePendingCbs) { if (typeof obj.filePendingCbs[i] == 'function') { obj.filePendingCbs[i](); } }
- obj.filePendingCbs = null;
- }
- if (obj.filePendingSets != null) {
- // Perform pending operations
- obj.dbCounters.fileSetBulk++;
- var ops = [];
- obj.filePendingCbs = obj.filePendingCb;
- obj.filePendingCb = null;
- for (var i in obj.filePendingSets) { ops.push({ replaceOne: { filter: { _id: i }, replacement: performTypedRecordEncrypt(common.escapeLinksFieldNameEx(obj.filePendingSets[i])), upsert: true } }); }
- obj.file.bulkWrite(ops, fileBulkWriteCompleted);
- obj.filePendingSets = null;
- } else {
- // All done, no pending operations.
- obj.filePendingSet = false;
- }
- }
-
- // MongoDB pending bulk write operation, perform fast bulk document replacement.
- function eventsFileBulkWriteCompleted() {
- // Callbacks
- if (obj.eventsFilePendingCbs != null) { for (var i in obj.eventsFilePendingCbs) { obj.eventsFilePendingCbs[i](); } obj.eventsFilePendingCbs = null; }
- if (obj.eventsFilePendingSets != null) {
- // Perform pending operations
- obj.dbCounters.eventsSetBulk++;
- var ops = [];
- for (var i in obj.eventsFilePendingSets) { ops.push({ insertOne: { document: obj.eventsFilePendingSets[i] } }); }
- obj.eventsFilePendingCbs = obj.eventsFilePendingCb;
- obj.eventsFilePendingCb = null;
- obj.eventsFilePendingSets = null;
- obj.eventsfile.bulkWrite(ops, eventsFileBulkWriteCompleted);
- } else {
- // All done, no pending operations.
- obj.eventsFilePendingSet = false;
- }
- }
-
- // MongoDB pending bulk write operation, perform fast bulk document replacement.
- function powerFileBulkWriteCompleted() {
- // Callbacks
- if (obj.powerFilePendingCbs != null) { for (var i in obj.powerFilePendingCbs) { obj.powerFilePendingCbs[i](); } obj.powerFilePendingCbs = null; }
- if (obj.powerFilePendingSets != null) {
- // Perform pending operations
- obj.dbCounters.powerSetBulk++;
- var ops = [];
- for (var i in obj.powerFilePendingSets) { ops.push({ insertOne: { document: obj.powerFilePendingSets[i] } }); }
- obj.powerFilePendingCbs = obj.powerFilePendingCb;
- obj.powerFilePendingCb = null;
- obj.powerFilePendingSets = null;
- obj.powerfile.bulkWrite(ops, powerFileBulkWriteCompleted);
- } else {
- // All done, no pending operations.
- obj.powerFilePendingSet = false;
- }
- }
-
- // Perform a server backup
- obj.performBackup = function (func) {
- parent.debug('backup','Entering performBackup');
- try {
- if (obj.performingBackup) return 'Backup alreay in progress.';
- if (parent.config.settings.autobackup.backupintervalhours == -1) { if (func) { func('Backup disabled.'); return 'Backup disabled.' }};
- obj.performingBackup = true;
- let backupPath = parent.backuppath;
- let dataPath = parent.datapath;
-
- const currentDate = new Date();
- const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2);
- obj.newAutoBackupFile = path.join(backupPath, parent.config.settings.autobackup.backupname + fileSuffix + '.zip');
- parent.debug('backup','newAutoBackupFile=' + obj.newAutoBackupFile);
-
- if ((obj.databaseType == DB_MONGOJS) || (obj.databaseType == DB_MONGODB)) {
- // Perform a MongoDump
- const dbname = (parent.args.mongodbname) ? (parent.args.mongodbname) : 'meshcentral';
- const dburl = parent.args.mongodb;
-
- obj.newDBDumpFile = path.join(backupPath, (dbname + '-mongodump-' + fileSuffix + '.archive'));
-
- var cmd = buildMongoDumpCommand();
- cmd += (dburl) ? ' --archive=\"' + obj.newDBDumpFile + '\"' :
- ' --db=\"' + dbname + '\" --archive=\"' + obj.newDBDumpFile + '\"';
- parent.debug('backup','Mongodump cmd: ' + cmd);
- const child_process = require('child_process');
- const dumpProcess = child_process.exec(
- cmd,
- { cwd: parent.parentpath },
- (error)=> {if (error) {obj.backupStatus |= BACKUPFAIL_DBDUMP; console.error('ERROR: Unable to perform MongoDB backup: ' + error + '\r\n'); obj.createBackupfile(func);}}
- );
-
- dumpProcess.on('exit', (code) => {
- if (code != 0) {console.log(`Mongodump child process exited with code ${code}`); obj.backupStatus |= BACKUPFAIL_DBDUMP;}
- obj.createBackupfile(func);
- });
-
- } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
- // Perform a MySqlDump backup
- const newBackupFile = 'mysqldump-' + fileSuffix;
- obj.newDBDumpFile = path.join(backupPath, newBackupFile + '.sql');
-
- var cmd = buildSqlDumpCommand();
- cmd += ' --result-file=\"' + obj.newDBDumpFile + '\"';
- parent.debug('backup','Maria/MySQLdump cmd: ' + cmd);
-
- const child_process = require('child_process');
- const dumpProcess = child_process.exec(
- cmd,
- { cwd: parent.parentpath },
- (error)=> {if (error) {obj.backupStatus |= BACKUPFAIL_DBDUMP; console.error('ERROR: Unable to perform MySQL backup: ' + error + '\r\n'); obj.createBackupfile(func);}}
- );
- dumpProcess.on('exit', (code) => {
- if (code != 0) {console.error(`MySQLdump child process exited with code ${code}`); obj.backupStatus |= BACKUPFAIL_DBDUMP;}
- obj.createBackupfile(func);
- });
-
- } else if (obj.databaseType == DB_SQLITE) {
- //.db3 suffix to escape escape backupfile glob to exclude the sqlite db files
- obj.newDBDumpFile = path.join(backupPath, databaseName + '-sqlitedump-' + fileSuffix + '.db3');
- // do a VACUUM INTO in favor of the backup API to compress the export, see https://www.sqlite.org/backup.html
- parent.debug('backup','SQLitedump: VACUUM INTO ' + obj.newDBDumpFile);
- obj.file.exec('VACUUM INTO \'' + obj.newDBDumpFile + '\'', function (err) {
- if (err) { console.error('SQLite backup error: ' + err); obj.backupStatus |=BACKUPFAIL_DBDUMP;};
- //always finish/clean up
- obj.createBackupfile(func);
- });
- } else if (obj.databaseType == DB_POSTGRESQL) {
- // Perform a PostgresDump backup
- const newBackupFile = 'pgdump-' + fileSuffix + '.sql';
- obj.newDBDumpFile = path.join(backupPath, newBackupFile);
- let cmd = '"' + parent.config.settings.autobackup.pgdumppath + '"'
- + ' --dbname=postgresql://' + encodeURIComponent(parent.config.settings.postgres.user) + ":" + encodeURIComponent(parent.config.settings.postgres.password)
- + "@" + parent.config.settings.postgres.host + ":" + parent.config.settings.postgres.port + "/" + encodeURIComponent(databaseName)
- + " --file=" + obj.newDBDumpFile;
- parent.debug('backup','Postgresqldump cmd: ' + cmd);
- const child_process = require('child_process');
- const dumpProcess = child_process.exec(
- cmd,
- { cwd: dataPath },
- (error)=> {if (error) {obj.backupStatus |= BACKUPFAIL_DBDUMP; console.log('ERROR: Unable to perform PostgreSQL dump: ' + error.message + '\r\n'); obj.createBackupfile(func);}}
- );
- dumpProcess.on('exit', (code) => {
- if (code != 0) {console.log(`PostgreSQLdump child process exited with code: ` + code); obj.backupStatus |= BACKUPFAIL_DBDUMP;}
- obj.createBackupfile(func);
- });
- } else {
- // NeDB/Acebase backup, no db dump needed, just make a file backup
- obj.createBackupfile(func);
- }
- } catch (ex) { console.error(ex); parent.addServerWarning( 'Something went wrong during performBackup, check errorlog: ' +ex.message, true); };
- return 'Starting auto-backup...';
- };
-
- obj.createBackupfile = function(func) {
- parent.debug('backup', 'Entering createBackupfile');
- let archiver = require('archiver');
- let archive = null;
- let zipLevel = Math.min(Math.max(Number(parent.config.settings.autobackup.zipcompression ? parent.config.settings.autobackup.zipcompression : 5),1),9);
-
- //if password defined, create encrypted zip
- if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.zippassword == 'string')) {
- try {
- //Only register format once, otherwise it triggers an error
- if (archiver.isRegisteredFormat('zip-encrypted') == false) { archiver.registerFormat('zip-encrypted', require('archiver-zip-encrypted')); }
- archive = archiver.create('zip-encrypted', { zlib: { level: zipLevel }, encryptionMethod: 'aes256', password: parent.config.settings.autobackup.zippassword });
- if (func) { func('Creating encrypted ZIP'); }
- } catch (ex) { // registering encryption failed, do not fall back to non-encrypted, fail backup and skip old backup removal as a precaution to not lose any backups
- obj.backupStatus |= BACKUPFAIL_ZIPMODULE;
- if (func) { func('Zipencryptionmodule failed, aborting');}
- console.error('Zipencryptionmodule failed, aborting');
- }
- } else {
- if (func) { func('Creating a NON-ENCRYPTED ZIP'); }
- archive = archiver('zip', { zlib: { level: zipLevel } });
- }
-
- //original behavior, just a filebackup if dbdump fails : (obj.backupStatus == 0 || obj.backupStatus == BACKUPFAIL_DBDUMP)
- if (obj.backupStatus == 0) {
- // Zip the data directory with the dbdump|NeDB files
- let output = fs.createWriteStream(obj.newAutoBackupFile);
-
- // Archive finalized and closed
- output.on('close', function () {
- if (obj.backupStatus == 0) {
- let mesg = 'Auto-backup completed: ' + obj.newAutoBackupFile + ', backup-size: ' + ((archive.pointer() / 1048576).toFixed(2)) + "Mb";
- console.log(mesg);
- if (func) { func(mesg); };
- obj.performCloudBackup(obj.newAutoBackupFile, func);
- obj.removeExpiredBackupfiles(func);
-
- } else {
- let mesg = 'Zipbackup failed (' + obj.backupStatus.toString(2).slice(-8) + '), deleting incomplete backup: ' + obj.newAutoBackupFile;
- if (func) { func(mesg) }
- else { parent.addServerWarning(mesg, true ) };
- if (fs.existsSync(obj.newAutoBackupFile)) { fs.unlink(obj.newAutoBackupFile, function (err) { if (err) {console.error('Failed to clean up backupfile: ' + err.message)} }) };
- };
- if (obj.databaseType != DB_NEDB) {
- //remove dump archive file, because zipped and otherwise fills up
- if (fs.existsSync(obj.newDBDumpFile)) { fs.unlink(obj.newDBDumpFile, function (err) { if (err) {console.error('Failed to clean up dbdump file: ' + err.message) } }) };
- };
- obj.performingBackup = false;
- obj.backupStatus = 0x0;
- }
- );
- output.on('end', function () { });
- output.on('error', function (err) {
- if ((obj.backupStatus & BACKUPFAIL_ZIPCREATE) == 0) {
- console.error('Output error: ' + err.message);
- if (func) { func('Output error: ' + err.message); };
- obj.backupStatus |= BACKUPFAIL_ZIPCREATE;
- archive.abort();
- };
- });
- archive.on('warning', function (err) {
- //if files added to the archiver object aren't reachable anymore (e.g. sqlite-journal files)
- //an ENOENT warning is given, but the archiver module has no option to/does not skip/resume
- //so the backup needs te be aborted as it otherwise leaves an incomplete zip and never 'ends'
- if ((obj.backupStatus & BACKUPFAIL_ZIPCREATE) == 0) {
- console.log('Zip warning: ' + err.message);
- if (func) { func('Zip warning: ' + err.message); };
- obj.backupStatus |= BACKUPFAIL_ZIPCREATE;
- archive.abort();
- };
- });
- archive.on('error', function (err) {
- if ((obj.backupStatus & BACKUPFAIL_ZIPCREATE) == 0) {
- console.error('Zip error: ' + err.message);
- if (func) { func('Zip error: ' + err.message); };
- obj.backupStatus |= BACKUPFAIL_ZIPCREATE;
- archive.abort();
- }
- });
- archive.pipe(output);
-
- let globIgnoreFiles;
- //slice in case exclusion gets pushed
- globIgnoreFiles = parent.config.settings.autobackup.backupignorefilesglob ? parent.config.settings.autobackup.backupignorefilesglob.slice() : [];
- if (parent.config.settings.sqlite3) { globIgnoreFiles.push (datapathFoldername + '/' + databaseName + '.sqlite*'); }; //skip sqlite database file, and temp files with ext -journal, -wal & -shm
- //archiver.glob doesn't seem to use the third param, archivesubdir. Bug?
- //workaround: go up a dir and add data dir explicitly to keep the zip tidy
- archive.glob((datapathFoldername + '/**'), {
- cwd: datapathParentPath,
- ignore: globIgnoreFiles,
- skip: (parent.config.settings.autobackup.backupskipfoldersglob ? parent.config.settings.autobackup.backupskipfoldersglob : [])
- });
-
- if (parent.config.settings.autobackup.backupwebfolders) {
- if (parent.webViewsOverridePath) { archive.directory(parent.webViewsOverridePath, 'meshcentral-views'); }
- if (parent.webPublicOverridePath) { archive.directory(parent.webPublicOverridePath, 'meshcentral-public'); }
- if (parent.webEmailsOverridePath) { archive.directory(parent.webEmailsOverridePath, 'meshcentral-emails'); }
- };
- if (parent.config.settings.autobackup.backupotherfolders) {
- archive.directory(parent.filespath, 'meshcentral-files');
- archive.directory(parent.recordpath, 'meshcentral-recordings');
- };
- //add dbdump to the root of the zip
- if (obj.newDBDumpFile != null) archive.file(obj.newDBDumpFile, { name: path.basename(obj.newDBDumpFile) });
- archive.finalize();
- } else {
- //failed somewhere before zipping
- console.error('Backup failed ('+ obj.backupStatus.toString(2).slice(-8) + ')');
- if (func) { func('Backup failed ('+ obj.backupStatus.toString(2).slice(-8) + ')') }
- else {
- parent.addServerWarning('Backup failed ('+ obj.backupStatus.toString(2).slice(-8) + ')', true);
- }
- //Just in case something's there
- if (fs.existsSync(obj.newDBDumpFile)) { fs.unlink(obj.newDBDumpFile, function (err) { if (err) {console.error('Failed to clean up dbdump file: ' + err.message) } }); };
- obj.backupStatus = 0x0;
- obj.performingBackup = false;
- };
- };
-
- // Remove expired backupfiles by filenamedate
- obj.removeExpiredBackupfiles = function (func) {
- if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.keeplastdaysbackup == 'number')) {
- let cutoffDate = new Date();
- cutoffDate.setDate(cutoffDate.getDate() - parent.config.settings.autobackup.keeplastdaysbackup);
- fs.readdir(parent.backuppath, function (err, dir) {
- try {
- if (err == null) {
- if (dir.length > 0) {
- let fileName = parent.config.settings.autobackup.backupname;
- let checked = 0;
- let removed = 0;
- for (var i in dir) {
- var name = dir[i];
- parent.debug('backup', "checking file: ", path.join(parent.backuppath, name));
- if (name.startsWith(fileName) && name.endsWith('.zip')) {
- var timex = name.substring(fileName.length, name.length - 4).split('-');
- if (timex.length == 5) {
- checked++;
- var fileDate = new Date(parseInt(timex[0]), parseInt(timex[1]) - 1, parseInt(timex[2]), parseInt(timex[3]), parseInt(timex[4]));
- if (fileDate && (cutoffDate > fileDate)) {
- console.log("Removing expired backup file: ", path.join(parent.backuppath, name));
- fs.unlink(path.join(parent.backuppath, name), function (err) { if (err) { console.error(err.message); if (func) {func('Error removing: ' + err.message); } } });
- removed++;
- }
- }
- else { parent.debug('backup', "file: " + name + " timestamp failure: ", timex); }
- }
- }
- let mesg= 'Checked ' + checked + ' candidates in ' + parent.backuppath + '. Removed ' + removed + ' expired backupfiles using cutoffDate: '+ cutoffDate.toLocaleString('default', { dateStyle: 'short', timeStyle: 'short' });
- parent.debug (mesg);
- if (func) { func(mesg); }
- } else { console.error('No files found in ' + parent.backuppath + '. There should be at least one.')}
- }
- else
- { console.error(err); parent.addServerWarning( 'Reading files in backup directory ' + parent.backuppath + ' failed, check errorlog: ' + err.message, true); }
- } catch (ex) { console.error(ex); parent.addServerWarning( 'Something went wrong during removeExpiredBackupfiles, check errorlog: ' +ex.message, true); }
- });
- }
- }
-
- async function webDAVBackup(filename, func) {
- try {
- const webDAV = await import ('webdav');
- const wdConfig = parent.config.settings.autobackup.webdav;
- const client = webDAV.createClient(wdConfig.url, {
- username: wdConfig.username,
- password: wdConfig.password,
- maxContentLength: Infinity,
- maxBodyLength: Infinity
- });
- if (await client.exists(wdConfig.foldername) === false) {
- await client.createDirectory(wdConfig.foldername, { recursive: true});
- } else {
- // Clean up our WebDAV folder
- if ((typeof wdConfig.maxfiles == 'number') && (wdConfig.maxfiles > 1)) {
- const fileName = parent.config.settings.autobackup.backupname;
- //only files matching our backupfilename
- let files = await client.getDirectoryContents(wdConfig.foldername, { deep: false, glob: "/**/" + fileName + "*.zip" });
- const xdateTimeSort = function (a, b) { if (a.xdate > b.xdate) return 1; if (a.xdate < b.xdate) return -1; return 0; }
- for (const i in files) { files[i].xdate = new Date(files[i].lastmod); }
- files.sort(xdateTimeSort);
- while (files.length >= wdConfig.maxfiles) {
- let delFile = files.shift().filename;
- await client.deleteFile(delFile);
- console.log('WebDAV file deleted: ' + delFile); if (func) { func('WebDAV file deleted: ' + delFile); }
- }
- }
- }
- // Upload to the WebDAV folder
- const { pipeline } = require('stream/promises');
- await pipeline(fs.createReadStream(filename), client.createWriteStream( wdConfig.foldername + path.basename(filename)));
- console.log('WebDAV upload completed: ' + wdConfig.foldername + path.basename(filename)); if (func) { func('WebDAV upload completed: ' + wdConfig.foldername + path.basename(filename)); }
- }
- catch(err) {
- console.error('WebDAV error: ' + err.message); if (func) { func('WebDAV error: ' + err.message);}
- }
- }
-
- // Perform cloud backup
- obj.performCloudBackup = function (filename, func) {
- // WebDAV Backup
- if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.webdav == 'object')) {
- parent.debug( 'backup', 'Entering WebDAV backup'); if (func) { func('Entering WebDAV backup.'); }
- webDAVBackup(filename, func);
- }
-
- // Google Drive Backup
- if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.googledrive == 'object')) {
- parent.debug( 'backup', 'Entering Google Drive backup');
- obj.Get('GoogleDriveBackup', function (err, docs) {
- if ((err != null) || (docs.length != 1) || (docs[0].state != 3)) return;
- if (func) { func('Attempting Google Drive upload...'); }
- const {google} = require('googleapis');
- const oAuth2Client = new google.auth.OAuth2(docs[0].clientid, docs[0].clientsecret, "urn:ietf:wg:oauth:2.0:oob");
- oAuth2Client.on('tokens', function (tokens) { if (tokens.refresh_token) { docs[0].token = tokens.refresh_token; parent.db.Set(docs[0]); } }); // Update the token in the database
- oAuth2Client.setCredentials(docs[0].token);
- const drive = google.drive({ version: 'v3', auth: oAuth2Client });
- const createdTimeSort = function (a, b) { if (a.createdTime > b.createdTime) return 1; if (a.createdTime < b.createdTime) return -1; return 0; }
-
- // Called once we know our folder id, clean up and upload a backup.
- var useGoogleDrive = function (folderid) {
- // List files to see if we need to delete older ones
- if (typeof parent.config.settings.autobackup.googledrive.maxfiles == 'number') {
- drive.files.list({
- q: 'trashed = false and \'' + folderid + '\' in parents',
- fields: 'nextPageToken, files(id, name, size, createdTime)',
- }, function (err, res) {
- if (err) {
- console.log('GoogleDrive (files.list) error: ' + err);
- if (func) { func('GoogleDrive (files.list) error: ' + err); }
- return;
- }
- // Delete any old files if more than 10 files are present in the backup folder.
- res.data.files.sort(createdTimeSort);
- while (res.data.files.length >= parent.config.settings.autobackup.googledrive.maxfiles) { drive.files.delete({ fileId: res.data.files.shift().id }, function (err, res) { }); }
- });
- }
-
- //console.log('Uploading...');
- if (func) { func('Uploading to Google Drive...'); }
-
- // Upload the backup
- drive.files.create({
- requestBody: { name: require('path').basename(filename), mimeType: 'text/plain', parents: [folderid] },
- media: { mimeType: 'application/zip', body: require('fs').createReadStream(filename) },
- }, function (err, res) {
- if (err) {
- console.log('GoogleDrive (files.create) error: ' + err);
- if (func) { func('GoogleDrive (files.create) error: ' + err); }
- return;
- }
- //console.log('Upload done.');
- if (func) { func('Google Drive upload completed.'); }
- });
- }
-
- // Fetch the folder name
- var folderName = 'MeshCentral-Backups';
- if (typeof parent.config.settings.autobackup.googledrive.foldername == 'string') { folderName = parent.config.settings.autobackup.googledrive.foldername; }
-
- // Find our backup folder, create one if needed.
- drive.files.list({
- q: 'mimeType = \'application/vnd.google-apps.folder\' and name=\'' + folderName + '\' and trashed = false',
- fields: 'nextPageToken, files(id, name)',
- }, function (err, res) {
- if (err) {
- console.log('GoogleDrive error: ' + err);
- if (func) { func('GoogleDrive error: ' + err); }
- return;
- }
- if (res.data.files.length == 0) {
- // Create a folder
- drive.files.create({ resource: { 'name': folderName, 'mimeType': 'application/vnd.google-apps.folder' }, fields: 'id' }, function (err, file) {
- if (err) {
- console.log('GoogleDrive (folder.create) error: ' + err);
- if (func) { func('GoogleDrive (folder.create) error: ' + err); }
- return;
- }
- useGoogleDrive(file.data.id);
- });
- } else { useGoogleDrive(res.data.files[0].id); }
- });
- });
- }
-
- // S3 Backup
- if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.s3 == 'object')) {
- parent.debug( 'backup', 'Entering S3 backup');
- var s3folderName = 'MeshCentral-Backups';
- if (typeof parent.config.settings.autobackup.s3.foldername == 'string') { s3folderName = parent.config.settings.autobackup.s3.foldername; }
- // Construct the config object
- var accessKey = parent.config.settings.autobackup.s3.accesskey,
- secretKey = parent.config.settings.autobackup.s3.secretkey,
- endpoint = parent.config.settings.autobackup.s3.endpoint ? parent.config.settings.autobackup.s3.endpoint : 's3.amazonaws.com',
- port = parent.config.settings.autobackup.s3.port ? parent.config.settings.autobackup.s3.port : 443,
- useSsl = parent.config.settings.autobackup.s3.ssl ? parent.config.settings.autobackup.s3.ssl : true,
- bucketName = parent.config.settings.autobackup.s3.bucketname,
- pathPrefix = s3folderName,
- threshold = parent.config.settings.autobackup.s3.maxfiles ? parent.config.settings.autobackup.s3.maxfiles : 0,
- fileToUpload = filename;
- // Create a MinIO client
- const Minio = require('minio');
- var minioClient = new Minio.Client({
- endPoint: endpoint,
- port: port,
- useSSL: useSsl,
- accessKey: accessKey,
- secretKey: secretKey
- });
- // List objects in the specified bucket and path prefix
- var listObjectsPromise = new Promise(function(resolve, reject) {
- var items = [];
- var stream = minioClient.listObjects(bucketName, pathPrefix, true);
- stream.on('data', function(item) {
- if (!item.name.endsWith('/')) { // Exclude directories
- items.push(item);
- }
- });
- stream.on('end', function() {
- resolve(items);
- });
- stream.on('error', function(err) {
- reject(err);
- });
- });
- listObjectsPromise.then(function(objects) {
- // Count the number of files
- var fileCount = objects.length;
- // Return if no files to carry on uploading
- if (fileCount === 0) { return Promise.resolve(); }
- // Sort the files by LastModified date (oldest first)
- objects.sort(function(a, b) { return new Date(a.lastModified) - new Date(b.lastModified); });
- // Check if the threshold is zero and return if
- if (threshold === 0) { return Promise.resolve(); }
- // Check if the number of files exceeds the threshold (maxfiles) is 0
- if (fileCount >= threshold) {
- // Calculate how many files need to be deleted to make space for the new file
- var filesToDelete = fileCount - threshold + 1; // +1 to make space for the new file
- if (func) { func('Deleting ' + filesToDelete + ' older ' + (filesToDelete == 1 ? 'file' : 'files') + ' from S3 ...'); }
- // Create an array of promises for deleting files
- var deletePromises = objects.slice(0, filesToDelete).map(function(fileToDelete) {
- return new Promise(function(resolve, reject) {
- minioClient.removeObject(bucketName, fileToDelete.name, function(err) {
- if (err) {
- reject(err);
- } else {
- if (func) { func('Deleted file: ' + fileToDelete.name + ' from S3'); }
- resolve();
- }
- });
- });
- });
- // Wait for all deletions to complete
- return Promise.all(deletePromises);
- } else {
- return Promise.resolve(); // No deletion needed
- }
- }).then(function() {
- // Determine the upload path by combining the pathPrefix with the filename
- var fileName = require('path').basename(fileToUpload);
- var uploadPath = require('path').join(pathPrefix, fileName);
- // Upload a new file
- var uploadPromise = new Promise(function(resolve, reject) {
- if (func) { func('Uploading file ' + uploadPath + ' to S3'); }
- minioClient.fPutObject(bucketName, uploadPath, fileToUpload, function(err, etag) {
- if (err) {
- reject(err);
- } else {
- if (func) { func('Uploaded file: ' + uploadPath + ' to S3'); }
- resolve(etag);
- }
- });
- });
- return uploadPromise;
- }).catch(function(error) {
- if (func) { func('Error managing files in S3: ' + error); }
- });
- }
- }
-
- // Transfer NeDB data into the current database
- obj.nedbtodb = function (func) {
- var nedbDatastore = null;
- try { nedbDatastore = require('@seald-io/nedb'); } catch (ex) { } // This is the NeDB with Node 23 support.
- if (nedbDatastore == null) {
- try { nedbDatastore = require('@yetzt/nedb'); } catch (ex) { } // This is the NeDB with fixed security dependencies.
- if (nedbDatastore == null) { nedbDatastore = require('nedb'); } // So not to break any existing installations, if the old NeDB is present, use it.
- }
-
- var datastoreOptions = { filename: parent.getConfigFilePath('meshcentral.db'), autoload: true };
-
- // If a DB encryption key is provided, perform database encryption
- if ((typeof parent.args.dbencryptkey == 'string') && (parent.args.dbencryptkey.length != 0)) {
- // Hash the database password into a AES256 key and setup encryption and decryption.
- var nedbKey = parent.crypto.createHash('sha384').update(parent.args.dbencryptkey).digest('raw').slice(0, 32);
- datastoreOptions.afterSerialization = function (plaintext) {
- const iv = parent.crypto.randomBytes(16);
- const aes = parent.crypto.createCipheriv('aes-256-cbc', nedbKey, iv);
- var ciphertext = aes.update(plaintext);
- ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
- return ciphertext.toString('base64');
- }
- datastoreOptions.beforeDeserialization = function (ciphertext) {
- const ciphertextBytes = Buffer.from(ciphertext, 'base64');
- const iv = ciphertextBytes.slice(0, 16);
- const data = ciphertextBytes.slice(16);
- const aes = parent.crypto.createDecipheriv('aes-256-cbc', nedbKey, iv);
- var plaintextBytes = Buffer.from(aes.update(data));
- plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
- return plaintextBytes.toString();
- }
- }
-
- // Setup all NeDB collections
- var nedbfile = new nedbDatastore(datastoreOptions);
- var nedbeventsfile = new nedbDatastore({ filename: parent.getConfigFilePath('meshcentral-events.db'), autoload: true, corruptAlertThreshold: 1 });
- var nedbpowerfile = new nedbDatastore({ filename: parent.getConfigFilePath('meshcentral-power.db'), autoload: true, corruptAlertThreshold: 1 });
- var nedbserverstatsfile = new nedbDatastore({ filename: parent.getConfigFilePath('meshcentral-stats.db'), autoload: true, corruptAlertThreshold: 1 });
-
- // Transfered record counts
- var normalRecordsTransferCount = 0;
- var eventRecordsTransferCount = 0;
- var powerRecordsTransferCount = 0;
- var statsRecordsTransferCount = 0;
- obj.pendingTransfer = 0;
-
- // Transfer the data from main database
- nedbfile.find({}, function (err, docs) {
- if ((err == null) && (docs.length > 0)) {
- performTypedRecordDecrypt(docs)
- for (var i in docs) {
- obj.pendingTransfer++;
- normalRecordsTransferCount++;
- obj.Set(common.unEscapeLinksFieldName(docs[i]), function () { obj.pendingTransfer--; });
- }
- }
-
- // Transfer events
- nedbeventsfile.find({}, function (err, docs) {
- if ((err == null) && (docs.length > 0)) {
- for (var i in docs) {
- obj.pendingTransfer++;
- eventRecordsTransferCount++;
- obj.StoreEvent(docs[i], function () { obj.pendingTransfer--; });
- }
- }
-
- // Transfer power events
- nedbpowerfile.find({}, function (err, docs) {
- if ((err == null) && (docs.length > 0)) {
- for (var i in docs) {
- obj.pendingTransfer++;
- powerRecordsTransferCount++;
- obj.storePowerEvent(docs[i], null, function () { obj.pendingTransfer--; });
- }
- }
-
- // Transfer server stats
- nedbserverstatsfile.find({}, function (err, docs) {
- if ((err == null) && (docs.length > 0)) {
- for (var i in docs) {
- obj.pendingTransfer++;
- statsRecordsTransferCount++;
- obj.SetServerStats(docs[i], function () { obj.pendingTransfer--; });
- }
- }
-
- // Only exit when all the records are stored.
- setInterval(function () {
- if (obj.pendingTransfer == 0) { func("Done. " + normalRecordsTransferCount + " record(s), " + eventRecordsTransferCount + " event(s), " + powerRecordsTransferCount + " power change(s), " + statsRecordsTransferCount + " stat(s)."); }
- }, 200)
- });
- });
- });
- });
- }
-
- function padNumber(number, digits) { return Array(Math.max(digits - String(number).length + 1, 0)).join(0) + number; }
-
- // Called when a node has changed
- function dbNodeChange(nodeChange, added) {
- if (parent.webserver == null) return;
- common.unEscapeLinksFieldName(nodeChange.fullDocument);
- const node = performTypedRecordDecrypt([nodeChange.fullDocument])[0];
- parent.DispatchEvent(['*', node.meshid], obj, { etype: 'node', action: (added ? 'addnode' : 'changenode'), node: parent.webserver.CloneSafeNode(node), nodeid: node._id, domain: node.domain, nolog: 1 });
- }
-
- // Called when a device group has changed
- function dbMeshChange(meshChange, added) {
- if (parent.webserver == null) return;
- common.unEscapeLinksFieldName(meshChange.fullDocument);
- const mesh = performTypedRecordDecrypt([meshChange.fullDocument])[0];
-
- // Update the mesh object in memory
- const mmesh = parent.webserver.meshes[mesh._id];
- if (mmesh != null) {
- // Update an existing device group
- for (var i in mesh) { mmesh[i] = mesh[i]; }
- for (var i in mmesh) { if (mesh[i] == null) { delete mmesh[i]; } }
- } else {
- // Device group not present, create it.
- parent.webserver.meshes[mesh._id] = mesh;
- }
-
- // Send the mesh update
- var mesh2 = Object.assign({}, mesh); // Shallow clone
- if (mesh2.deleted) { mesh2.action = 'deletemesh'; } else { mesh2.action = (added ? 'createmesh' : 'meshchange'); }
- mesh2.meshid = mesh2._id;
- mesh2.nolog = 1;
- delete mesh2.type;
- delete mesh2._id;
- parent.DispatchEvent(['*', mesh2.meshid], obj, parent.webserver.CloneSafeMesh(mesh2));
- }
-
- // Called when a user account has changed
- function dbUserChange(userChange, added) {
- if (parent.webserver == null) return;
- common.unEscapeLinksFieldName(userChange.fullDocument);
- const user = performTypedRecordDecrypt([userChange.fullDocument])[0];
-
- // Update the user object in memory
- const muser = parent.webserver.users[user._id];
- if (muser != null) {
- // Update an existing user
- for (var i in user) { muser[i] = user[i]; }
- for (var i in muser) { if (user[i] == null) { delete muser[i]; } }
- } else {
- // User not present, create it.
- parent.webserver.users[user._id] = user;
- }
-
- // Send the user update
- var targets = ['*', 'server-users', user._id];
- if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
- parent.DispatchEvent(targets, obj, { etype: 'user', userid: user._id, username: user.name, account: parent.webserver.CloneSafeUser(user), action: (added ? 'accountcreate' : 'accountchange'), domain: user.domain, nolog: 1 });
- }
-
- // Called when a user group has changed
- function dbUGrpChange(ugrpChange, added) {
- if (parent.webserver == null) return;
- common.unEscapeLinksFieldName(ugrpChange.fullDocument);
- const usergroup = ugrpChange.fullDocument;
-
- // Update the user group object in memory
- const uusergroup = parent.webserver.userGroups[usergroup._id];
- if (uusergroup != null) {
- // Update an existing user group
- for (var i in usergroup) { uusergroup[i] = usergroup[i]; }
- for (var i in uusergroup) { if (usergroup[i] == null) { delete uusergroup[i]; } }
- } else {
- // Usergroup not present, create it.
- parent.webserver.userGroups[usergroup._id] = usergroup;
- }
-
- // Send the user group update
- var usergroup2 = Object.assign({}, usergroup); // Shallow clone
- usergroup2.action = (added ? 'createusergroup' : 'usergroupchange');
- usergroup2.ugrpid = usergroup2._id;
- usergroup2.nolog = 1;
- delete usergroup2.type;
- delete usergroup2._id;
- parent.DispatchEvent(['*', usergroup2.ugrpid], obj, usergroup2);
- }
-
- function dbMergeSqlArray(arr) {
- var x = '';
- for (var i in arr) { if (x != '') { x += ','; } x += '\'' + arr[i] + '\''; }
- return x;
- }
-
- return obj;
-};
+/**
+* @description MeshCentral database module
+* @author Ylian Saint-Hilaire
+* @copyright Intel Corporation 2018-2022
+* @license Apache-2.0
+* @version v0.0.2
+*/
+
+/*xjslint node: true */
+/*xjslint plusplus: true */
+/*xjslint maxlen: 256 */
+/*jshint node: true */
+/*jshint strict: false */
+/*jshint esversion: 6 */
+"use strict";
+
+//
+// Construct Meshcentral database object
+//
+// The default database is NeDB
+// https://github.com/louischatriot/nedb
+//
+// Alternativety, MongoDB can be used
+// https://www.mongodb.com/
+// Just run with --mongodb [connectionstring], where the connection string is documented here: https://docs.mongodb.com/manual/reference/connection-string/
+// The default collection is "meshcentral", but you can override it using --mongodbcol [collection]
+//
+module.exports.CreateDB = function (parent, func) {
+ var obj = {};
+ var Datastore = null;
+ var expireEventsSeconds = (60 * 60 * 24 * 20); // By default, expire events after 20 days (1728000). (Seconds * Minutes * Hours * Days)
+ var expirePowerEventsSeconds = (60 * 60 * 24 * 10); // By default, expire power events after 10 days (864000). (Seconds * Minutes * Hours * Days)
+ var expireServerStatsSeconds = (60 * 60 * 24 * 30); // By default, expire server stats after 30 days (2592000). (Seconds * Minutes * Hours * Days)
+ const common = require('./common.js');
+ const path = require('path');
+ const fs = require('fs');
+ const DB_NEDB = 1, DB_MONGOJS = 2, DB_MONGODB = 3,DB_MARIADB = 4, DB_MYSQL = 5, DB_POSTGRESQL = 6, DB_ACEBASE = 7, DB_SQLITE = 8;
+ const DB_LIST = ['None', 'NeDB', 'MongoJS', 'MongoDB', 'MariaDB', 'MySQL', 'PostgreSQL', 'AceBase', 'SQLite']; //for the info command
+ let databaseName = 'meshcentral';
+ let datapathParentPath = path.dirname(parent.datapath);
+ let datapathFoldername = path.basename(parent.datapath);
+ const SQLITE_AUTOVACUUM = ['none', 'full', 'incremental'];
+ const SQLITE_SYNCHRONOUS = ['off', 'normal', 'full', 'extra'];
+ obj.sqliteConfig = {
+ maintenance: '',
+ startupVacuum: false,
+ autoVacuum: 'full',
+ incrementalVacuum: 100,
+ journalMode: 'delete',
+ journalSize: 4096000,
+ synchronous: 'full',
+ };
+ obj.performingBackup = false;
+ const BACKUPFAIL_ZIPCREATE = 0x0001;
+ const BACKUPFAIL_ZIPMODULE = 0x0010;
+ const BACKUPFAIL_DBDUMP = 0x0100;
+ obj.backupStatus = 0x0;
+ obj.newAutoBackupFile = null;
+ obj.newDBDumpFile = null;
+ obj.identifier = null;
+ obj.dbKey = null;
+ obj.dbRecordsEncryptKey = null;
+ obj.dbRecordsDecryptKey = null;
+ obj.changeStream = false;
+ obj.pluginsActive = ((parent.config) && (parent.config.settings) && (parent.config.settings.plugins != null) && (parent.config.settings.plugins != false) && ((typeof parent.config.settings.plugins != 'object') || (parent.config.settings.plugins.enabled != false)));
+ obj.dbCounters = {
+ fileSet: 0,
+ fileRemove: 0,
+ powerSet: 0,
+ eventsSet: 0
+ }
+
+ // MongoDB bulk operations state
+ if (parent.config.settings.mongodbbulkoperations) {
+ // Added counters
+ obj.dbCounters.fileSetPending = 0;
+ obj.dbCounters.fileSetBulk = 0;
+ obj.dbCounters.fileRemovePending = 0;
+ obj.dbCounters.fileRemoveBulk = 0;
+ obj.dbCounters.powerSetPending = 0;
+ obj.dbCounters.powerSetBulk = 0;
+ obj.dbCounters.eventsSetPending = 0;
+ obj.dbCounters.eventsSetBulk = 0;
+
+ /// Added bulk accumulators
+ obj.filePendingGet = null;
+ obj.filePendingGets = null;
+ obj.filePendingRemove = null;
+ obj.filePendingRemoves = null;
+ obj.filePendingSet = false;
+ obj.filePendingSets = null;
+ obj.filePendingCb = null;
+ obj.filePendingCbs = null;
+ obj.powerFilePendingSet = false;
+ obj.powerFilePendingSets = null;
+ obj.powerFilePendingCb = null;
+ obj.powerFilePendingCbs = null;
+ obj.eventsFilePendingSet = false;
+ obj.eventsFilePendingSets = null;
+ obj.eventsFilePendingCb = null;
+ obj.eventsFilePendingCbs = null;
+ }
+
+ obj.SetupDatabase = function (func) {
+ // Check if the database unique identifier is present
+ // This is used to check that in server peering mode, everyone is using the same database.
+ obj.Get('DatabaseIdentifier', function (err, docs) {
+ if (err != null) { parent.debug('db', 'ERROR (Get DatabaseIdentifier): ' + err); }
+ if ((err == null) && (docs.length == 1) && (docs[0].value != null)) {
+ obj.identifier = docs[0].value;
+ } else {
+ obj.identifier = Buffer.from(require('crypto').randomBytes(48), 'binary').toString('hex');
+ obj.Set({ _id: 'DatabaseIdentifier', value: obj.identifier });
+ }
+ });
+
+ // Load database schema version and check if we need to update
+ obj.Get('SchemaVersion', function (err, docs) {
+ if (err != null) { parent.debug('db', 'ERROR (Get SchemaVersion): ' + err); }
+ var ver = 0;
+ if ((err == null) && (docs.length == 1)) { ver = docs[0].value; }
+ if (ver == 1) { console.log('This is an unsupported beta 1 database, delete it to create a new one.'); process.exit(0); }
+
+ // TODO: Any schema upgrades here...
+ obj.Set({ _id: 'SchemaVersion', value: 2 });
+
+ func(ver);
+ });
+ };
+
+ // Perform database maintenance
+ obj.maintenance = function () {
+ parent.debug('db', 'Entering database maintenance');
+ if (obj.databaseType == DB_NEDB) { // NeDB will not remove expired records unless we try to access them. This will force the removal.
+ obj.eventsfile.remove({ time: { '$lt': new Date(Date.now() - (expireEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
+ obj.powerfile.remove({ time: { '$lt': new Date(Date.now() - (expirePowerEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
+ obj.serverstatsfile.remove({ time: { '$lt': new Date(Date.now() - (expireServerStatsSeconds * 1000)) } }, { multi: true }); // Force delete older events
+ } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) { // MariaDB or MySQL
+ sqlDbQuery('DELETE FROM events WHERE time < ?', [new Date(Date.now() - (expireEventsSeconds * 1000))], function (doc, err) { }); // Delete events older than expireEventsSeconds
+ sqlDbQuery('DELETE FROM power WHERE time < ?', [new Date(Date.now() - (expirePowerEventsSeconds * 1000))], function (doc, err) { }); // Delete events older than expirePowerSeconds
+ sqlDbQuery('DELETE FROM serverstats WHERE expire < ?', [new Date()], function (doc, err) { }); // Delete events where expiration date is in the past
+ sqlDbQuery('DELETE FROM smbios WHERE expire < ?', [new Date()], function (doc, err) { }); // Delete events where expiration date is in the past
+ } else if (obj.databaseType == DB_ACEBASE) { // AceBase
+ //console.log('Performing AceBase maintenance');
+ obj.file.query('events').filter('time', '<', new Date(Date.now() - (expireEventsSeconds * 1000))).remove().then(function () {
+ obj.file.query('stats').filter('time', '<', new Date(Date.now() - (expireServerStatsSeconds * 1000))).remove().then(function () {
+ obj.file.query('power').filter('time', '<', new Date(Date.now() - (expirePowerEventsSeconds * 1000))).remove().then(function () {
+ //console.log('AceBase maintenance done');
+ });
+ });
+ });
+ } else if (obj.databaseType == DB_SQLITE) { // SQLite3
+ //sqlite does not return rows affected for INSERT, UPDATE or DELETE statements, see https://www.sqlite.org/pragma.html#pragma_count_changes
+ obj.file.serialize(function () {
+ obj.file.run('DELETE FROM events WHERE time < ?', [new Date(Date.now() - (expireEventsSeconds * 1000))]);
+ obj.file.run('DELETE FROM power WHERE time < ?', [new Date(Date.now() - (expirePowerEventsSeconds * 1000))]);
+ obj.file.run('DELETE FROM serverstats WHERE expire < ?', [new Date()]);
+ obj.file.run('DELETE FROM smbios WHERE expire < ?', [new Date()]);
+ obj.file.exec(obj.sqliteConfig.maintenance, function (err) {
+ if (err) {console.log('Maintenance error: ' + err.message)};
+ if (parent.config.settings.debug) {
+ sqliteGetPragmas(['freelist_count', 'page_size', 'page_count', 'cache_size' ], function (pragma, pragmaValue) {
+ parent.debug('db', 'SQLite Maintenance: ' + pragma + '=' + pragmaValue);
+ });
+ };
+ });
+ });
+ }
+ obj.removeInactiveDevices();
+ }
+
+ // Remove inactive devices
+ obj.removeInactiveDevices = function (showall, cb) {
+ // Get a list of domains and what their inactive device removal setting is
+ var removeInactiveDevicesPerDomain = {}, minRemoveInactiveDevicesPerDomain = {}, minRemoveInactiveDevice = 9999;
+ for (var i in parent.config.domains) {
+ if (typeof parent.config.domains[i].autoremoveinactivedevices == 'number') {
+ var v = parent.config.domains[i].autoremoveinactivedevices;
+ if ((v >= 1) && (v <= 2000)) {
+ if (v < minRemoveInactiveDevice) { minRemoveInactiveDevice = v; }
+ removeInactiveDevicesPerDomain[i] = v;
+ minRemoveInactiveDevicesPerDomain[i] = v;
+ }
+ }
+ }
+
+ // Check if any device groups have a inactive device removal setting
+ for (var i in parent.webserver.meshes) {
+ if (typeof parent.webserver.meshes[i].expireDevs == 'number') {
+ var v = parent.webserver.meshes[i].expireDevs;
+ if ((v >= 1) && (v <= 2000)) {
+ if (v < minRemoveInactiveDevice) { minRemoveInactiveDevice = v; }
+ if ((minRemoveInactiveDevicesPerDomain[parent.webserver.meshes[i].domain] == null) || (minRemoveInactiveDevicesPerDomain[parent.webserver.meshes[i].domain] > v)) {
+ minRemoveInactiveDevicesPerDomain[parent.webserver.meshes[i].domain] = v;
+ }
+ } else {
+ delete parent.webserver.meshes[i].expireDevs;
+ }
+ }
+ }
+
+ // If there are no such settings for any domain, we can exit now.
+ if (minRemoveInactiveDevice == 9999) { if (cb) { cb("No device removal policy set, nothing to do."); } return; }
+ const now = Date.now();
+
+ // For each domain with a inactive device removal setting, get a list of last device connections
+ for (var domainid in minRemoveInactiveDevicesPerDomain) {
+ obj.GetAllTypeNoTypeField('lastconnect', domainid, function (err, docs) {
+ if ((err != null) || (docs == null)) return;
+ for (var j in docs) {
+ const days = Math.floor((now - docs[j].time) / 86400000); // Calculate the number of inactive days
+ var expireDays = -1;
+ if (removeInactiveDevicesPerDomain[docs[j].domain]) { expireDays = removeInactiveDevicesPerDomain[docs[j].domain]; }
+ const mesh = parent.webserver.meshes[docs[j].meshid];
+ if (mesh && (typeof mesh.expireDevs == 'number')) { expireDays = mesh.expireDevs; }
+ var remove = false;
+ if (expireDays > 0) {
+ if (expireDays < days) { remove = true; }
+ if (cb) { if (showall || remove) { cb(docs[j]._id.substring(2) + ', ' + days + ' days, expire ' + expireDays + ' days' + (remove ? ', removing' : '')); } }
+ if (remove) {
+ // Check if this device is connected right now
+ const nodeid = docs[j]._id.substring(2);
+ const conn = parent.GetConnectivityState(nodeid);
+ if (conn == null) {
+ // Remove the device
+ obj.Get(nodeid, function (err, docs) {
+ if (err != null) return;
+ if ((docs == null) || (docs.length != 1)) { obj.Remove('lc' + nodeid); return; } // Remove last connect time
+ const node = docs[0];
+
+ // Delete this node including network interface information, events and timeline
+ obj.Remove(node._id); // Remove node with that id
+ obj.Remove('if' + node._id); // Remove interface information
+ obj.Remove('nt' + node._id); // Remove notes
+ obj.Remove('lc' + node._id); // Remove last connect time
+ obj.Remove('si' + node._id); // Remove system information
+ obj.Remove('al' + node._id); // Remove error log last time
+ if (obj.RemoveSMBIOS) { obj.RemoveSMBIOS(node._id); } // Remove SMBios data
+ obj.RemoveAllNodeEvents(node._id); // Remove all events for this node
+ obj.removeAllPowerEventsForNode(node._id); // Remove all power events for this node
+ if (typeof node.pmt == 'string') { obj.Remove('pmt_' + node.pmt); } // Remove Push Messaging Token
+ obj.Get('ra' + node._id, function (err, nodes) {
+ if ((nodes != null) && (nodes.length == 1)) { obj.Remove('da' + nodes[0].daid); } // Remove diagnostic agent to real agent link
+ obj.Remove('ra' + node._id); // Remove real agent to diagnostic agent link
+ });
+
+ // Remove any user node links
+ if (node.links != null) {
+ for (var i in node.links) {
+ if (i.startsWith('user/')) {
+ var cuser = parent.webserver.users[i];
+ if ((cuser != null) && (cuser.links != null) && (cuser.links[node._id] != null)) {
+ // Remove the user link & save the user
+ delete cuser.links[node._id];
+ if (Object.keys(cuser.links).length == 0) { delete cuser.links; }
+ obj.SetUser(cuser);
+
+ // Notify user change
+ var targets = ['*', 'server-users', cuser._id];
+ var event = { etype: 'user', userid: cuser._id, username: cuser.name, action: 'accountchange', msgid: 86, msgArgs: [cuser.name], msg: 'Removed user device rights for ' + cuser.name, domain: node.domain, account: parent.webserver.CloneSafeUser(cuser) };
+ if (obj.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
+ parent.DispatchEvent(targets, obj, event);
+ }
+ } else if (i.startsWith('ugrp/')) {
+ var cusergroup = parent.userGroups[i];
+ if ((cusergroup != null) && (cusergroup.links != null) && (cusergroup.links[node._id] != null)) {
+ // Remove the user link & save the user
+ delete cusergroup.links[node._id];
+ if (Object.keys(cusergroup.links).length == 0) { delete cusergroup.links; }
+ obj.Set(cusergroup);
+
+ // Notify user change
+ var targets = ['*', 'server-users', cusergroup._id];
+ var event = { etype: 'ugrp', ugrpid: cusergroup._id, name: cusergroup.name, desc: cusergroup.desc, action: 'usergroupchange', links: cusergroup.links, msgid: 163, msgArgs: [node.name, cusergroup.name], msg: 'Removed device ' + node.name + ' from user group ' + cusergroup.name };
+ if (obj.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
+ parent.DispatchEvent(targets, obj, event);
+ }
+ }
+ }
+ }
+
+ // Event node deletion
+ var meshname = '(unknown)';
+ if ((parent.webserver.meshes[node.meshid] != null) && (parent.webserver.meshes[node.meshid].name != null)) { meshname = parent.webserver.meshes[node.meshid].name; }
+ var event = { etype: 'node', action: 'removenode', nodeid: node._id, msgid: 87, msgArgs: [node.name, meshname], msg: 'Removed device ' + node.name + ' from device group ' + meshname, domain: node.domain };
+ // TODO: We can't use the changeStream for node delete because we will not know the meshid the device was in.
+ //if (obj.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to remove the node. Another event will come.
+ parent.DispatchEvent(parent.webserver.CreateNodeDispatchTargets(node.meshid, node._id), obj, event);
+ });
+ }
+ }
+ }
+ }
+ });
+ }
+ }
+
+ // Remove all reference to a domain from the database
+ obj.removeDomain = function (domainName, func) {
+ var pendingCalls;
+ // Remove all events, power events and SMBIOS data from the main collection. They are all in seperate collections now.
+ if (obj.databaseType == DB_ACEBASE) {
+ // AceBase
+ pendingCalls = 3;
+ obj.file.query('meshcentral').filter('domain', '==', domainName).remove().then(function () { if (--pendingCalls == 0) { func(); } });
+ obj.file.query('events').filter('domain', '==', domainName).remove().then(function () { if (--pendingCalls == 0) { func(); } });
+ obj.file.query('power').filter('domain', '==', domainName).remove().then(function () { if (--pendingCalls == 0) { func(); } });
+ } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL) || (obj.databaseType == DB_POSTGRESQL)) {
+ // MariaDB, MySQL or PostgreSQL
+ pendingCalls = 2;
+ sqlDbQuery('DELETE FROM main WHERE domain = $1', [domainName], function () { if (--pendingCalls == 0) { func(); } });
+ sqlDbQuery('DELETE FROM events WHERE domain = $1', [domainName], function () { if (--pendingCalls == 0) { func(); } });
+ } else if (obj.databaseType == DB_MONGODB) {
+ // MongoDB
+ pendingCalls = 3;
+ obj.file.deleteMany({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
+ obj.eventsfile.deleteMany({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
+ obj.powerfile.deleteMany({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
+ } else {
+ // NeDB or MongoJS
+ pendingCalls = 3;
+ obj.file.remove({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
+ obj.eventsfile.remove({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
+ obj.powerfile.remove({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
+ }
+ }
+
+ obj.cleanup = function (func) {
+ // TODO: Remove all mesh links to invalid users
+ // TODO: Remove all meshes that dont have any links
+
+ // Remove all events, power events and SMBIOS data from the main collection. They are all in seperate collections now.
+ if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL) || (obj.databaseType == DB_POSTGRESQL)) {
+ // MariaDB, MySQL or PostgreSQL
+ obj.RemoveAllOfType('event', function () { });
+ obj.RemoveAllOfType('power', function () { });
+ obj.RemoveAllOfType('smbios', function () { });
+ } else if (obj.databaseType == DB_MONGODB) {
+ // MongoDB
+ obj.file.deleteMany({ type: 'event' }, { multi: true });
+ obj.file.deleteMany({ type: 'power' }, { multi: true });
+ obj.file.deleteMany({ type: 'smbios' }, { multi: true });
+ } else if ((obj.databaseType == DB_NEDB) || (obj.databaseType == DB_MONGOJS)) {
+ // NeDB or MongoJS
+ obj.file.remove({ type: 'event' }, { multi: true });
+ obj.file.remove({ type: 'power' }, { multi: true });
+ obj.file.remove({ type: 'smbios' }, { multi: true });
+ }
+
+ // List of valid identifiers
+ var validIdentifiers = {}
+
+ // Load all user groups
+ obj.GetAllType('ugrp', function (err, docs) {
+ if (err != null) { parent.debug('db', 'ERROR (GetAll user): ' + err); }
+ if ((err == null) && (docs.length > 0)) {
+ for (var i in docs) {
+ // Add this as a valid user identifier
+ validIdentifiers[docs[i]._id] = 1;
+ }
+ }
+
+ // Fix all of the creating & login to ticks by seconds, not milliseconds.
+ obj.GetAllType('user', function (err, docs) {
+ if (err != null) { parent.debug('db', 'ERROR (GetAll user): ' + err); }
+ if ((err == null) && (docs.length > 0)) {
+ for (var i in docs) {
+ var fixed = false;
+
+ // Add this as a valid user identifier
+ validIdentifiers[docs[i]._id] = 1;
+
+ // Fix email address capitalization
+ if (docs[i].email && (docs[i].email != docs[i].email.toLowerCase())) {
+ docs[i].email = docs[i].email.toLowerCase(); fixed = true;
+ }
+
+ // Fix account creation
+ if (docs[i].creation) {
+ if (docs[i].creation > 1300000000000) { docs[i].creation = Math.floor(docs[i].creation / 1000); fixed = true; }
+ if ((docs[i].creation % 1) != 0) { docs[i].creation = Math.floor(docs[i].creation); fixed = true; }
+ }
+
+ // Fix last account login
+ if (docs[i].login) {
+ if (docs[i].login > 1300000000000) { docs[i].login = Math.floor(docs[i].login / 1000); fixed = true; }
+ if ((docs[i].login % 1) != 0) { docs[i].login = Math.floor(docs[i].login); fixed = true; }
+ }
+
+ // Fix last password change
+ if (docs[i].passchange) {
+ if (docs[i].passchange > 1300000000000) { docs[i].passchange = Math.floor(docs[i].passchange / 1000); fixed = true; }
+ if ((docs[i].passchange % 1) != 0) { docs[i].passchange = Math.floor(docs[i].passchange); fixed = true; }
+ }
+
+ // Fix subscriptions
+ if (docs[i].subscriptions != null) { delete docs[i].subscriptions; fixed = true; }
+
+ // Save the user if needed
+ if (fixed) { obj.Set(docs[i]); }
+ }
+
+ // Remove all objects that have a "meshid" that no longer points to a valid mesh.
+ // Fix any incorrectly escaped user identifiers
+ obj.GetAllType('mesh', function (err, docs) {
+ if (err != null) { parent.debug('db', 'ERROR (GetAll mesh): ' + err); }
+ var meshlist = [];
+ if ((err == null) && (docs.length > 0)) {
+ for (var i in docs) {
+ var meshChange = false;
+ docs[i] = common.unEscapeLinksFieldName(docs[i]);
+ meshlist.push(docs[i]._id);
+
+ // Make sure all mesh types are number type, if not, fix it.
+ if (typeof docs[i].mtype == 'string') { docs[i].mtype = parseInt(docs[i].mtype); meshChange = true; }
+
+ // If the device group is deleted, remove any invite codes
+ if (docs[i].deleted && docs[i].invite) { delete docs[i].invite; meshChange = true; }
+
+ // Take a look at the links
+ if (docs[i].links != null) {
+ for (var j in docs[i].links) {
+ if (validIdentifiers[j] == null) {
+ // This identifier is not known, let see if we can fix it.
+ var xid = j, xid2 = common.unEscapeFieldName(xid);
+ while ((xid != xid2) && (validIdentifiers[xid2] == null)) { xid = xid2; xid2 = common.unEscapeFieldName(xid2); }
+ if (validIdentifiers[xid2] == 1) {
+ //console.log('Fixing id: ' + j + ' to ' + xid2);
+ docs[i].links[xid2] = docs[i].links[j];
+ delete docs[i].links[j];
+ meshChange = true;
+ } else {
+ // TODO: here, we may want to clean up links to users and user groups that do not exist anymore.
+ //console.log('Unknown id: ' + j);
+ }
+ }
+ }
+ }
+
+ // Save the updated device group if needed
+ if (meshChange) { obj.Set(docs[i]); }
+ }
+ }
+ if (obj.databaseType == DB_SQLITE) {
+ // SQLite
+
+ } else if (obj.databaseType == DB_ACEBASE) {
+ // AceBase
+
+ } else if (obj.databaseType == DB_POSTGRESQL) {
+ // Postgres
+ sqlDbQuery('DELETE FROM Main WHERE ((extra != NULL) AND (extra LIKE (\'mesh/%\')) AND (extra != ANY ($1)))', [meshlist], function (err, response) { });
+ } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
+ // MariaDB
+ sqlDbQuery('DELETE FROM Main WHERE (extra LIKE ("mesh/%") AND (extra NOT IN ?)', [meshlist], function (err, response) { });
+ } else if (obj.databaseType == DB_MONGODB) {
+ // MongoDB
+ obj.file.deleteMany({ meshid: { $exists: true, $nin: meshlist } }, { multi: true });
+ } else {
+ // NeDB or MongoJS
+ obj.file.remove({ meshid: { $exists: true, $nin: meshlist } }, { multi: true });
+ }
+
+ // We are done
+ validIdentifiers = null;
+ if (func) { func(); }
+ });
+ }
+ });
+ });
+ };
+
+ // Get encryption key
+ obj.getEncryptDataKey = function (password, salt, iterations) {
+ if (typeof password != 'string') return null;
+ let key;
+ try {
+ key = parent.crypto.pbkdf2Sync(password, salt, iterations, 32, 'sha384');
+ } catch (ex) {
+ // If this previous call fails, it's probably because older pbkdf2 did not specify the hashing function, just use the default.
+ key = parent.crypto.pbkdf2Sync(password, salt, iterations, 32);
+ }
+ return key
+ }
+
+ // Encrypt data
+ obj.encryptData = function (password, plaintext) {
+ let encryptionVersion = 0x01;
+ let iterations = 100000
+ const iv = parent.crypto.randomBytes(16);
+ var key = obj.getEncryptDataKey(password, iv, iterations);
+ if (key == null) return null;
+ const aes = parent.crypto.createCipheriv('aes-256-gcm', key, iv);
+ var ciphertext = aes.update(plaintext);
+ let versionbuf = Buffer.allocUnsafe(2);
+ versionbuf.writeUInt16BE(encryptionVersion);
+ let iterbuf = Buffer.allocUnsafe(4);
+ iterbuf.writeUInt32BE(iterations);
+ let encryptedBuf = aes.final();
+ ciphertext = Buffer.concat([versionbuf, iterbuf, aes.getAuthTag(), iv, ciphertext, encryptedBuf]);
+ return ciphertext.toString('base64');
+ }
+
+ // Decrypt data
+ obj.decryptData = function (password, ciphertext) {
+ // Adding an encryption version lets us avoid try catching in the future
+ let ciphertextBytes = Buffer.from(ciphertext, 'base64');
+ let encryptionVersion = ciphertextBytes.readUInt16BE(0);
+ try {
+ switch (encryptionVersion) {
+ case 0x01:
+ let iterations = ciphertextBytes.readUInt32BE(2);
+ let authTag = ciphertextBytes.slice(6, 22);
+ const iv = ciphertextBytes.slice(22, 38);
+ const data = ciphertextBytes.slice(38);
+ let key = obj.getEncryptDataKey(password, iv, iterations);
+ if (key == null) return null;
+ const aes = parent.crypto.createDecipheriv('aes-256-gcm', key, iv);
+ aes.setAuthTag(authTag);
+ let plaintextBytes = Buffer.from(aes.update(data));
+ plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
+ return plaintextBytes;
+ default:
+ return obj.oldDecryptData(password, ciphertextBytes);
+ }
+ } catch (ex) { return obj.oldDecryptData(password, ciphertextBytes); }
+ }
+
+ // Encrypt data
+ // The older encryption system uses CBC without integraty checking.
+ // This method is kept only for testing
+ obj.oldEncryptData = function (password, plaintext) {
+ let key = parent.crypto.createHash('sha384').update(password).digest('raw').slice(0, 32);
+ if (key == null) return null;
+ const iv = parent.crypto.randomBytes(16);
+ const aes = parent.crypto.createCipheriv('aes-256-cbc', key, iv);
+ var ciphertext = aes.update(plaintext);
+ ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
+ return ciphertext.toString('base64');
+ }
+
+ // Decrypt data
+ // The older encryption system uses CBC without integraty checking.
+ // This method is kept only to convert the old encryption to the new one.
+ obj.oldDecryptData = function (password, ciphertextBytes) {
+ if (typeof password != 'string') return null;
+ try {
+ const iv = ciphertextBytes.slice(0, 16);
+ const data = ciphertextBytes.slice(16);
+ let key = parent.crypto.createHash('sha384').update(password).digest('raw').slice(0, 32);
+ const aes = parent.crypto.createDecipheriv('aes-256-cbc', key, iv);
+ let plaintextBytes = Buffer.from(aes.update(data));
+ plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
+ return plaintextBytes;
+ } catch (ex) { return null; }
+ }
+
+ // Get the number of records in the database for various types, this is the slow NeDB way.
+ // WARNING: This is a terrible query for database performance. Only do this when needed. This query will look at almost every document in the database.
+ obj.getStats = function (func) {
+ if (obj.databaseType == DB_ACEBASE) {
+ // AceBase
+ // TODO
+ } else if (obj.databaseType == DB_POSTGRESQL) {
+ // PostgreSQL
+ // TODO
+ } else if (obj.databaseType == DB_MYSQL) {
+ // MySQL
+ // TODO
+ } else if (obj.databaseType == DB_MARIADB) {
+ // MariaDB
+ // TODO
+ } else if (obj.databaseType == DB_MONGODB) {
+ // MongoDB
+ obj.file.aggregate([{ "$group": { _id: "$type", count: { $sum: 1 } } }]).toArray(function (err, docs) {
+ var counters = {}, totalCount = 0;
+ if (err == null) { for (var i in docs) { if (docs[i]._id != null) { counters[docs[i]._id] = docs[i].count; totalCount += docs[i].count; } } }
+ func(counters);
+ });
+ } else if (obj.databaseType == DB_MONGOJS) {
+ // MongoJS
+ obj.file.aggregate([{ "$group": { _id: "$type", count: { $sum: 1 } } }], function (err, docs) {
+ var counters = {}, totalCount = 0;
+ if (err == null) { for (var i in docs) { if (docs[i]._id != null) { counters[docs[i]._id] = docs[i].count; totalCount += docs[i].count; } } }
+ func(counters);
+ });
+ } else if (obj.databaseType == DB_NEDB) {
+ // NeDB version
+ obj.file.count({ type: 'node' }, function (err, nodeCount) {
+ obj.file.count({ type: 'mesh' }, function (err, meshCount) {
+ obj.file.count({ type: 'user' }, function (err, userCount) {
+ obj.file.count({ type: 'sysinfo' }, function (err, sysinfoCount) {
+ obj.file.count({ type: 'note' }, function (err, noteCount) {
+ obj.file.count({ type: 'iploc' }, function (err, iplocCount) {
+ obj.file.count({ type: 'ifinfo' }, function (err, ifinfoCount) {
+ obj.file.count({ type: 'cfile' }, function (err, cfileCount) {
+ obj.file.count({ type: 'lastconnect' }, function (err, lastconnectCount) {
+ obj.file.count({}, function (err, totalCount) {
+ func({ node: nodeCount, mesh: meshCount, user: userCount, sysinfo: sysinfoCount, iploc: iplocCount, note: noteCount, ifinfo: ifinfoCount, cfile: cfileCount, lastconnect: lastconnectCount, total: totalCount });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ }
+ }
+
+ // This is used to rate limit a number of operation per day. Returns a startValue each new days, but you can substract it and save the value in the db.
+ obj.getValueOfTheDay = function (id, startValue, func) { obj.Get(id, function (err, docs) { var date = new Date(), t = date.toLocaleDateString(); if ((err == null) && (docs.length == 1)) { var r = docs[0]; if (r.day == t) { func({ _id: id, value: r.value, day: t }); return; } } func({ _id: id, value: startValue, day: t }); }); };
+ obj.escapeBase64 = function escapeBase64(val) { return (val.replace(/\+/g, '@').replace(/\//g, '$')); }
+
+ // Encrypt an database object
+ obj.performRecordEncryptionRecode = function (func) {
+ var count = 0;
+ obj.GetAllType('user', function (err, docs) {
+ if (err != null) { parent.debug('db', 'ERROR (performRecordEncryptionRecode): ' + err); }
+ if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } }
+ obj.GetAllType('node', function (err, docs) {
+ if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } }
+ obj.GetAllType('mesh', function (err, docs) {
+ if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } }
+ if (obj.databaseType == DB_NEDB) { // If we are using NeDB, compact the database.
+ obj.file.compactDatafile();
+ obj.file.on('compaction.done', function () { func(count); }); // It's important to wait for compaction to finish before exit, otherwise NeDB may corrupt.
+ } else {
+ func(count); // For all other databases, normal exit.
+ }
+ });
+ });
+ });
+ }
+
+ // Encrypt an database object
+ function performTypedRecordDecrypt(data) {
+ if ((data == null) || (obj.dbRecordsDecryptKey == null) || (typeof data != 'object')) return data;
+ for (var i in data) {
+ if ((data[i] == null) || (typeof data[i] != 'object')) continue;
+ data[i] = performPartialRecordDecrypt(data[i]);
+ if ((data[i].intelamt != null) && (typeof data[i].intelamt == 'object') && (data[i].intelamt._CRYPT)) { data[i].intelamt = performPartialRecordDecrypt(data[i].intelamt); }
+ if ((data[i].amt != null) && (typeof data[i].amt == 'object') && (data[i].amt._CRYPT)) { data[i].amt = performPartialRecordDecrypt(data[i].amt); }
+ if ((data[i].kvm != null) && (typeof data[i].kvm == 'object') && (data[i].kvm._CRYPT)) { data[i].kvm = performPartialRecordDecrypt(data[i].kvm); }
+ }
+ return data;
+ }
+
+ // Encrypt an database object
+ function performTypedRecordEncrypt(data) {
+ if (obj.dbRecordsEncryptKey == null) return data;
+ if (data.type == 'user') { return performPartialRecordEncrypt(Clone(data), ['otpkeys', 'otphkeys', 'otpsecret', 'salt', 'hash', 'oldpasswords']); }
+ else if ((data.type == 'node') && (data.ssh || data.rdp || data.intelamt)) {
+ var xdata = Clone(data);
+ if (data.ssh || data.rdp) { xdata = performPartialRecordEncrypt(xdata, ['ssh', 'rdp']); }
+ if (data.intelamt) { xdata.intelamt = performPartialRecordEncrypt(xdata.intelamt, ['pass', 'mpspass']); }
+ return xdata;
+ }
+ else if ((data.type == 'mesh') && (data.amt || data.kvm)) {
+ var xdata = Clone(data);
+ if (data.amt) { xdata.amt = performPartialRecordEncrypt(xdata.amt, ['password']); }
+ if (data.kvm) { xdata.kvm = performPartialRecordEncrypt(xdata.kvm, ['pass']); }
+ return xdata;
+ }
+ return data;
+ }
+
+ // Encrypt an object and return a buffer.
+ function performPartialRecordEncrypt(plainobj, encryptNames) {
+ if (typeof plainobj != 'object') return plainobj;
+ var enc = {}, enclen = 0;
+ for (var i in encryptNames) { if (plainobj[encryptNames[i]] != null) { enclen++; enc[encryptNames[i]] = plainobj[encryptNames[i]]; delete plainobj[encryptNames[i]]; } }
+ if (enclen > 0) { plainobj._CRYPT = performRecordEncrypt(enc); } else { delete plainobj._CRYPT; }
+ return plainobj;
+ }
+
+ // Encrypt an object and return a buffer.
+ function performPartialRecordDecrypt(plainobj) {
+ if ((typeof plainobj != 'object') || (plainobj._CRYPT == null)) return plainobj;
+ var enc = performRecordDecrypt(plainobj._CRYPT);
+ if (enc != null) { for (var i in enc) { plainobj[i] = enc[i]; } }
+ delete plainobj._CRYPT;
+ return plainobj;
+ }
+
+ // Encrypt an object and return a base64.
+ function performRecordEncrypt(plainobj) {
+ if (obj.dbRecordsEncryptKey == null) return null;
+ const iv = parent.crypto.randomBytes(12);
+ const aes = parent.crypto.createCipheriv('aes-256-gcm', obj.dbRecordsEncryptKey, iv);
+ var ciphertext = aes.update(JSON.stringify(plainobj));
+ var cipherfinal = aes.final();
+ ciphertext = Buffer.concat([iv, aes.getAuthTag(), ciphertext, cipherfinal]);
+ return ciphertext.toString('base64');
+ }
+
+ // Takes a base64 and return an object.
+ function performRecordDecrypt(ciphertext) {
+ if (obj.dbRecordsDecryptKey == null) return null;
+ const ciphertextBytes = Buffer.from(ciphertext, 'base64');
+ const iv = ciphertextBytes.slice(0, 12);
+ const data = ciphertextBytes.slice(28);
+ const aes = parent.crypto.createDecipheriv('aes-256-gcm', obj.dbRecordsDecryptKey, iv);
+ aes.setAuthTag(ciphertextBytes.slice(12, 28));
+ var plaintextBytes, r;
+ try {
+ plaintextBytes = Buffer.from(aes.update(data));
+ plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
+ r = JSON.parse(plaintextBytes.toString());
+ } catch (e) { throw "Incorrect DbRecordsDecryptKey/DbRecordsEncryptKey or invalid database _CRYPT data: " + e; }
+ return r;
+ }
+
+ // Clone an object (TODO: Make this more efficient)
+ function Clone(v) { return JSON.parse(JSON.stringify(v)); }
+
+ // Read expiration time from configuration file
+ if (typeof parent.args.dbexpire == 'object') {
+ if (typeof parent.args.dbexpire.events == 'number') { expireEventsSeconds = parent.args.dbexpire.events; }
+ if (typeof parent.args.dbexpire.powerevents == 'number') { expirePowerEventsSeconds = parent.args.dbexpire.powerevents; }
+ if (typeof parent.args.dbexpire.statsevents == 'number') { expireServerStatsSeconds = parent.args.dbexpire.statsevents; }
+ }
+
+ // If a DB record encryption key is provided, perform database record encryption
+ if ((typeof parent.args.dbrecordsencryptkey == 'string') && (parent.args.dbrecordsencryptkey.length != 0)) {
+ // Hash the database password into a AES256 key and setup encryption and decryption.
+ obj.dbRecordsEncryptKey = obj.dbRecordsDecryptKey = parent.crypto.createHash('sha384').update(parent.args.dbrecordsencryptkey).digest('raw').slice(0, 32);
+ }
+
+ // If a DB record decryption key is provided, perform database record decryption
+ if ((typeof parent.args.dbrecordsdecryptkey == 'string') && (parent.args.dbrecordsdecryptkey.length != 0)) {
+ // Hash the database password into a AES256 key and setup encryption and decryption.
+ obj.dbRecordsDecryptKey = parent.crypto.createHash('sha384').update(parent.args.dbrecordsdecryptkey).digest('raw').slice(0, 32);
+ }
+
+
+ function createTablesIfNotExist(dbname) {
+ var useDatabase = 'USE ' + dbname;
+ sqlDbQuery(useDatabase, null, function (err, docs) {
+ if (err != null) {
+ console.log("Unable to connect to database: " + err);
+ process.exit();
+ }
+ if (err == null) {
+ parent.debug('db', 'Checking tables...');
+ sqlDbBatchExec([
+ 'CREATE TABLE IF NOT EXISTS main (id VARCHAR(256) NOT NULL, type CHAR(32), domain CHAR(64), extra CHAR(255), extraex CHAR(255), doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))',
+ 'CREATE TABLE IF NOT EXISTS events (id INT NOT NULL AUTO_INCREMENT, time DATETIME, domain CHAR(64), action CHAR(255), nodeid CHAR(255), userid CHAR(255), doc JSON, PRIMARY KEY(id), CHECK(json_valid(doc)))',
+ 'CREATE TABLE IF NOT EXISTS eventids (fkid INT NOT NULL, target CHAR(255), CONSTRAINT fk_eventid FOREIGN KEY (fkid) REFERENCES events (id) ON DELETE CASCADE ON UPDATE RESTRICT)',
+ 'CREATE TABLE IF NOT EXISTS serverstats (time DATETIME, expire DATETIME, doc JSON, PRIMARY KEY(time), CHECK (json_valid(doc)))',
+ 'CREATE TABLE IF NOT EXISTS power (id INT NOT NULL AUTO_INCREMENT, time DATETIME, nodeid CHAR(255), doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))',
+ 'CREATE TABLE IF NOT EXISTS smbios (id CHAR(255), time DATETIME, expire DATETIME, doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))',
+ 'CREATE TABLE IF NOT EXISTS plugin (id INT NOT NULL AUTO_INCREMENT, doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))'
+ ], function (err) {
+ parent.debug('db', 'Checking indexes...');
+ sqlDbExec('CREATE INDEX ndxtypedomainextra ON main (type, domain, extra)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxextra ON main (extra)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxextraex ON main (extraex)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxeventstime ON events(time)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxeventsusername ON events(domain, userid, time)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxeventsdomainnodeidtime ON events(domain, nodeid, time)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxeventids ON eventids(target)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxserverstattime ON serverstats (time)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxserverstatexpire ON serverstats (expire)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxpowernodeidtime ON power (nodeid, time)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxsmbiostime ON smbios (time)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxsmbiosexpire ON smbios (expire)', null, function (err, response) { });
+ setupFunctions(func);
+ });
+ }
+ });
+ }
+
+ if (parent.args.sqlite3) {
+ // SQLite3 database setup
+ obj.databaseType = DB_SQLITE;
+ const sqlite3 = require('sqlite3');
+ let configParams = parent.config.settings.sqlite3;
+ if (typeof configParams == 'string') {databaseName = configParams} else {databaseName = configParams.name ? configParams.name : 'meshcentral';};
+ obj.sqliteConfig.startupVacuum = configParams.startupvacuum ? configParams.startupvacuum : false;
+ obj.sqliteConfig.autoVacuum = configParams.autovacuum ? configParams.autovacuum.toLowerCase() : 'incremental';
+ obj.sqliteConfig.incrementalVacuum = configParams.incrementalvacuum ? configParams.incrementalvacuum : 100;
+ obj.sqliteConfig.journalMode = configParams.journalmode ? configParams.journalmode.toLowerCase() : 'delete';
+ //allowed modes, 'none' excluded because not usefull for this app, maybe also remove 'memory'?
+ if (!(['delete', 'truncate', 'persist', 'memory', 'wal'].includes(obj.sqliteConfig.journalMode))) { obj.sqliteConfig.journalMode = 'delete'};
+ obj.sqliteConfig.journalSize = configParams.journalsize ? configParams.journalsize : 409600;
+ //wal can use the more performant 'normal' mode, see https://www.sqlite.org/pragma.html#pragma_synchronous
+ obj.sqliteConfig.synchronous = (obj.sqliteConfig.journalMode == 'wal') ? 'normal' : 'full';
+ if (obj.sqliteConfig.journalMode == 'wal') {obj.sqliteConfig.maintenance += 'PRAGMA wal_checkpoint(PASSIVE);'};
+ if (obj.sqliteConfig.autoVacuum == 'incremental') {obj.sqliteConfig.maintenance += 'PRAGMA incremental_vacuum(' + obj.sqliteConfig.incrementalVacuum + ');'};
+ obj.sqliteConfig.maintenance += 'PRAGMA optimize;';
+
+ parent.debug('db', 'SQlite config options: ' + JSON.stringify(obj.sqliteConfig, null, 4));
+ if (obj.sqliteConfig.journalMode == 'memory') { console.log('[WARNING] journal_mode=memory: this can lead to database corruption if there is a crash during a transaction. See https://www.sqlite.org/pragma.html#pragma_journal_mode') };
+ //.cached not usefull
+ obj.file = new sqlite3.Database(path.join(parent.datapath, databaseName + '.sqlite'), sqlite3.OPEN_READWRITE, function (err) {
+ if (err && (err.code == 'SQLITE_CANTOPEN')) {
+ // Database needs to be created
+ obj.file = new sqlite3.Database(path.join(parent.datapath, databaseName + '.sqlite'), function (err) {
+ if (err) { console.log("SQLite Error: " + err); process.exit(1); }
+ obj.file.exec(`
+ CREATE TABLE main (id VARCHAR(256) PRIMARY KEY NOT NULL, type CHAR(32), domain CHAR(64), extra CHAR(255), extraex CHAR(255), doc JSON);
+ CREATE TABLE events(id INTEGER PRIMARY KEY, time TIMESTAMP, domain CHAR(64), action CHAR(255), nodeid CHAR(255), userid CHAR(255), doc JSON);
+ CREATE TABLE eventids(fkid INT NOT NULL, target CHAR(255), CONSTRAINT fk_eventid FOREIGN KEY (fkid) REFERENCES events (id) ON DELETE CASCADE ON UPDATE RESTRICT);
+ CREATE TABLE serverstats (time TIMESTAMP PRIMARY KEY, expire TIMESTAMP, doc JSON);
+ CREATE TABLE power (id INTEGER PRIMARY KEY, time TIMESTAMP, nodeid CHAR(255), doc JSON);
+ CREATE TABLE smbios (id CHAR(255) PRIMARY KEY, time TIMESTAMP, expire TIMESTAMP, doc JSON);
+ CREATE TABLE plugin (id INTEGER PRIMARY KEY, doc JSON);
+ CREATE INDEX ndxtypedomainextra ON main (type, domain, extra);
+ CREATE INDEX ndxextra ON main (extra);
+ CREATE INDEX ndxextraex ON main (extraex);
+ CREATE INDEX ndxeventstime ON events(time);
+ CREATE INDEX ndxeventsusername ON events(domain, userid, time);
+ CREATE INDEX ndxeventsdomainnodeidtime ON events(domain, nodeid, time);
+ CREATE INDEX ndxeventids ON eventids(target);
+ CREATE INDEX ndxserverstattime ON serverstats (time);
+ CREATE INDEX ndxserverstatexpire ON serverstats (expire);
+ CREATE INDEX ndxpowernodeidtime ON power (nodeid, time);
+ CREATE INDEX ndxsmbiostime ON smbios (time);
+ CREATE INDEX ndxsmbiosexpire ON smbios (expire);
+ `, function (err) {
+ // Completed DB creation of SQLite3
+ sqliteSetOptions(func);
+ //setupFunctions could be put in the sqliteSetupOptions, but left after it for clarity
+ setupFunctions(func);
+ }
+ );
+ });
+ return;
+ } else if (err) { console.log("SQLite Error: " + err); process.exit(0); }
+
+ //for existing db's
+ sqliteSetOptions();
+ //setupFunctions could be put in the sqliteSetupOptions, but left after it for clarity
+ setupFunctions(func);
+ });
+ } else if (parent.args.acebase) {
+ // AceBase database setup
+ obj.databaseType = DB_ACEBASE;
+ const { AceBase } = require('acebase');
+ // For information on AceBase sponsor: https://github.com/appy-one/acebase/discussions/100
+ obj.file = new AceBase('meshcentral', { sponsor: ((typeof parent.args.acebase == 'object') && (parent.args.acebase.sponsor)), logLevel: 'error', storage: { path: parent.datapath } });
+ // Get all the databases ready
+ obj.file.ready(function () {
+ // Create AceBase indexes
+ obj.file.indexes.create('meshcentral', 'type', { include: ['domain', 'meshid'] });
+ obj.file.indexes.create('meshcentral', 'email');
+ obj.file.indexes.create('meshcentral', 'meshid');
+ obj.file.indexes.create('meshcentral', 'intelamt.uuid');
+ obj.file.indexes.create('events', 'userid', { include: ['action'] });
+ obj.file.indexes.create('events', 'domain', { include: ['nodeid', 'time'] });
+ obj.file.indexes.create('events', 'ids', { include: ['time'] });
+ obj.file.indexes.create('events', 'time');
+ obj.file.indexes.create('power', 'nodeid', { include: ['time'] });
+ obj.file.indexes.create('power', 'time');
+ obj.file.indexes.create('stats', 'time');
+ obj.file.indexes.create('stats', 'expire');
+ // Completed setup of AceBase
+ setupFunctions(func);
+ });
+ } else if (parent.args.mariadb || parent.args.mysql) {
+ var connectinArgs = (parent.args.mariadb) ? parent.args.mariadb : parent.args.mysql;
+ if (typeof connectinArgs == 'string') {
+ const parts = connectinArgs.split(/[:@/]+/);
+ var connectionObject = {
+ "user": parts[1],
+ "password": parts[2],
+ "host": parts[3],
+ "port": parts[4],
+ "database": parts[5]
+ };
+ var dbname = (connectionObject.database != null) ? connectionObject.database : 'meshcentral';
+ } else {
+ var dbname = (connectinArgs.database != null) ? connectinArgs.database : 'meshcentral';
+
+ // Including the db name in the connection obj will cause a connection faliure if it does not exist
+ var connectionObject = Clone(connectinArgs);
+ delete connectionObject.database;
+
+ try {
+ if (connectinArgs.ssl) {
+ if (connectinArgs.ssl.dontcheckserveridentity == true) { connectionObject.ssl.checkServerIdentity = function (name, cert) { return undefined; } };
+ if (connectinArgs.ssl.cacertpath) { connectionObject.ssl.ca = [require('fs').readFileSync(connectinArgs.ssl.cacertpath, 'utf8')]; }
+ if (connectinArgs.ssl.clientcertpath) { connectionObject.ssl.cert = [require('fs').readFileSync(connectinArgs.ssl.clientcertpath, 'utf8')]; }
+ if (connectinArgs.ssl.clientkeypath) { connectionObject.ssl.key = [require('fs').readFileSync(connectinArgs.ssl.clientkeypath, 'utf8')]; }
+ }
+ } catch (ex) {
+ console.log('Error loading SQL Connector certificate: ' + ex);
+ process.exit();
+ }
+ }
+
+ if (parent.args.mariadb) {
+ // Use MariaDB
+ obj.databaseType = DB_MARIADB;
+ var tempDatastore = require('mariadb').createPool(connectionObject);
+ tempDatastore.getConnection().then(function (conn) {
+ conn.query('CREATE DATABASE IF NOT EXISTS ' + dbname).then(function (result) {
+ conn.release();
+ }).catch(function (ex) { console.log('Auto-create database failed: ' + ex); });
+ }).catch(function (ex) { console.log('Auto-create database failed: ' + ex); });
+ setTimeout(function () { tempDatastore.end(); }, 2000);
+
+ connectionObject.database = dbname;
+ Datastore = require('mariadb').createPool(connectionObject);
+ createTablesIfNotExist(dbname);
+ } else if (parent.args.mysql) {
+ // Use MySQL
+ obj.databaseType = DB_MYSQL;
+ var tempDatastore = require('mysql2').createPool(connectionObject);
+ tempDatastore.query('CREATE DATABASE IF NOT EXISTS ' + dbname, function (error) {
+ if (error != null) {
+ console.log('Auto-create database failed: ' + error);
+ }
+ connectionObject.database = dbname;
+ Datastore = require('mysql2').createPool(connectionObject);
+ createTablesIfNotExist(dbname);
+ });
+ setTimeout(function () { tempDatastore.end(); }, 2000);
+ }
+ } else if (parent.args.postgres) {
+ // Postgres SQL
+ let connectinArgs = parent.args.postgres;
+ connectinArgs.database = (databaseName = (connectinArgs.database != null) ? connectinArgs.database : 'meshcentral');
+
+ let DatastoreTest;
+ obj.databaseType = DB_POSTGRESQL;
+ const { Client } = require('pg');
+ Datastore = new Client(connectinArgs);
+ //Connect to and check pg db first to check if own db exists. Otherwise errors out on 'database does not exist'
+ connectinArgs.database = 'postgres';
+ DatastoreTest = new Client(connectinArgs);
+ DatastoreTest.connect();
+ connectinArgs.database = databaseName; //put the name back for backupconfig info
+ DatastoreTest.query('SELECT 1 FROM pg_catalog.pg_database WHERE datname = $1', [databaseName], function (err, res) { // check database exists first before creating
+ if (res.rowCount != 0) { // database exists now check tables exists
+ DatastoreTest.end();
+ Datastore.connect();
+ Datastore.query('SELECT doc FROM main WHERE id = $1', ['DatabaseIdentifier'], function (err, res) {
+ if (err == null) {
+ (res.rowCount ==0) ? postgreSqlCreateTables(func) : setupFunctions(func)
+ } else
+ if (err.code == '42P01') { //42P01 = undefined table, https://www.postgresql.org/docs/current/errcodes-appendix.html
+ postgreSqlCreateTables(func);
+ } else {
+ console.log('Postgresql database exists, other error: ', err.message); process.exit(0);
+ };
+ });
+ } else { // If not present, create the tables and indexes
+ //not needed, just use a create db statement: const pgtools = require('pgtools');
+ DatastoreTest.query('CREATE DATABASE '+ databaseName + ';', [], function (err, res) {
+ if (err == null) {
+ // Create the tables and indexes
+ DatastoreTest.end();
+ Datastore.connect();
+ postgreSqlCreateTables(func);
+ } else {
+ console.log('Postgresql database create error: ', err.message);
+ process.exit(0);
+ }
+ });
+ }
+ });
+ } else if (parent.args.mongodb) {
+ // Use MongoDB
+ obj.databaseType = DB_MONGODB;
+
+ // If running an older NodeJS version, TextEncoder/TextDecoder is required
+ if (global.TextEncoder == null) { global.TextEncoder = require('util').TextEncoder; }
+ if (global.TextDecoder == null) { global.TextDecoder = require('util').TextDecoder; }
+
+ require('mongodb').MongoClient.connect(parent.args.mongodb, { useNewUrlParser: true, useUnifiedTopology: true, enableUtf8Validation: false }, function (err, client) {
+ if (err != null) { console.log("Unable to connect to database: " + err); process.exit(); return; }
+ Datastore = client;
+ parent.debug('db', 'Connected to MongoDB database...');
+
+ // Get the database name and setup the database client
+ var dbname = 'meshcentral';
+ if (parent.args.mongodbname) { dbname = parent.args.mongodbname; }
+ const dbcollectionname = (parent.args.mongodbcol) ? (parent.args.mongodbcol) : 'meshcentral';
+ const db = client.db(dbname);
+
+ // Check the database version
+ db.admin().serverInfo(function (err, info) {
+ if ((err != null) || (info == null) || (info.versionArray == null) || (Array.isArray(info.versionArray) == false) || (info.versionArray.length < 2) || (typeof info.versionArray[0] != 'number') || (typeof info.versionArray[1] != 'number')) {
+ console.log('WARNING: Unable to check MongoDB version.');
+ } else {
+ if ((info.versionArray[0] < 3) || ((info.versionArray[0] == 3) && (info.versionArray[1] < 6))) {
+ // We are running with mongoDB older than 3.6, this is not good.
+ parent.addServerWarning("Current version of MongoDB (" + info.version + ") is too old, please upgrade to MongoDB 3.6 or better.", true);
+ }
+ }
+ });
+
+ // Setup MongoDB main collection and indexes
+ obj.file = db.collection(dbcollectionname);
+ obj.file.indexes(function (err, indexes) {
+ // Check if we need to reset indexes
+ var indexesByName = {}, indexCount = 0;
+ for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
+ if ((indexCount != 5) || (indexesByName['TypeDomainMesh1'] == null) || (indexesByName['TypeDomainRnameIndex1'] == null) || (indexesByName['Email1'] == null) || (indexesByName['Mesh1'] == null) || (indexesByName['AmtUuid1'] == null)) {
+ console.log('Resetting main indexes...');
+ obj.file.dropIndexes(function (err) {
+ obj.file.createIndex({ type: 1, domain: 1, meshid: 1 }, { sparse: 1, name: 'TypeDomainMesh1' }); // Speeds up GetAllTypeNoTypeField() and GetAllTypeNoTypeFieldMeshFiltered()
+ obj.file.createIndex({ type: 1, domain: 1, rname: 1 }, { sparse: 1, name: "TypeDomainRnameIndex1" }); // Speeds up
+ obj.file.createIndex({ email: 1 }, { sparse: 1, name: 'Email1' }); // Speeds up GetUserWithEmail() and GetUserWithVerifiedEmail()
+ obj.file.createIndex({ meshid: 1 }, { sparse: 1, name: 'Mesh1' }); // Speeds up RemoveMesh()
+ obj.file.createIndex({ 'intelamt.uuid': 1 }, { sparse: 1, name: 'AmtUuid1' }); // Speeds up getAmtUuidMeshNode()
+ });
+ }
+ });
+
+ // Setup the changeStream on the MongoDB main collection if possible
+ if (parent.args.mongodbchangestream == true) {
+ obj.dbCounters.changeStream = { change: 0, update: 0, insert: 0, delete: 0 };
+ if (typeof obj.file.watch != 'function') {
+ console.log('WARNING: watch() is not a function, MongoDB ChangeStream not supported.');
+ } else {
+ obj.fileChangeStream = obj.file.watch([{ $match: { $or: [{ 'fullDocument.type': { $in: ['node', 'mesh', 'user', 'ugrp'] } }, { 'operationType': 'delete' }] } }], { fullDocument: 'updateLookup' });
+ obj.fileChangeStream.on('change', function (change) {
+ obj.dbCounters.changeStream.change++;
+ if ((change.operationType == 'update') || (change.operationType == 'replace')) {
+ obj.dbCounters.changeStream.update++;
+ switch (change.fullDocument.type) {
+ case 'node': { dbNodeChange(change, false); break; } // A node has changed
+ case 'mesh': { dbMeshChange(change, false); break; } // A device group has changed
+ case 'user': { dbUserChange(change, false); break; } // A user account has changed
+ case 'ugrp': { dbUGrpChange(change, false); break; } // A user account has changed
+ }
+ } else if (change.operationType == 'insert') {
+ obj.dbCounters.changeStream.insert++;
+ switch (change.fullDocument.type) {
+ case 'node': { dbNodeChange(change, true); break; } // A node has added
+ case 'mesh': { dbMeshChange(change, true); break; } // A device group has created
+ case 'user': { dbUserChange(change, true); break; } // A user account has created
+ case 'ugrp': { dbUGrpChange(change, true); break; } // A user account has created
+ }
+ } else if (change.operationType == 'delete') {
+ obj.dbCounters.changeStream.delete++;
+ if ((change.documentKey == null) || (change.documentKey._id == null)) return;
+ var splitId = change.documentKey._id.split('/');
+ switch (splitId[0]) {
+ case 'node': {
+ //Not Good: Problem here is that we don't know what meshid the node belonged to before the delete.
+ //parent.DispatchEvent(['*', node.meshid], obj, { etype: 'node', action: 'removenode', nodeid: change.documentKey._id, domain: splitId[1] });
+ break;
+ }
+ case 'mesh': {
+ parent.DispatchEvent(['*', change.documentKey._id], obj, { etype: 'mesh', action: 'deletemesh', meshid: change.documentKey._id, domain: splitId[1] });
+ break;
+ }
+ case 'user': {
+ //Not Good: This is not a perfect user removal because we don't know what groups the user was in.
+ //parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', action: 'accountremove', userid: change.documentKey._id, domain: splitId[1], username: splitId[2] });
+ break;
+ }
+ case 'ugrp': {
+ parent.DispatchEvent(['*', change.documentKey._id], obj, { etype: 'ugrp', action: 'deleteusergroup', ugrpid: change.documentKey._id, domain: splitId[1] });
+ break;
+ }
+ }
+ }
+ });
+ obj.changeStream = true;
+ }
+ }
+
+ // Setup MongoDB events collection and indexes
+ obj.eventsfile = db.collection('events'); // Collection containing all events
+ obj.eventsfile.indexes(function (err, indexes) {
+ // Check if we need to reset indexes
+ var indexesByName = {}, indexCount = 0;
+ for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
+ if ((indexCount != 5) || (indexesByName['UseridAction1'] == null) || (indexesByName['DomainNodeTime1'] == null) || (indexesByName['IdsAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
+ // Reset all indexes
+ console.log("Resetting events indexes...");
+ obj.eventsfile.dropIndexes(function (err) {
+ obj.eventsfile.createIndex({ userid: 1, action: 1 }, { sparse: 1, name: 'UseridAction1' });
+ obj.eventsfile.createIndex({ domain: 1, nodeid: 1, time: -1 }, { sparse: 1, name: 'DomainNodeTime1' });
+ obj.eventsfile.createIndex({ ids: 1, time: -1 }, { sparse: 1, name: 'IdsAndTime1' });
+ obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
+ });
+ } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireEventsSeconds) {
+ // Reset the timeout index
+ console.log("Resetting events expire index...");
+ obj.eventsfile.dropIndex('ExpireTime1', function (err) {
+ obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
+ });
+ }
+ });
+
+ // Setup MongoDB power events collection and indexes
+ obj.powerfile = db.collection('power'); // Collection containing all power events
+ obj.powerfile.indexes(function (err, indexes) {
+ // Check if we need to reset indexes
+ var indexesByName = {}, indexCount = 0;
+ for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
+ if ((indexCount != 3) || (indexesByName['NodeIdAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
+ // Reset all indexes
+ console.log("Resetting power events indexes...");
+ obj.powerfile.dropIndexes(function (err) {
+ // Create all indexes
+ obj.powerfile.createIndex({ nodeid: 1, time: 1 }, { sparse: 1, name: 'NodeIdAndTime1' });
+ obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
+ });
+ } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expirePowerEventsSeconds) {
+ // Reset the timeout index
+ console.log("Resetting power events expire index...");
+ obj.powerfile.dropIndex('ExpireTime1', function (err) {
+ // Reset the expire power events index
+ obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
+ });
+ }
+ });
+
+ // Setup MongoDB smbios collection, no indexes needed
+ obj.smbiosfile = db.collection('smbios'); // Collection containing all smbios information
+
+ // Setup MongoDB server stats collection
+ obj.serverstatsfile = db.collection('serverstats'); // Collection of server stats
+ obj.serverstatsfile.indexes(function (err, indexes) {
+ // Check if we need to reset indexes
+ var indexesByName = {}, indexCount = 0;
+ for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
+ if ((indexCount != 3) || (indexesByName['ExpireTime1'] == null)) {
+ // Reset all indexes
+ console.log("Resetting server stats indexes...");
+ obj.serverstatsfile.dropIndexes(function (err) {
+ // Create all indexes
+ obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
+ obj.serverstatsfile.createIndex({ 'expire': 1 }, { expireAfterSeconds: 0, name: 'ExpireTime2' }); // Auto-expire events
+ });
+ } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireServerStatsSeconds) {
+ // Reset the timeout index
+ console.log("Resetting server stats expire index...");
+ obj.serverstatsfile.dropIndex('ExpireTime1', function (err) {
+ // Reset the expire server stats index
+ obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
+ });
+ }
+ });
+
+ // Setup plugin info collection
+ if (obj.pluginsActive) { obj.pluginsfile = db.collection('plugins'); }
+
+ setupFunctions(func); // Completed setup of MongoDB
+ });
+ } else if (parent.args.xmongodb) {
+ // Use MongoJS, this is the old system.
+ obj.databaseType = DB_MONGOJS;
+ Datastore = require('mongojs');
+ var db = Datastore(parent.args.xmongodb);
+ var dbcollection = 'meshcentral';
+ if (parent.args.mongodbcol) { dbcollection = parent.args.mongodbcol; }
+
+ // Setup MongoDB main collection and indexes
+ obj.file = db.collection(dbcollection);
+ obj.file.getIndexes(function (err, indexes) {
+ // Check if we need to reset indexes
+ var indexesByName = {}, indexCount = 0;
+ for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
+ if ((indexCount != 5) || (indexesByName['TypeDomainMesh1'] == null) || (indexesByName['TypeDomainRnameIndex1'] == null) || (indexesByName['Email1'] == null) || (indexesByName['Mesh1'] == null) || (indexesByName['AmtUuid1'] == null)) {
+ console.log("Resetting main indexes...");
+ obj.file.dropIndexes(function (err) {
+ obj.file.createIndex({ type: 1, domain: 1, meshid: 1 }, { sparse: 1, name: 'TypeDomainMesh1' }); // Speeds up GetAllTypeNoTypeField() and GetAllTypeNoTypeFieldMeshFiltered()
+ obj.file.createIndex({ type: 1, domain: 1, rname: 1 }, { sparse: 1, name: "TypeDomainRnameIndex1" });
+ obj.file.createIndex({ email: 1 }, { sparse: 1, name: 'Email1' }); // Speeds up GetUserWithEmail() and GetUserWithVerifiedEmail()
+ obj.file.createIndex({ meshid: 1 }, { sparse: 1, name: 'Mesh1' }); // Speeds up RemoveMesh()
+ obj.file.createIndex({ 'intelamt.uuid': 1 }, { sparse: 1, name: 'AmtUuid1' }); // Speeds up getAmtUuidMeshNode()
+ });
+ }
+ });
+
+ // Setup MongoDB events collection and indexes
+ obj.eventsfile = db.collection('events'); // Collection containing all events
+ obj.eventsfile.getIndexes(function (err, indexes) {
+ // Check if we need to reset indexes
+ var indexesByName = {}, indexCount = 0;
+ for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
+ if ((indexCount != 5) || (indexesByName['UseridAction1'] == null) || (indexesByName['DomainNodeTime1'] == null) || (indexesByName['IdsAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
+ // Reset all indexes
+ console.log("Resetting events indexes...");
+ obj.eventsfile.dropIndexes(function (err) {
+ obj.eventsfile.createIndex({ userid: 1, action: 1 }, { sparse: 1, name: 'UseridAction1' });
+ obj.eventsfile.createIndex({ domain: 1, nodeid: 1, time: -1 }, { sparse: 1, name: 'DomainNodeTime1' });
+ obj.eventsfile.createIndex({ ids: 1, time: -1 }, { sparse: 1, name: 'IdsAndTime1' });
+ obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
+ });
+ } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireEventsSeconds) {
+ // Reset the timeout index
+ console.log("Resetting events expire index...");
+ obj.eventsfile.dropIndex('ExpireTime1', function (err) {
+ obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
+ });
+ }
+ });
+
+ // Setup MongoDB power events collection and indexes
+ obj.powerfile = db.collection('power'); // Collection containing all power events
+ obj.powerfile.getIndexes(function (err, indexes) {
+ // Check if we need to reset indexes
+ var indexesByName = {}, indexCount = 0;
+ for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
+ if ((indexCount != 3) || (indexesByName['NodeIdAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
+ // Reset all indexes
+ console.log("Resetting power events indexes...");
+ obj.powerfile.dropIndexes(function (err) {
+ // Create all indexes
+ obj.powerfile.createIndex({ nodeid: 1, time: 1 }, { sparse: 1, name: 'NodeIdAndTime1' });
+ obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
+ });
+ } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expirePowerEventsSeconds) {
+ // Reset the timeout index
+ console.log("Resetting power events expire index...");
+ obj.powerfile.dropIndex('ExpireTime1', function (err) {
+ // Reset the expire power events index
+ obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
+ });
+ }
+ });
+
+ // Setup MongoDB smbios collection, no indexes needed
+ obj.smbiosfile = db.collection('smbios'); // Collection containing all smbios information
+
+ // Setup MongoDB server stats collection
+ obj.serverstatsfile = db.collection('serverstats'); // Collection of server stats
+ obj.serverstatsfile.getIndexes(function (err, indexes) {
+ // Check if we need to reset indexes
+ var indexesByName = {}, indexCount = 0;
+ for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
+ if ((indexCount != 3) || (indexesByName['ExpireTime1'] == null)) {
+ // Reset all indexes
+ console.log("Resetting server stats indexes...");
+ obj.serverstatsfile.dropIndexes(function (err) {
+ // Create all indexes
+ obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
+ obj.serverstatsfile.createIndex({ 'expire': 1 }, { expireAfterSeconds: 0, name: 'ExpireTime2' }); // Auto-expire events
+ });
+ } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireServerStatsSeconds) {
+ // Reset the timeout index
+ console.log("Resetting server stats expire index...");
+ obj.serverstatsfile.dropIndex('ExpireTime1', function (err) {
+ // Reset the expire server stats index
+ obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
+ });
+ }
+ });
+
+ // Setup plugin info collection
+ if (obj.pluginsActive) { obj.pluginsfile = db.collection('plugins'); }
+
+ setupFunctions(func); // Completed setup of MongoJS
+ } else {
+ // Use NeDB (The default)
+ obj.databaseType = DB_NEDB;
+ try { Datastore = require('@seald-io/nedb'); } catch (ex) { } // This is the NeDB with Node 23 support.
+ if (Datastore == null) {
+ try { Datastore = require('@yetzt/nedb'); } catch (ex) { } // This is the NeDB with fixed security dependencies.
+ if (Datastore == null) { Datastore = require('nedb'); } // So not to break any existing installations, if the old NeDB is present, use it.
+ }
+ var datastoreOptions = { filename: parent.getConfigFilePath('meshcentral.db'), autoload: true };
+
+ // If a DB encryption key is provided, perform database encryption
+ if ((typeof parent.args.dbencryptkey == 'string') && (parent.args.dbencryptkey.length != 0)) {
+ // Hash the database password into a AES256 key and setup encryption and decryption.
+ obj.dbKey = parent.crypto.createHash('sha384').update(parent.args.dbencryptkey).digest('raw').slice(0, 32);
+ datastoreOptions.afterSerialization = function (plaintext) {
+ const iv = parent.crypto.randomBytes(16);
+ const aes = parent.crypto.createCipheriv('aes-256-cbc', obj.dbKey, iv);
+ var ciphertext = aes.update(plaintext);
+ ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
+ return ciphertext.toString('base64');
+ }
+ datastoreOptions.beforeDeserialization = function (ciphertext) {
+ const ciphertextBytes = Buffer.from(ciphertext, 'base64');
+ const iv = ciphertextBytes.slice(0, 16);
+ const data = ciphertextBytes.slice(16);
+ const aes = parent.crypto.createDecipheriv('aes-256-cbc', obj.dbKey, iv);
+ var plaintextBytes = Buffer.from(aes.update(data));
+ plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
+ return plaintextBytes.toString();
+ }
+ }
+
+ // Start NeDB main collection and setup indexes
+ obj.file = new Datastore(datastoreOptions);
+ obj.file.setAutocompactionInterval(86400000); // Compact once a day
+ obj.file.ensureIndex({ fieldName: 'type' });
+ obj.file.ensureIndex({ fieldName: 'domain' });
+ obj.file.ensureIndex({ fieldName: 'meshid', sparse: true });
+ obj.file.ensureIndex({ fieldName: 'nodeid', sparse: true });
+ obj.file.ensureIndex({ fieldName: 'email', sparse: true });
+
+ // Setup the events collection and setup indexes
+ obj.eventsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-events.db'), autoload: true, corruptAlertThreshold: 1 });
+ obj.eventsfile.setAutocompactionInterval(86400000); // Compact once a day
+ obj.eventsfile.ensureIndex({ fieldName: 'ids' }); // TODO: Not sure if this is a good index, this is a array field.
+ obj.eventsfile.ensureIndex({ fieldName: 'nodeid', sparse: true });
+ obj.eventsfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: expireEventsSeconds });
+ obj.eventsfile.remove({ time: { '$lt': new Date(Date.now() - (expireEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
+
+ // Setup the power collection and setup indexes
+ obj.powerfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-power.db'), autoload: true, corruptAlertThreshold: 1 });
+ obj.powerfile.setAutocompactionInterval(86400000); // Compact once a day
+ obj.powerfile.ensureIndex({ fieldName: 'nodeid' });
+ obj.powerfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: expirePowerEventsSeconds });
+ obj.powerfile.remove({ time: { '$lt': new Date(Date.now() - (expirePowerEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
+
+ // Setup the SMBIOS collection, for NeDB we don't setup SMBIOS since NeDB will corrupt the database. Remove any existing ones.
+ //obj.smbiosfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-smbios.db'), autoload: true, corruptAlertThreshold: 1 });
+ fs.unlink(parent.getConfigFilePath('meshcentral-smbios.db'), function () { });
+
+ // Setup the server stats collection and setup indexes
+ obj.serverstatsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-stats.db'), autoload: true, corruptAlertThreshold: 1 });
+ obj.serverstatsfile.setAutocompactionInterval(86400000); // Compact once a day
+ obj.serverstatsfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: expireServerStatsSeconds });
+ obj.serverstatsfile.ensureIndex({ fieldName: 'expire', expireAfterSeconds: 0 }); // Auto-expire events
+ obj.serverstatsfile.remove({ time: { '$lt': new Date(Date.now() - (expireServerStatsSeconds * 1000)) } }, { multi: true }); // Force delete older events
+
+ // Setup plugin info collection
+ if (obj.pluginsActive) {
+ obj.pluginsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-plugins.db'), autoload: true });
+ obj.pluginsfile.setAutocompactionInterval(86400000); // Compact once a day
+ }
+
+ setupFunctions(func); // Completed setup of NeDB
+ }
+
+ function sqliteSetOptions(func) {
+ //get current auto_vacuum mode for comparison
+ obj.file.get('PRAGMA auto_vacuum;', function(err, current){
+ let pragma = 'PRAGMA journal_mode=' + obj.sqliteConfig.journalMode + ';' +
+ 'PRAGMA synchronous='+ obj.sqliteConfig.synchronous + ';' +
+ 'PRAGMA journal_size_limit=' + obj.sqliteConfig.journalSize + ';' +
+ 'PRAGMA auto_vacuum=' + obj.sqliteConfig.autoVacuum + ';' +
+ 'PRAGMA incremental_vacuum=' + obj.sqliteConfig.incrementalVacuum + ';' +
+ 'PRAGMA optimize=0x10002;';
+ //check new autovacuum mode, if changing from or to 'none', a VACUUM needs to be done to activate it. See https://www.sqlite.org/pragma.html#pragma_auto_vacuum
+ if ( obj.sqliteConfig.startupVacuum
+ || (current.auto_vacuum == 0 && obj.sqliteConfig.autoVacuum !='none')
+ || (current.auto_vacuum != 0 && obj.sqliteConfig.autoVacuum =='none'))
+ {
+ pragma += 'VACUUM;';
+ };
+ parent.debug ('db', 'Config statement: ' + pragma);
+
+ obj.file.exec( pragma,
+ function (err) {
+ if (err) { parent.debug('db', 'Config pragma error: ' + (err.message)) };
+ sqliteGetPragmas(['journal_mode', 'journal_size_limit', 'freelist_count', 'auto_vacuum', 'page_size', 'wal_autocheckpoint', 'synchronous'], function (pragma, pragmaValue) {
+ parent.debug('db', 'PRAGMA: ' + pragma + '=' + pragmaValue);
+ });
+ });
+ });
+ //setupFunctions(func);
+ }
+
+ function sqliteGetPragmas (pragmas, func){
+ //pragmas can only be gotting one by one
+ pragmas.forEach (function (pragma) {
+ obj.file.get('PRAGMA ' + pragma + ';', function(err, res){
+ if (pragma == 'auto_vacuum') { res[pragma] = SQLITE_AUTOVACUUM[res[pragma]] };
+ if (pragma == 'synchronous') { res[pragma] = SQLITE_SYNCHRONOUS[res[pragma]] };
+ if (func) { func (pragma, res[pragma]); }
+ });
+ });
+ }
+ // Create the PostgreSQL tables
+ function postgreSqlCreateTables(func) {
+ // Database was created, create the tables
+ parent.debug('db', 'Creating tables...');
+ sqlDbBatchExec([
+ 'CREATE TABLE IF NOT EXISTS main (id VARCHAR(256) PRIMARY KEY NOT NULL, type CHAR(32), domain CHAR(64), extra CHAR(255), extraex CHAR(255), doc JSON)',
+ 'CREATE TABLE IF NOT EXISTS events(id SERIAL PRIMARY KEY, time TIMESTAMP, domain CHAR(64), action CHAR(255), nodeid CHAR(255), userid CHAR(255), doc JSON)',
+ 'CREATE TABLE IF NOT EXISTS eventids(fkid INT NOT NULL, target CHAR(255), CONSTRAINT fk_eventid FOREIGN KEY (fkid) REFERENCES events (id) ON DELETE CASCADE ON UPDATE RESTRICT)',
+ 'CREATE TABLE IF NOT EXISTS serverstats (time TIMESTAMP PRIMARY KEY, expire TIMESTAMP, doc JSON)',
+ 'CREATE TABLE IF NOT EXISTS power (id SERIAL PRIMARY KEY, time TIMESTAMP, nodeid CHAR(255), doc JSON)',
+ 'CREATE TABLE IF NOT EXISTS smbios (id CHAR(255) PRIMARY KEY, time TIMESTAMP, expire TIMESTAMP, doc JSON)',
+ 'CREATE TABLE IF NOT EXISTS plugin (id SERIAL PRIMARY KEY, doc JSON)'
+ ], function (results) {
+ parent.debug('db', 'Creating indexes...');
+ sqlDbExec('CREATE INDEX ndxtypedomainextra ON main (type, domain, extra)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxextra ON main (extra)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxextraex ON main (extraex)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxeventstime ON events(time)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxeventsusername ON events(domain, userid, time)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxeventsdomainnodeidtime ON events(domain, nodeid, time)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxeventids ON eventids(target)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxserverstattime ON serverstats (time)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxserverstatexpire ON serverstats (expire)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxpowernodeidtime ON power (nodeid, time)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxsmbiostime ON smbios (time)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxsmbiosexpire ON smbios (expire)', null, function (err, response) { });
+ setupFunctions(func);
+ });
+ }
+
+ // Check the object names for a "."
+ function checkObjectNames(r, tag) {
+ if (typeof r != 'object') return;
+ for (var i in r) {
+ if (i.indexOf('.') >= 0) { throw ('BadDbName (' + tag + '): ' + JSON.stringify(r)); }
+ checkObjectNames(r[i], tag);
+ }
+ }
+
+ // Query the database
+ function sqlDbQuery(query, args, func, debug) {
+ if (obj.databaseType == DB_SQLITE) { // SQLite
+ if (args == null) { args = []; }
+ obj.file.all(query, args, function (err, docs) {
+ if (err != null) { console.log(query, args, err, docs); }
+ if (docs != null) {
+ for (var i in docs) {
+ if (typeof docs[i].doc == 'string') {
+ try { docs[i] = JSON.parse(docs[i].doc); } catch (ex) {
+ console.log(query, args, docs[i]);
+ }
+ }
+ }
+ }
+ if (func) { func(err, docs); }
+ });
+ } else if (obj.databaseType == DB_MARIADB) { // MariaDB
+ Datastore.getConnection()
+ .then(function (conn) {
+ conn.query(query, args)
+ .then(function (rows) {
+ conn.release();
+ var docs = [];
+ for (var i in rows) {
+ if (rows[i].doc) {
+ docs.push(performTypedRecordDecrypt((typeof rows[i].doc == 'object') ? rows[i].doc : JSON.parse(rows[i].doc)));
+ } else if ((rows.length == 1) && (rows[i]['COUNT(doc)'] != null)) {
+ // This is a SELECT COUNT() operation
+ docs = parseInt(rows[i]['COUNT(doc)']);
+ }
+ }
+ if (func) try { func(null, docs); } catch (ex) { console.log('SQLERR1', ex); }
+ })
+ .catch(function (err) { conn.release(); if (func) try { func(err); } catch (ex) { console.log('SQLERR2', ex); } });
+ }).catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log('SQLERR3', ex); } } });
+ } else if (obj.databaseType == DB_MYSQL) { // MySQL
+ Datastore.query(query, args, function (error, results, fields) {
+ if (error != null) {
+ if (func) try { func(error); } catch (ex) { console.log('SQLERR4', ex); }
+ } else {
+ var docs = [];
+ for (var i in results) {
+ if (results[i].doc) {
+ if (typeof results[i].doc == 'string') {
+ docs.push(JSON.parse(results[i].doc));
+ } else {
+ docs.push(results[i].doc);
+ }
+ } else if ((results.length == 1) && (results[i]['COUNT(doc)'] != null)) {
+ // This is a SELECT COUNT() operation
+ docs = results[i]['COUNT(doc)'];
+ }
+ }
+ if (func) { try { func(null, docs); } catch (ex) { console.log('SQLERR5', ex); } }
+ }
+ });
+ } else if (obj.databaseType == DB_POSTGRESQL) { // Postgres SQL
+ Datastore.query(query, args, function (error, results) {
+ if (error != null) {
+ if (func) try { func(error); } catch (ex) { console.log('SQLERR4', ex); }
+ } else {
+ var docs = [];
+ if ((results.command == 'INSERT') && (results.rows != null) && (results.rows.length == 1)) { docs = results.rows[0]; }
+ else if (results.command == 'SELECT') {
+ for (var i in results.rows) {
+ if (results.rows[i].doc) {
+ if (typeof results.rows[i].doc == 'string') {
+ docs.push(JSON.parse(results.rows[i].doc));
+ } else {
+ docs.push(results.rows[i].doc);
+ }
+ } else if (results.rows[i].count && (results.rows.length == 1)) {
+ // This is a SELECT COUNT() operation
+ docs = parseInt(results.rows[i].count);
+ }
+ }
+ }
+ if (func) { try { func(null, docs, results); } catch (ex) { console.log('SQLERR5', ex); } }
+ }
+ });
+ }
+ }
+
+ // Exec on the database
+ function sqlDbExec(query, args, func) {
+ if (obj.databaseType == DB_MARIADB) { // MariaDB
+ Datastore.getConnection()
+ .then(function (conn) {
+ conn.query(query, args)
+ .then(function (rows) {
+ conn.release();
+ if (func) try { func(null, rows[0]); } catch (ex) { console.log(ex); }
+ })
+ .catch(function (err) { conn.release(); if (func) try { func(err); } catch (ex) { console.log(ex); } });
+ }).catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } });
+ } else if ((obj.databaseType == DB_MYSQL) || (obj.databaseType == DB_POSTGRESQL)) { // MySQL or Postgres SQL
+ Datastore.query(query, args, function (error, results, fields) {
+ if (func) try { func(error, results ? results[0] : null); } catch (ex) { console.log(ex); }
+ });
+ }
+ }
+
+ // Execute a batch of commands on the database
+ function sqlDbBatchExec(queries, func) {
+ if (obj.databaseType == DB_MARIADB) { // MariaDB
+ Datastore.getConnection()
+ .then(function (conn) {
+ var Promises = [];
+ for (var i in queries) { if (typeof queries[i] == 'string') { Promises.push(conn.query(queries[i])); } else { Promises.push(conn.query(queries[i][0], queries[i][1])); } }
+ Promise.all(Promises)
+ .then(function (rows) { conn.release(); if (func) { try { func(null); } catch (ex) { console.log(ex); } } })
+ .catch(function (err) { conn.release(); if (func) { try { func(err); } catch (ex) { console.log(ex); } } });
+ })
+ .catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } });
+ } else if (obj.databaseType == DB_MYSQL) { // MySQL
+ Datastore.getConnection(function(err, connection) {
+ if (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } return; }
+ var Promises = [];
+ for (var i in queries) { if (typeof queries[i] == 'string') { Promises.push(connection.promise().query(queries[i])); } else { Promises.push(connection.promise().query(queries[i][0], queries[i][1])); } }
+ Promise.all(Promises)
+ .then(function (error, results, fields) { connection.release(); if (func) { try { func(error, results); } catch (ex) { console.log(ex); } } })
+ .catch(function (error, results, fields) { connection.release(); if (func) { try { func(error); } catch (ex) { console.log(ex); } } });
+ });
+ } else if (obj.databaseType == DB_POSTGRESQL) { // Postgres
+ var Promises = [];
+ for (var i in queries) { if (typeof queries[i] == 'string') { Promises.push(Datastore.query(queries[i])); } else { Promises.push(Datastore.query(queries[i][0], queries[i][1])); } }
+ Promise.all(Promises)
+ .then(function (error, results, fields) { if (func) { try { func(error, results); } catch (ex) { console.log(ex); } } })
+ .catch(function (error, results, fields) { if (func) { try { func(error); } catch (ex) { console.log(ex); } } });
+ }
+ }
+
+ function setupFunctions(func) {
+ if (obj.databaseType == DB_SQLITE) {
+ // Database actions on the main collection. SQLite3: https://www.linode.com/docs/guides/getting-started-with-nodejs-sqlite/
+ obj.Set = function (value, func) {
+ obj.dbCounters.fileSet++;
+ var extra = null, extraex = null;
+ value = common.escapeLinksFieldNameEx(value);
+ if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
+ if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
+ if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
+ sqlDbQuery('INSERT INTO main VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET type = $2, domain = $3, extra = $4, extraex = $5, doc = $6;', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, JSON.stringify(performTypedRecordEncrypt(value))], func);
+ }
+ obj.SetRaw = function (value, func) {
+ obj.dbCounters.fileSet++;
+ var extra = null, extraex = null;
+ if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
+ if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
+ if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
+ sqlDbQuery('INSERT INTO main VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET type = $2, domain = $3, extra = $4, extraex = $5, doc = $6;', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, JSON.stringify(performTypedRecordEncrypt(value))], func);
+ }
+ obj.Get = function (_id, func) {
+ sqlDbQuery('SELECT doc FROM main WHERE id = $1', [_id], function (err, docs) {
+ if ((docs != null) && (docs.length > 0)) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ }
+ obj.GetAll = function (func) {
+ sqlDbQuery('SELECT domain, doc FROM main', null, function (err, docs) {
+ if ((docs != null) && (docs.length > 0)) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ }
+ obj.GetHash = function (id, func) {
+ sqlDbQuery('SELECT doc FROM main WHERE id = $1', [id], function (err, docs) {
+ if ((docs != null) && (docs.length > 0)) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ }
+ obj.GetAllTypeNoTypeField = function (type, domain, func) {
+ sqlDbQuery('SELECT doc FROM main WHERE type = $1 AND domain = $2', [type, domain], function (err, docs) {
+ if ((docs != null) && (docs.length > 0)) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ };
+ obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
+ if (limit == 0) { limit = -1; } // In SQLite, no limit is -1
+ if (id && (id != '')) {
+ sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra IN (' + dbMergeSqlArray(meshes) + ')) LIMIT $4 OFFSET $5', [id, type, domain, limit, skip], function (err, docs) {
+ if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ } else {
+ if (extrasids == null) {
+ sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra IN (' + dbMergeSqlArray(meshes) + ')) LIMIT $3 OFFSET $4', [type, domain, limit, skip], function (err, docs) {
+ if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ } else {
+ sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra IN (' + dbMergeSqlArray(meshes) + ')) OR (id IN (' + dbMergeSqlArray(extrasids) + '))) LIMIT $3 OFFSET $4', [type, domain, limit, skip], function (err, docs) {
+ if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ }
+ }
+ };
+ obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
+ if (id && (id != '')) {
+ sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra IN (' + dbMergeSqlArray(meshes) + '))', [id, type, domain], function (err, docs) {
+ func(err, (err == null) ? docs[0]['COUNT(doc)'] : null);
+ });
+ } else {
+ if (extrasids == null) {
+ sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (type = $1) AND (domain = $2) AND (extra IN (' + dbMergeSqlArray(meshes) + '))', [type, domain], function (err, docs) {
+ func(err, (err == null) ? docs[0]['COUNT(doc)'] : null);
+ });
+ } else {
+ sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (type = $1) AND (domain = $2) AND ((extra IN (' + dbMergeSqlArray(meshes) + ')) OR (id IN (' + dbMergeSqlArray(extrasids) + ')))', [type, domain], function (err, docs) {
+ func(err, (err == null) ? docs[0]['COUNT(doc)'] : null);
+ });
+ }
+ }
+ };
+ obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
+ if (id && (id != '')) {
+ sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra IN (' + dbMergeSqlArray(nodes) + '))', [id, type, domain], function (err, docs) {
+ if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ } else {
+ sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra IN (' + dbMergeSqlArray(nodes) + '))', [type, domain], function (err, docs) {
+ if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ }
+ };
+ obj.GetAllType = function (type, func) {
+ sqlDbQuery('SELECT doc FROM main WHERE type = $1', [type], function (err, docs) {
+ if (docs != null) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ }
+ obj.GetAllIdsOfType = function (ids, domain, type, func) {
+ sqlDbQuery('SELECT doc FROM main WHERE (id IN (' + dbMergeSqlArray(ids) + ')) AND domain = $1 AND type = $2', [domain, type], function (err, docs) {
+ if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ }
+ obj.GetUserWithEmail = function (domain, email, func) {
+ sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extra = $2', [domain, 'email/' + email], function (err, docs) {
+ if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ }
+ obj.GetUserWithVerifiedEmail = function (domain, email, func) {
+ sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extra = $2', [domain, 'email/' + email], function (err, docs) {
+ if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ }
+ obj.GetNodeByComputerName = function (domain, rname, func) {
+ sqlDbQuery('SELECT doc FROM main WHERE type = $1 AND domain = $2 AND JSON_EXTRACT(doc, "$.rname") = $3 ORDER BY lastbootuptime',
+ ['node', domain, rname], function (err, docs) {
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ };
+ obj.Remove = function (id, func) { sqlDbQuery('DELETE FROM main WHERE id = $1', [id], func); };
+ obj.RemoveAll = function (func) { sqlDbQuery('DELETE FROM main', null, func); };
+ obj.RemoveAllOfType = function (type, func) { sqlDbQuery('DELETE FROM main WHERE type = $1', [type], func); };
+ obj.InsertMany = function (data, func) { var pendingOps = 0; for (var i in data) { pendingOps++; obj.SetRaw(data[i], function () { if (--pendingOps == 0) { func(); } }); } }; // Insert records directly, no link escaping
+ obj.RemoveMeshDocuments = function (id, func) { sqlDbQuery('DELETE FROM main WHERE extra = $1', [id], function () { sqlDbQuery('DELETE FROM main WHERE id = $1', ['nt' + id], func); }); };
+ obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
+ obj.DeleteDomain = function (domain, func) { sqlDbQuery('DELETE FROM main WHERE domain = $1', [domain], func); };
+ obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
+ obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
+ obj.getLocalAmtNodes = function (func) {
+ sqlDbQuery('SELECT doc FROM main WHERE (type = \'node\') AND (extraex IS NULL)', null, function (err, docs) {
+ if (docs != null) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ var r = []; if (err == null) { for (var i in docs) { if (docs[i].host != null && docs[i].intelamt != null) { r.push(docs[i]); } } } func(err, r);
+ });
+ };
+ obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) {
+ sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extraex = $2', [domainid, 'uuid/' + uuid], function (err, docs) {
+ if (docs != null) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, docs);
+ });
+ };
+ obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { sqlDbExec('SELECT COUNT(id) FROM main WHERE domain = $1 AND type = $2', [domainid, type], function (err, response) { func((response['COUNT(id)'] == null) || (response['COUNT(id)'] > max), response['COUNT(id)']) }); } }
+
+ // Database actions on the events collection
+ obj.GetAllEvents = function (func) {
+ sqlDbQuery('SELECT doc FROM events', null, func);
+ };
+ obj.StoreEvent = function (event, func) {
+ obj.dbCounters.eventsSet++;
+ sqlDbQuery('INSERT INTO events VALUES (NULL, $1, $2, $3, $4, $5, $6) RETURNING id', [event.time, ((typeof event.domain == 'string') ? event.domain : null), event.action, event.nodeid ? event.nodeid : null, event.userid ? event.userid : null, JSON.stringify(event)], function (err, docs) {
+ if(func){ func(); }
+ if ((err == null) && (docs[0].id)) {
+ for (var i in event.ids) {
+ if (event.ids[i] != '*') {
+ obj.pendingTransfer++;
+ sqlDbQuery('INSERT INTO eventids VALUES ($1, $2)', [docs[0].id, event.ids[i]], function(){ if(func){ func(); } });
+ }
+ }
+ }
+ });
+ };
+ obj.GetEvents = function (ids, domain, filter, func) {
+ var query = "SELECT doc FROM events ";
+ var dataarray = [domain];
+ if (ids.indexOf('*') >= 0) {
+ query = query + "WHERE (domain = $1";
+ if (filter != null) {
+ query = query + " AND action = $2";
+ dataarray.push(filter);
+ }
+ query = query + ") ORDER BY time DESC";
+ } else {
+ query = query + 'JOIN eventids ON id = fkid WHERE (domain = $1 AND (target IN (' + dbMergeSqlArray(ids) + '))';
+ if (filter != null) {
+ query = query + " AND action = $2";
+ dataarray.push(filter);
+ }
+ query = query + ") GROUP BY id ORDER BY time DESC ";
+ }
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
+ var query = "SELECT doc FROM events ";
+ var dataarray = [domain];
+ if (ids.indexOf('*') >= 0) {
+ query = query + "WHERE (domain = $1";
+ if (filter != null) {
+ query = query + " AND action = $2) ORDER BY time DESC LIMIT $3";
+ dataarray.push(filter);
+ } else {
+ query = query + ") ORDER BY time DESC LIMIT $2";
+ }
+ } else {
+ query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND (target IN (" + dbMergeSqlArray(ids) + "))";
+ if (filter != null) {
+ query = query + " AND action = $2) GROUP BY id ORDER BY time DESC LIMIT $3";
+ dataarray.push(filter);
+ } else {
+ query = query + ") GROUP BY id ORDER BY time DESC LIMIT $2";
+ }
+ }
+ dataarray.push(limit);
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetUserEvents = function (ids, domain, userid, filter, func) {
+ var query = "SELECT doc FROM events ";
+ var dataarray = [domain, userid];
+ if (ids.indexOf('*') >= 0) {
+ query = query + "WHERE (domain = $1 AND userid = $2";
+ if (filter != null) {
+ query = query + " AND action = $3";
+ dataarray.push(filter);
+ }
+ query = query + ") ORDER BY time DESC";
+ } else {
+ query = query + 'JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target IN (' + dbMergeSqlArray(ids) + '))';
+ if (filter != null) {
+ query = query + " AND action = $3";
+ dataarray.push(filter);
+ }
+ query = query + ") GROUP BY id ORDER BY time DESC";
+ }
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
+ var query = "SELECT doc FROM events ";
+ var dataarray = [domain, userid];
+ if (ids.indexOf('*') >= 0) {
+ query = query + "WHERE (domain = $1 AND userid = $2";
+ if (filter != null) {
+ query = query + " AND action = $3) ORDER BY time DESC LIMIT $4";
+ dataarray.push(filter);
+ } else {
+ query = query + ") ORDER BY time DESC LIMIT $3";
+ }
+ } else {
+ query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target IN (" + dbMergeSqlArray(ids) + "))";
+ if (filter != null) {
+ query = query + " AND action = $3) GROUP BY id ORDER BY time DESC LIMIT $4";
+ dataarray.push(filter);
+ } else {
+ query = query + ") GROUP BY id ORDER BY time DESC LIMIT $3";
+ }
+ }
+ dataarray.push(limit);
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
+ if (ids.indexOf('*') >= 0) {
+ sqlDbQuery('SELECT doc FROM events WHERE ((domain = $1) AND (time BETWEEN $2 AND $3)) ORDER BY time', [domain, start, end], func);
+ } else {
+ sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE ((domain = $1) AND (target IN (' + dbMergeSqlArray(ids) + ')) AND (time BETWEEN $2 AND $3)) GROUP BY id ORDER BY time', [domain, start, end], func);
+ }
+ };
+ //obj.GetUserLoginEvents = function (domain, userid, func) { } // TODO
+ obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
+ var query = "SELECT doc FROM events WHERE (nodeid = $1 AND domain = $2";
+ var dataarray = [nodeid, domain];
+ if (filter != null) {
+ query = query + " AND action = $3) ORDER BY time DESC LIMIT $4";
+ dataarray.push(filter);
+ } else {
+ query = query + ") ORDER BY time DESC LIMIT $3";
+ }
+ dataarray.push(limit);
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
+ var query = "SELECT doc FROM events WHERE (nodeid = $1) AND (domain = $2) AND ((userid = $3) OR (userid IS NULL)) ";
+ var dataarray = [nodeid, domain, userid];
+ if (filter != null) {
+ query = query + "AND (action = $4) ORDER BY time DESC LIMIT $5";
+ dataarray.push(filter);
+ } else {
+ query = query + "ORDER BY time DESC LIMIT $4";
+ }
+ dataarray.push(limit);
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.RemoveAllEvents = function (domain) { sqlDbQuery('DELETE FROM events', null, function (err, docs) { }); };
+ obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = $1 AND nodeid = $2', [domain, nodeid], function (err, docs) { }); };
+ obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = $1 AND userid = $2', [domain, userid], function (err, docs) { }); };
+ obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) { sqlDbQuery('SELECT COUNT(*) FROM events WHERE action = \'authfail\' AND domain = $1 AND userid = $2 AND time > $3', [domainid, userid, lastlogin], function (err, response) { func(err == null ? response[0]['COUNT(*)'] : 0); }); }
+
+ // Database actions on the power collection
+ obj.getAllPower = function (func) { sqlDbQuery('SELECT doc FROM power', null, func); };
+ obj.storePowerEvent = function (event, multiServer, func) { obj.dbCounters.powerSet++; if (multiServer != null) { event.server = multiServer.serverid; } sqlDbQuery('INSERT INTO power VALUES (NULL, $1, $2, $3)', [event.time, event.nodeid ? event.nodeid : null, JSON.stringify(event)], func); };
+ obj.getPowerTimeline = function (nodeid, func) { sqlDbQuery('SELECT doc FROM power WHERE ((nodeid = $1) OR (nodeid = \'*\')) ORDER BY time ASC', [nodeid], func); };
+ obj.removeAllPowerEvents = function () { sqlDbQuery('DELETE FROM power', null, function (err, docs) { }); };
+ obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; sqlDbQuery('DELETE FROM power WHERE nodeid = $1', [nodeid], function (err, docs) { }); };
+
+ // Database actions on the SMBIOS collection
+ obj.GetAllSMBIOS = function (func) { sqlDbQuery('SELECT doc FROM smbios', null, func); };
+ obj.SetSMBIOS = function (smbios, func) { var expire = new Date(smbios.time); expire.setMonth(expire.getMonth() + 6); sqlDbQuery('INSERT INTO smbios VALUES ($1, $2, $3, $4) ON CONFLICT (id) DO UPDATE SET time = $2, expire = $3, doc = $4', [smbios._id, smbios.time, expire, JSON.stringify(smbios)], func); };
+ obj.RemoveSMBIOS = function (id) { sqlDbQuery('DELETE FROM smbios WHERE id = $1', [id], function (err, docs) { }); };
+ obj.GetSMBIOS = function (id, func) { sqlDbQuery('SELECT doc FROM smbios WHERE id = $1', [id], func); };
+
+ // Database actions on the Server Stats collection
+ obj.SetServerStats = function (data, func) { sqlDbQuery('INSERT INTO serverstats VALUES ($1, $2, $3) ON CONFLICT (time) DO UPDATE SET expire = $2, doc = $3', [data.time, data.expire, JSON.stringify(data)], func); };
+ obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); sqlDbQuery('SELECT doc FROM serverstats WHERE time > $1', [t], func); }; // TODO: Expire old entries
+
+ // Read a configuration file from the database
+ obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
+
+ // Write a configuration file to the database
+ obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
+
+ // List all configuration files
+ obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM main WHERE type = "cfile" ORDER BY id', func); }
+
+ // Get database information (TODO: Complete this)
+ obj.getDbStats = function (func) {
+ obj.stats = { c: 4 };
+ sqlDbQuery('SELECT COUNT(*) FROM main', null, function (err, response) { obj.stats.meshcentral = (err == null ? response[0]['COUNT(*)'] : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ sqlDbQuery('SELECT COUNT(*) FROM serverstats', null, function (err, response) { obj.stats.serverstats = (err == null ? response[0]['COUNT(*)'] : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ sqlDbQuery('SELECT COUNT(*) FROM power', null, function (err, response) { obj.stats.power = (err == null ? response[0]['COUNT(*)'] : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ sqlDbQuery('SELECT COUNT(*) FROM smbios', null, function (err, response) { obj.stats.smbios = (err == null ? response[0]['COUNT(*)'] : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ }
+
+ // Plugin operations
+ if (obj.pluginsActive) {
+ obj.addPlugin = function (plugin, func) { sqlDbQuery('INSERT INTO plugin VALUES (NULL, $1)', [JSON.stringify(plugin)], func); }; // Add a plugin
+ obj.getPlugins = function (func) { sqlDbQuery('SELECT JSON_INSERT(doc, "$._id", id) as doc FROM plugin', null, func); }; // Get all plugins
+ obj.getPlugin = function (id, func) { sqlDbQuery('SELECT JSON_INSERT(doc, "$._id", id) as doc FROM plugin WHERE id = $1', [id], func); }; // Get plugin
+ obj.deletePlugin = function (id, func) { sqlDbQuery('DELETE FROM plugin WHERE id = $1', [id], func); }; // Delete plugin
+ obj.setPluginStatus = function (id, status, func) { sqlDbQuery('UPDATE plugin SET doc=JSON_SET(doc,"$.status",$1) WHERE id=$2', [status,id], func); };
+ obj.updatePlugin = function (id, args, func) { delete args._id; sqlDbQuery('UPDATE plugin SET doc=json_patch(doc,$1) WHERE id=$2', [JSON.stringify(args),id], func); };
+ }
+ } else if (obj.databaseType == DB_ACEBASE) {
+ // Database actions on the main collection. AceBase: https://github.com/appy-one/acebase
+ obj.Set = function (data, func) {
+ data = common.escapeLinksFieldNameEx(data);
+ var xdata = performTypedRecordEncrypt(data);
+ obj.dbCounters.fileSet++;
+ obj.file.ref('meshcentral').child(encodeURIComponent(xdata._id)).set(common.aceEscapeFieldNames(xdata)).then(function (ref) { if (func) { func(); } })
+ };
+ obj.Get = function (id, func) {
+ obj.file.ref('meshcentral').child(encodeURIComponent(id)).get(function (snapshot) {
+ if (snapshot.exists()) { func(null, performTypedRecordDecrypt([common.aceUnEscapeFieldNames(snapshot.val())])); } else { func(null, []); }
+ });
+ };
+ obj.GetAll = function (func) {
+ obj.file.ref('meshcentral').get(function(snapshot) {
+ const val = snapshot.val();
+ const docs = Object.keys(val).map(function(key) { return val[key]; });
+ func(null, common.aceUnEscapeAllFieldNames(docs));
+ });
+ };
+ obj.GetHash = function (id, func) {
+ obj.file.ref('meshcentral').child(encodeURIComponent(id)).get({ include: ['hash'] }, function (snapshot) {
+ if (snapshot.exists()) { func(null, snapshot.val()); } else { func(null, null); }
+ });
+ };
+ obj.GetAllTypeNoTypeField = function (type, domain, func) {
+ obj.file.query('meshcentral').filter('type', '==', type).filter('domain', '==', domain).get({ exclude: ['type'] }, function (snapshots) {
+ const docs = [];
+ for (var i in snapshots) { const x = snapshots[i].val(); docs.push(x); }
+ func(null, common.aceUnEscapeAllFieldNames(docs));
+ });
+ }
+ obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
+ if (meshes.length == 0) { func(null, []); return; }
+ var query = obj.file.query('meshcentral').skip(skip).take(limit).filter('type', '==', type).filter('domain', '==', domain);
+ if (id) { query = query.filter('_id', '==', id); }
+ if (extrasids == null) {
+ query = query.filter('meshid', 'in', meshes);
+ query.get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); });
+ } else {
+ // TODO: This is a slow query as we did not find a filter-or-filter, so we query everything and filter manualy.
+ query.get(function (snapshots) {
+ const docs = [];
+ for (var i in snapshots) { const x = snapshots[i].val(); if ((extrasids.indexOf(x._id) >= 0) || (meshes.indexOf(x.meshid) >= 0)) { docs.push(x); } }
+ func(null, performTypedRecordDecrypt(docs));
+ });
+ }
+ };
+ obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
+ var query = obj.file.query('meshcentral').filter('type', '==', type).filter('domain', '==', domain).filter('nodeid', 'in', nodes);
+ if (id) { query = query.filter('_id', '==', id); }
+ query.get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); });
+ };
+ obj.GetAllType = function (type, func) {
+ obj.file.query('meshcentral').filter('type', '==', type).get(function (snapshots) {
+ const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); }
+ func(null, common.aceUnEscapeAllFieldNames(performTypedRecordDecrypt(docs)));
+ });
+ };
+ obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.query('meshcentral').filter('_id', 'in', ids).filter('domain', '==', domain).filter('type', '==', type).get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
+ obj.GetUserWithEmail = function (domain, email, func) { obj.file.query('meshcentral').filter('type', '==', 'user').filter('domain', '==', domain).filter('email', '==', email).get({ exclude: ['type'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
+ obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.query('meshcentral').filter('type', '==', 'user').filter('domain', '==', domain).filter('email', '==', email).filter('emailVerified', '==', true).get({ exclude: ['type'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
+ obj.GetNodeByComputerName = function (domain, rname, func) { obj.file.query('meshcentral').filter('type', '==', 'node').filter('domain', '==', domain).filter('rname', '==', rname).get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
+ obj.Remove = function (id, func) { obj.file.ref('meshcentral').child(encodeURIComponent(id)).remove().then(function () { if (func) { func(); } }); };
+ obj.RemoveAll = function (func) { obj.file.query('meshcentral').remove().then(function () { if (func) { func(); } }); };
+ obj.RemoveAllOfType = function (type, func) { obj.file.query('meshcentral').filter('type', '==', type).remove().then(function () { if (func) { func(); } }); };
+ obj.InsertMany = function (data, func) { var r = {}; for (var i in data) { const ref = obj.file.ref('meshcentral').child(encodeURIComponent(data[i]._id)); r[ref.key] = common.aceEscapeFieldNames(data[i]); } obj.file.ref('meshcentral').set(r).then(function (ref) { func(); }); }; // Insert records directly, no link escaping
+ obj.RemoveMeshDocuments = function (id) { obj.file.query('meshcentral').filter('meshid', '==', id).remove(); obj.file.ref('meshcentral').child(encodeURIComponent('nt' + id)).remove(); };
+ obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
+ obj.DeleteDomain = function (domain, func) { obj.file.query('meshcentral').filter('domain', '==', domain).remove().then(function () { if (func) { func(); } }); };
+ obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
+ obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
+ obj.getLocalAmtNodes = function (func) { obj.file.query('meshcentral').filter('type', '==', 'node').filter('host', 'exists').filter('host', '!=', null).filter('intelamt', 'exists').get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
+ obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { obj.file.query('meshcentral').filter('type', '==', 'node').filter('domain', '==', domainid).filter('mtype', '!=', mtype).filter('intelamt.uuid', '==', uuid).get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
+ obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { obj.file.query('meshcentral').filter('type', '==', type).filter('domain', '==', domainid).get({ snapshots: false }, function (snapshots) { func((snapshots.length > max), snapshots.length); }); } }
+
+ // Database actions on the events collection
+ obj.GetAllEvents = function (func) {
+ obj.file.ref('events').get(function (snapshot) {
+ const val = snapshot.val();
+ const docs = Object.keys(val).map(function(key) { return val[key]; });
+ func(null, docs);
+ })
+ };
+ obj.StoreEvent = function (event, func) {
+ if (typeof event.account == 'object') { event = Object.assign({}, event); event.account = common.aceEscapeFieldNames(event.account); }
+ obj.dbCounters.eventsSet++;
+ obj.file.ref('events').push(event).then(function (userRef) { if (func) { func(); } });
+ };
+ obj.GetEvents = function (ids, domain, filter, func) {
+ // This request is slow since we have not found a .filter() that will take two arrays and match a single item.
+ if (filter != null) {
+ obj.file.query('events').filter('domain', '==', domain).filter('action', '==', filter).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) {
+ const docs = [];
+ for (var i in snapshots) {
+ const doc = snapshots[i].val();
+ if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue;
+ var found = false;
+ for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches
+ if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); }
+ }
+ func(null, docs);
+ });
+ } else {
+ obj.file.query('events').filter('domain', '==', domain).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) {
+ const docs = [];
+ for (var i in snapshots) {
+ const doc = snapshots[i].val();
+ if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue;
+ var found = false;
+ for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches
+ if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); }
+ }
+ func(null, docs);
+ });
+ }
+ };
+ obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
+ // This request is slow since we have not found a .filter() that will take two arrays and match a single item.
+ // TODO: Request a new AceBase feature for a 'array:contains-one-of' filter:
+ // obj.file.indexes.create('events', 'ids', { type: 'array' });
+ // db.query('events').filter('ids', 'array:contains-one-of', ids)
+ if (filter != null) {
+ obj.file.query('events').filter('domain', '==', domain).filter('action', '==', filter).take(limit).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) {
+ const docs = [];
+ for (var i in snapshots) {
+ const doc = snapshots[i].val();
+ if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue;
+ var found = false;
+ for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches
+ if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); }
+ }
+ func(null, docs);
+ });
+ } else {
+ obj.file.query('events').filter('domain', '==', domain).take(limit).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) {
+ const docs = [];
+ for (var i in snapshots) {
+ const doc = snapshots[i].val();
+ if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue;
+ var found = false;
+ for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches
+ if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); }
+ }
+ func(null, docs);
+ });
+ }
+ };
+ obj.GetUserEvents = function (ids, domain, userid, filter, func) {
+ if (filter != null) {
+ obj.file.query('events').filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).filter('action', '==', filter).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
+ } else {
+ obj.file.query('events').filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
+ }
+ };
+ obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
+ if (filter != null) {
+ obj.file.query('events').take(limit).filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).filter('action', '==', filter).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
+ } else {
+ obj.file.query('events').take(limit).filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
+ }
+ };
+ obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
+ obj.file.query('events').filter('domain', '==', domain).filter('ids', 'in', ids).filter('msgid', 'in', msgids).filter('time', 'between', [start, end]).sort('time', false).get({ exclude: ['type', '_id', 'domain', 'node'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
+ };
+ obj.GetUserLoginEvents = function (domain, userid, func) {
+ obj.file.query('events').filter('domain', '==', domain).filter('action', 'in', ['authfail', 'login']).filter('userid', '==', userid).filter('msgArgs', 'exists').sort('time', false).get({ include: ['action', 'time', 'msgid', 'msgArgs', 'tokenName'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
+ };
+ obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
+ if (filter != null) {
+ obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).filter('action', '==', filter).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
+ } else {
+ obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
+ }
+ };
+ obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
+ if (filter != null) {
+ obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).filter('userid', '==', userid).filter('action', '==', filter).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
+ } else {
+ obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).filter('userid', '==', userid).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
+ }
+ obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).filter('userid', '==', userid).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
+ };
+ obj.RemoveAllEvents = function (domain) {
+ obj.file.query('events').filter('domain', '==', domain).remove().then(function () { if (func) { func(); } });;
+ };
+ obj.RemoveAllNodeEvents = function (domain, nodeid) {
+ if ((domain == null) || (nodeid == null)) return;
+ obj.file.query('events').filter('domain', '==', domain).filter('nodeid', '==', nodeid).remove().then(function () { if (func) { func(); } });;
+ };
+ obj.RemoveAllUserEvents = function (domain, userid) {
+ if ((domain == null) || (userid == null)) return;
+ obj.file.query('events').filter('domain', '==', domain).filter('userid', '==', userid).remove().then(function () { if (func) { func(); } });;
+ };
+ obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) {
+ obj.file.query('events').filter('domain', '==', domainid).filter('userid', '==', userid).filter('time', '>', lastlogin).sort('time', false).get({ snapshots: false }, function (snapshots) { func(null, snapshots.length); });
+ }
+
+ // Database actions on the power collection
+ obj.getAllPower = function (func) {
+ obj.file.ref('power').get(function (snapshot) {
+ const val = snapshot.val();
+ const docs = Object.keys(val).map(function(key) { return val[key]; });
+ func(null, docs);
+ });
+ };
+ obj.storePowerEvent = function (event, multiServer, func) {
+ if (multiServer != null) { event.server = multiServer.serverid; }
+ obj.file.ref('power').push(event).then(function (userRef) { if (func) { func(); } });
+ };
+ obj.getPowerTimeline = function (nodeid, func) {
+ obj.file.query('power').filter('nodeid', 'in', ['*', nodeid]).sort('time').get({ exclude: ['_id', 'nodeid', 's'] }, function (snapshots) {
+ const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs);
+ });
+ };
+ obj.removeAllPowerEvents = function () {
+ obj.file.ref('power').remove().then(function () { if (func) { func(); } });
+ };
+ obj.removeAllPowerEventsForNode = function (nodeid) {
+ if (nodeid == null) return;
+ obj.file.query('power').filter('nodeid', '==', nodeid).remove().then(function () { if (func) { func(); } });
+ };
+
+ // Database actions on the SMBIOS collection
+ if (obj.smbiosfile != null) {
+ obj.GetAllSMBIOS = function (func) {
+ obj.file.ref('smbios').get(function (snapshot) {
+ const val = snapshot.val();
+ const docs = Object.keys(val).map(function(key) { return val[key]; });
+ func(null, docs);
+ });
+ };
+ obj.SetSMBIOS = function (smbios, func) {
+ obj.file.ref('meshcentral/' + encodeURIComponent(smbios._id)).set(smbios).then(function (ref) { if (func) { func(); } })
+ };
+ obj.RemoveSMBIOS = function (id) {
+ obj.file.query('smbios').filter('_id', '==', id).remove().then(function () { if (func) { func(); } });
+ };
+ obj.GetSMBIOS = function (id, func) {
+ obj.file.query('smbios').filter('_id', '==', id).get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
+ };
+ }
+
+ // Database actions on the Server Stats collection
+ obj.SetServerStats = function (data, func) {
+ obj.file.ref('stats').push(data).then(function (userRef) { if (func) { func(); } });
+ };
+ obj.GetServerStats = function (hours, func) {
+ var t = new Date();
+ t.setTime(t.getTime() - (60 * 60 * 1000 * hours));
+ obj.file.query('stats').filter('time', '>', t).get({ exclude: ['_id', 'cpu'] }, function (snapshots) {
+ const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs);
+ });
+ };
+
+ // Read a configuration file from the database
+ obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
+
+ // Write a configuration file to the database
+ obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
+
+ // List all configuration files
+ obj.listConfigFiles = function (func) {
+ obj.file.query('meshcentral').filter('type', '==', 'cfile').sort('_id').get(function (snapshots) {
+ const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs);
+ });
+ }
+
+ // Get database information
+ obj.getDbStats = function (func) {
+ obj.stats = { c: 5 };
+ obj.file.ref('meshcentral').count().then(function (count) { obj.stats.meshcentral = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ obj.file.ref('events').count().then(function (count) { obj.stats.events = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ obj.file.ref('power').count().then(function (count) { obj.stats.power = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ obj.file.ref('smbios').count().then(function (count) { obj.stats.smbios = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ obj.file.ref('stats').count().then(function (count) { obj.stats.serverstats = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ }
+
+ // Plugin operations
+ if (obj.pluginsActive) {
+ obj.addPlugin = function (plugin, func) { plugin.type = 'plugin'; obj.file.ref('plugin').child(encodeURIComponent(plugin._id)).set(plugin).then(function (ref) { if (func) { func(); } }) }; // Add a plugin
+ obj.getPlugins = function (func) {
+ obj.file.ref('plugin').get({ exclude: ['type'] }, function (snapshot) {
+ const val = snapshot.val();
+ const docs = Object.keys(val).map(function(key) { return val[key]; }).sort(function(a, b) { return a.name < b.name ? -1 : 1 });
+ func(null, docs);
+ });
+ }; // Get all plugins
+ obj.getPlugin = function (id, func) { obj.file.query('plugin').filter('_id', '==', id).get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); }); }; // Get plugin
+ obj.deletePlugin = function (id, func) { obj.file.ref('plugin').child(encodeURIComponent(id)).remove().then(function () { if (func) { func(); } }); }; // Delete plugin
+ obj.setPluginStatus = function (id, status, func) { obj.file.ref('plugin').child(encodeURIComponent(id)).update({ status: status }).then(function (ref) { if (func) { func(); } }) };
+ obj.updatePlugin = function (id, args, func) { delete args._id; obj.file.ref('plugin').child(encodeURIComponent(id)).set(args).then(function (ref) { if (func) { func(); } }) };
+ }
+ } else if (obj.databaseType == DB_POSTGRESQL) {
+ // Database actions on the main collection (Postgres)
+ obj.Set = function (value, func) {
+ obj.dbCounters.fileSet++;
+ var extra = null, extraex = null;
+ value = common.escapeLinksFieldNameEx(value);
+ if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
+ if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
+ if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
+ sqlDbQuery('INSERT INTO main VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET type = $2, domain = $3, extra = $4, extraex = $5, doc = $6;', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, performTypedRecordEncrypt(value)], func);
+ }
+ obj.SetRaw = function (value, func) {
+ obj.dbCounters.fileSet++;
+ var extra = null, extraex = null;
+ if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
+ if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
+ if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
+ sqlDbQuery('INSERT INTO main VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET type = $2, domain = $3, extra = $4, extraex = $5, doc = $6;', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, performTypedRecordEncrypt(value)], func);
+ }
+ obj.Get = function (_id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = $1', [_id], function (err, docs) { if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); } func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetAll = function (func) { sqlDbQuery('SELECT domain, doc FROM main', null, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetHash = function (id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = $1', [id], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetAllTypeNoTypeField = function (type, domain, func) { sqlDbQuery('SELECT doc FROM main WHERE type = $1 AND domain = $2', [type, domain], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
+ if (limit == 0) { limit = 0xFFFFFFFF; }
+ if (id && (id != '')) {
+ sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra = ANY ($4)) LIMIT $5 OFFSET $6', [id, type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
+ } else {
+ if (extrasids == null) {
+ sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3)) LIMIT $4 OFFSET $5', [type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }, true);
+ } else {
+ sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra = ANY ($3)) OR (id = ANY ($4))) LIMIT $5 OFFSET $6', [type, domain, meshes, extrasids, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
+ }
+ }
+ };
+ obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
+ if (id && (id != '')) {
+ sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra = ANY ($4))', [id, type, domain, meshes], function (err, docs) { func(err, docs); });
+ } else {
+ if (extrasids == null) {
+ sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3))', [type, domain, meshes], function (err, docs) { func(err, docs); }, true);
+ } else {
+ sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (type = $1) AND (domain = $2) AND ((extra = ANY ($3)) OR (id = ANY ($4)))', [type, domain, meshes, extrasids], function (err, docs) { func(err, docs); });
+ }
+ }
+ };
+ obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
+ if (id && (id != '')) {
+ sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra = ANY ($4))', [id, type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
+ } else {
+ sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3))', [type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
+ }
+ };
+ obj.GetAllType = function (type, func) { sqlDbQuery('SELECT doc FROM main WHERE type = $1', [type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetAllIdsOfType = function (ids, domain, type, func) { sqlDbQuery('SELECT doc FROM main WHERE (id = ANY ($1)) AND domain = $2 AND type = $3', [ids, domain, type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetUserWithEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extra = $2', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetUserWithVerifiedEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extra = $2', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetNodeByComputerName = function (domain, rname, func) { sqlDbQuery('SELECT doc FROM main WHERE type = "$1" AND domain = $2 AND doc->>\'rname\' = $3 ORDER BY lastbootuptime', ['node', domain, rname], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });};
+ obj.Remove = function (id, func) { sqlDbQuery('DELETE FROM main WHERE id = $1', [id], func); };
+ obj.RemoveAll = function (func) { sqlDbQuery('DELETE FROM main', null, func); };
+ obj.RemoveAllOfType = function (type, func) { sqlDbQuery('DELETE FROM main WHERE type = $1', [type], func); };
+ obj.InsertMany = function (data, func) { var pendingOps = 0; for (var i in data) { pendingOps++; obj.SetRaw(data[i], function () { if (--pendingOps == 0) { func(); } }); } }; // Insert records directly, no link escaping
+ obj.RemoveMeshDocuments = function (id, func) { sqlDbQuery('DELETE FROM main WHERE extra = $1', [id], function () { sqlDbQuery('DELETE FROM main WHERE id = $1', ['nt' + id], func); }); };
+ obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
+ obj.DeleteDomain = function (domain, func) { sqlDbQuery('DELETE FROM main WHERE domain = $1', [domain], func); };
+ obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
+ obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
+ obj.getLocalAmtNodes = function (func) { sqlDbQuery('SELECT doc FROM main WHERE (type = \'node\') AND (extraex IS NULL)', null, function (err, docs) { var r = []; if (err == null) { for (var i in docs) { if (docs[i].host != null && docs[i].intelamt != null) { r.push(docs[i]); } } } func(err, r); }); };
+ obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extraex = $2', [domainid, 'uuid/' + uuid], func); };
+ obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { sqlDbExec('SELECT COUNT(id) FROM main WHERE domain = $1 AND type = $2', [domainid, type], function (err, response) { func((response['COUNT(id)'] == null) || (response['COUNT(id)'] > max), response['COUNT(id)']) }); } }
+
+ // Database actions on the events collection
+ obj.GetAllEvents = function (func) { sqlDbQuery('SELECT doc FROM events', null, func); };
+ obj.StoreEvent = function (event, func) {
+ obj.dbCounters.eventsSet++;
+ sqlDbQuery('INSERT INTO events VALUES (DEFAULT, $1, $2, $3, $4, $5, $6) RETURNING id', [event.time, ((typeof event.domain == 'string') ? event.domain : null), event.action, event.nodeid ? event.nodeid : null, event.userid ? event.userid : null, event], function (err, docs) {
+ if(func){ func(); }
+ if (docs.id) {
+ for (var i in event.ids) {
+ if (event.ids[i] != '*') {
+ obj.pendingTransfer++;
+ sqlDbQuery('INSERT INTO eventids VALUES ($1, $2)', [docs.id, event.ids[i]], function(){ if(func){ func(); } });
+ }
+ }
+ }
+ });
+ };
+ obj.GetEvents = function (ids, domain, filter, func) {
+ var query = "SELECT doc FROM events ";
+ var dataarray = [domain];
+ if (ids.indexOf('*') >= 0) {
+ query = query + "WHERE (domain = $1";
+ if (filter != null) {
+ query = query + " AND action = $2";
+ dataarray.push(filter);
+ }
+ query = query + ") ORDER BY time DESC";
+ } else {
+ query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND (target = ANY ($2))";
+ dataarray.push(ids);
+ if (filter != null) {
+ query = query + " AND action = $3";
+ dataarray.push(filter);
+ }
+ query = query + ") GROUP BY id ORDER BY time DESC";
+ }
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
+ var query = "SELECT doc FROM events ";
+ var dataarray = [domain];
+ if (ids.indexOf('*') >= 0) {
+ query = query + "WHERE (domain = $1";
+ if (filter != null) {
+ query = query + " AND action = $2) ORDER BY time DESC LIMIT $3";
+ dataarray.push(filter);
+ } else {
+ query = query + ") ORDER BY time DESC LIMIT $2";
+ }
+ } else {
+ if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND (target = ANY ($2))";
+ dataarray.push(ids);
+ if (filter != null) {
+ query = query + " AND action = $3) ORDER BY time DESC LIMIT $4";
+ dataarray.push(filter);
+ } else {
+ query = query + ") ORDER BY time DESC LIMIT $3";
+ }
+ }
+ dataarray.push(limit);
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetUserEvents = function (ids, domain, userid, filter, func) {
+ var query = "SELECT doc FROM events ";
+ var dataarray = [domain, userid];
+ if (ids.indexOf('*') >= 0) {
+ query = query + "WHERE (domain = $1 AND userid = $2";
+ if (filter != null) {
+ query = query + " AND action = $3";
+ dataarray.push(filter);
+ }
+ query = query + ") ORDER BY time DESC";
+ } else {
+ if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target = ANY ($3))";
+ dataarray.push(ids);
+ if (filter != null) {
+ query = query + " AND action = $4";
+ dataarray.push(filter);
+ }
+ query = query + ") GROUP BY id ORDER BY time DESC";
+ }
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
+ var query = "SELECT doc FROM events ";
+ var dataarray = [domain, userid];
+ if (ids.indexOf('*') >= 0) {
+ query = query + "WHERE (domain = $1 AND userid = $2";
+ if (filter != null) {
+ query = query + " AND action = $3) ORDER BY time DESC LIMIT $4 ";
+ dataarray.push(filter);
+ } else {
+ query = query + ") ORDER BY time DESC LIMIT $3";
+ }
+ } else {
+ if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target = ANY ($3))";
+ dataarray.push(ids);
+ if (filter != null) {
+ query = query + " AND action = $4) GROUP BY id ORDER BY time DESC LIMIT $5";
+ dataarray.push(filter);
+ } else {
+ query = query + ") GROUP BY id ORDER BY time DESC LIMIT $4";
+ }
+ }
+ dataarray.push(limit);
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
+ if (ids.indexOf('*') >= 0) {
+ sqlDbQuery('SELECT doc FROM events WHERE ((domain = $1) AND (time BETWEEN $2 AND $3)) ORDER BY time', [domain, start, end], func);
+ } else {
+ sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE ((domain = $1) AND (target = ANY ($2)) AND (time BETWEEN $3 AND $4)) GROUP BY id ORDER BY time', [domain, ids, start, end], func);
+ }
+ };
+ //obj.GetUserLoginEvents = function (domain, userid, func) { } // TODO
+ obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
+ var query = "SELECT doc FROM events WHERE (nodeid = $1 AND domain = $2";
+ var dataarray = [nodeid, domain];
+ if (filter != null) {
+ query = query + " AND action = $3) ORDER BY time DESC LIMIT $4";
+ dataarray.push(filter);
+ } else {
+ query = query + ") ORDER BY time DESC LIMIT $3";
+ }
+ dataarray.push(limit);
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
+ var query = "SELECT doc FROM events WHERE (nodeid = $1 AND domain = $2 AND ((userid = $3) OR (userid IS NULL))";
+ var dataarray = [nodeid, domain, userid];
+ if (filter != null) {
+ query = query + " AND action = $4) ORDER BY time DESC LIMIT $5";
+ dataarray.push(filter);
+ } else {
+ query = query + ") ORDER BY time DESC LIMIT $4";
+ }
+ dataarray.push(limit);
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.RemoveAllEvents = function (domain) { sqlDbQuery('DELETE FROM events', null, function (err, docs) { }); };
+ obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = $1 AND nodeid = $2', [domain, nodeid], function (err, docs) { }); };
+ obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = $1 AND userid = $2', [domain, userid], function (err, docs) { }); };
+ obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) { sqlDbQuery('SELECT COUNT(*) FROM events WHERE action = \'authfail\' AND domain = $1 AND userid = $2 AND time > $3', [domainid, userid, lastlogin], function (err, response, raw) { func(err == null ? parseInt(raw.rows[0].count) : 0); }); }
+
+ // Database actions on the power collection
+ obj.getAllPower = function (func) { sqlDbQuery('SELECT doc FROM power', null, func); };
+ obj.storePowerEvent = function (event, multiServer, func) { obj.dbCounters.powerSet++; if (multiServer != null) { event.server = multiServer.serverid; } sqlDbQuery('INSERT INTO power VALUES (DEFAULT, $1, $2, $3)', [event.time, event.nodeid ? event.nodeid : null, event], func); };
+ obj.getPowerTimeline = function (nodeid, func) { sqlDbQuery('SELECT doc FROM power WHERE ((nodeid = $1) OR (nodeid = \'*\')) ORDER BY time ASC', [nodeid], func); };
+ obj.removeAllPowerEvents = function () { sqlDbQuery('DELETE FROM power', null, function (err, docs) { }); };
+ obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; sqlDbQuery('DELETE FROM power WHERE nodeid = $1', [nodeid], function (err, docs) { }); };
+
+ // Database actions on the SMBIOS collection
+ obj.GetAllSMBIOS = function (func) { sqlDbQuery('SELECT doc FROM smbios', null, func); };
+ obj.SetSMBIOS = function (smbios, func) { var expire = new Date(smbios.time); expire.setMonth(expire.getMonth() + 6); sqlDbQuery('INSERT INTO smbios VALUES ($1, $2, $3, $4) ON CONFLICT (id) DO UPDATE SET time = $2, expire = $3, doc = $4', [smbios._id, smbios.time, expire, smbios], func); };
+ obj.RemoveSMBIOS = function (id) { sqlDbQuery('DELETE FROM smbios WHERE id = $1', [id], function (err, docs) { }); };
+ obj.GetSMBIOS = function (id, func) { sqlDbQuery('SELECT doc FROM smbios WHERE id = $1', [id], func); };
+
+ // Database actions on the Server Stats collection
+ obj.SetServerStats = function (data, func) { sqlDbQuery('INSERT INTO serverstats VALUES ($1, $2, $3) ON CONFLICT (time) DO UPDATE SET expire = $2, doc = $3', [data.time, data.expire, data], func); };
+ obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); sqlDbQuery('SELECT doc FROM serverstats WHERE time > $1', [t], func); }; // TODO: Expire old entries
+
+ // Read a configuration file from the database
+ obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
+
+ // Write a configuration file to the database
+ obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
+
+ // List all configuration files
+ obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM main WHERE type = "cfile" ORDER BY id', func); }
+
+ // Get database information (TODO: Complete this)
+ obj.getDbStats = function (func) {
+ obj.stats = { c: 4 };
+ sqlDbQuery('SELECT COUNT(*) FROM main', null, function (err, response, raw) { obj.stats.meshcentral = (err == null ? parseInt(raw.rows[0].count) : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ sqlDbQuery('SELECT COUNT(*) FROM serverstats', null, function (err, response, raw) { obj.stats.serverstats = (err == null ? parseInt(raw.rows[0].count) : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ sqlDbQuery('SELECT COUNT(*) FROM power', null, function (err, response, raw) { obj.stats.power = (err == null ? parseInt(raw.rows[0].count) : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ sqlDbQuery('SELECT COUNT(*) FROM smbios', null, function (err, response, raw) { obj.stats.smbios = (err == null ? parseInt(raw.rows[0].count) : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ }
+
+ // Plugin operations
+ if (obj.pluginsActive) {
+ obj.addPlugin = function (plugin, func) { sqlDbQuery('INSERT INTO plugin VALUES (DEFAULT, $1)', [plugin], func); }; // Add a plugin
+ obj.getPlugins = function (func) { sqlDbQuery("SELECT doc::jsonb || ('{\"_id\":' || plugin.id || '}')::jsonb as doc FROM plugin", null, func); }; // Get all plugins
+ obj.getPlugin = function (id, func) { sqlDbQuery("SELECT doc::jsonb || ('{\"_id\":' || plugin.id || '}')::jsonb as doc FROM plugin WHERE id = $1", [id], func); }; // Get plugin
+ obj.deletePlugin = function (id, func) { sqlDbQuery('DELETE FROM plugin WHERE id = $1', [id], func); }; // Delete plugin
+ obj.setPluginStatus = function (id, status, func) { sqlDbQuery("UPDATE plugin SET doc= jsonb_set(doc::jsonb,'{status}',$1) WHERE id=$2", [status,id], func); };
+ obj.updatePlugin = function (id, args, func) { delete args._id; sqlDbQuery('UPDATE plugin SET doc= doc::jsonb || ($1) WHERE id=$2', [args,id], func); };
+ }
+ } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
+ // Database actions on the main collection (MariaDB or MySQL)
+ obj.Set = function (value, func) {
+ obj.dbCounters.fileSet++;
+ var extra = null, extraex = null;
+ value = common.escapeLinksFieldNameEx(value);
+ if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
+ if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
+ if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
+ sqlDbQuery('REPLACE INTO main VALUE (?, ?, ?, ?, ?, ?)', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, JSON.stringify(performTypedRecordEncrypt(value))], func);
+ }
+ obj.SetRaw = function (value, func) {
+ obj.dbCounters.fileSet++;
+ var extra = null, extraex = null;
+ if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
+ if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
+ if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
+ sqlDbQuery('REPLACE INTO main VALUE (?, ?, ?, ?, ?, ?)', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, JSON.stringify(performTypedRecordEncrypt(value))], func);
+ }
+ obj.Get = function (_id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = ?', [_id], function (err, docs) { if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); } func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetAll = function (func) { sqlDbQuery('SELECT domain, doc FROM main', null, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetHash = function (id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = ?', [id], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetAllTypeNoTypeField = function (type, domain, func) { sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ?', [type, domain], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
+ if (limit == 0) { limit = 0xFFFFFFFF; }
+ if ((meshes == null) || (meshes.length == 0)) { meshes = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ if ((extrasids == null) || (extrasids.length == 0)) { extrasids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ if (id && (id != '')) {
+ sqlDbQuery('SELECT doc FROM main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?) LIMIT ? OFFSET ?', [id, type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
+ } else {
+ sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND (extra IN (?) OR id IN (?)) LIMIT ? OFFSET ?', [type, domain, meshes, extrasids, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
+ }
+ };
+ obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
+ if ((meshes == null) || (meshes.length == 0)) { meshes = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ if ((extrasids == null) || (extrasids.length == 0)) { extrasids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ if (id && (id != '')) {
+ sqlDbQuery('SELECT COUNT(doc) FROM main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?)', [id, type, domain, meshes], function (err, docs) { func(err, docs); });
+ } else {
+ sqlDbQuery('SELECT COUNT(doc) FROM main WHERE type = ? AND domain = ? AND (extra IN (?) OR id IN (?))', [type, domain, meshes, extrasids], function (err, docs) { func(err, docs); });
+ }
+ };
+ obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
+ if ((nodes == null) || (nodes.length == 0)) { nodes = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ if (id && (id != '')) {
+ sqlDbQuery('SELECT doc FROM main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?)', [id, type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
+ } else {
+ sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND extra IN (?)', [type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
+ }
+ };
+ obj.GetAllType = function (type, func) { sqlDbQuery('SELECT doc FROM main WHERE type = ?', [type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetAllIdsOfType = function (ids, domain, type, func) {
+ if ((ids == null) || (ids.length == 0)) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ sqlDbQuery('SELECT doc FROM main WHERE id IN (?) AND domain = ? AND type = ?', [ids, domain, type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
+ }
+ obj.GetUserWithEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = ? AND extra = ?', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetUserWithVerifiedEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = ? AND extra = ?', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetNodeByComputerName = function (domain, rname, func) { sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND JSON_EXTRACT(doc, "$.rname") = ? ORDER BY lastbootuptime', ['node', domain, rname], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.Remove = function (id, func) { sqlDbQuery('DELETE FROM main WHERE id = ?', [id], func); };
+ obj.RemoveAll = function (func) { sqlDbQuery('DELETE FROM main', null, func); };
+ obj.RemoveAllOfType = function (type, func) { sqlDbQuery('DELETE FROM main WHERE type = ?', [type], func); };
+ obj.InsertMany = function (data, func) { var pendingOps = 0; for (var i in data) { pendingOps++; obj.SetRaw(data[i], function () { if (--pendingOps == 0) { func(); } }); } }; // Insert records directly, no link escaping
+ obj.RemoveMeshDocuments = function (id, func) { sqlDbQuery('DELETE FROM main WHERE extra = ?', [id], function () { sqlDbQuery('DELETE FROM main WHERE id = ?', ['nt' + id], func); } ); };
+ obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
+ obj.DeleteDomain = function (domain, func) { sqlDbQuery('DELETE FROM main WHERE domain = ?', [domain], func); };
+ obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
+ obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
+ obj.getLocalAmtNodes = function (func) { sqlDbQuery('SELECT doc FROM main WHERE (type = "node") AND (extraex IS NULL)', null, function (err, docs) { var r = []; if (err == null) { for (var i in docs) { if (docs[i].host != null && docs[i].intelamt != null) { r.push(docs[i]); } } } func(err, r); }); };
+ obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = ? AND extraex = ?', [domainid, 'uuid/' + uuid], func); };
+ obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { sqlDbExec('SELECT COUNT(id) FROM main WHERE domain = ? AND type = ?', [domainid, type], function (err, response) { func((response['COUNT(id)'] == null) || (response['COUNT(id)'] > max), response['COUNT(id)']) }); } }
+
+ // Database actions on the events collection
+ obj.GetAllEvents = function (func) { sqlDbQuery('SELECT doc FROM events', null, func); };
+ obj.StoreEvent = function (event, func) {
+ obj.dbCounters.eventsSet++;
+ var batchQuery = [['INSERT INTO events VALUE (?, ?, ?, ?, ?, ?, ?)', [null, event.time, ((typeof event.domain == 'string') ? event.domain : null), event.action, event.nodeid ? event.nodeid : null, event.userid ? event.userid : null, JSON.stringify(event)]]];
+ for (var i in event.ids) { if (event.ids[i] != '*') { batchQuery.push(['INSERT INTO eventids VALUE (LAST_INSERT_ID(), ?)', [event.ids[i]]]); } }
+ sqlDbBatchExec(batchQuery, function (err, docs) { if (func != null) { func(err, docs); } });
+ };
+ obj.GetEvents = function (ids, domain, filter, func) {
+ var query = "SELECT doc FROM events ";
+ var dataarray = [domain];
+ if (ids.indexOf('*') >= 0) {
+ query = query + "WHERE (domain = ?";
+ if (filter != null) {
+ query = query + " AND action = ?";
+ dataarray.push(filter);
+ }
+ query = query + ") ORDER BY time DESC";
+ } else {
+ if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND target IN (?)";
+ dataarray.push(ids);
+ if (filter != null) {
+ query = query + " AND action = ?";
+ dataarray.push(filter);
+ }
+ query = query + ") GROUP BY id ORDER BY time DESC";
+ }
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
+ var query = "SELECT doc FROM events ";
+ var dataarray = [domain];
+ if (ids.indexOf('*') >= 0) {
+ query = query + "WHERE (domain = ?";
+ if (filter != null) {
+ query = query + " AND action = ? ";
+ dataarray.push(filter);
+ }
+ query = query + ") ORDER BY time DESC LIMIT ?";
+ } else {
+ if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND target IN (?)";
+ dataarray.push(ids);
+ if (filter != null) {
+ query = query + " AND action = ?";
+ dataarray.push(filter);
+ }
+ query = query + ") GROUP BY id ORDER BY time DESC LIMIT ?";
+ }
+ dataarray.push(limit);
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetUserEvents = function (ids, domain, userid, filter, func) {
+ var query = "SELECT doc FROM events ";
+ var dataarray = [domain, userid];
+ if (ids.indexOf('*') >= 0) {
+ query = query + "WHERE (domain = ? AND userid = ?";
+ if (filter != null) {
+ query = query + " AND action = ?";
+ dataarray.push(filter);
+ }
+ query = query + ") ORDER BY time DESC";
+ } else {
+ if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND userid = ? AND target IN (?)";
+ dataarray.push(ids);
+ if (filter != null) {
+ query = query + " AND action = ?";
+ dataarray.push(filter);
+ }
+ query = query + ") GROUP BY id ORDER BY time DESC";
+ }
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
+ var query = "SELECT doc FROM events ";
+ var dataarray = [domain, userid];
+ if (ids.indexOf('*') >= 0) {
+ query = query + "WHERE (domain = ? AND userid = ?";
+ if (filter != null) {
+ query = query + " AND action = ?";
+ dataarray.push(filter);
+ }
+ query = query + ") ORDER BY time DESC LIMIT ?";
+ } else {
+ if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND userid = ? AND target IN (?)";
+ dataarray.push(ids);
+ if (filter != null) {
+ query = query + " AND action = ?";
+ dataarray.push(filter);
+ }
+ query = query + ") GROUP BY id ORDER BY time DESC LIMIT ?";
+ }
+ dataarray.push(limit);
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
+ if (ids.indexOf('*') >= 0) {
+ sqlDbQuery('SELECT doc FROM events WHERE ((domain = ?) AND (time BETWEEN ? AND ?)) ORDER BY time', [domain, start, end], func);
+ } else {
+ if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE ((domain = ?) AND (target IN (?)) AND (time BETWEEN ? AND ?)) GROUP BY id ORDER BY time', [domain, ids, start, end], func);
+ }
+ };
+ //obj.GetUserLoginEvents = function (domain, userid, func) { } // TODO
+ obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
+ var query = "SELECT doc FROM events WHERE (nodeid = ? AND domain = ?";
+ var dataarray = [nodeid, domain];
+ if (filter != null) {
+ query = query + " AND action = ?) ORDER BY time DESC LIMIT ?";
+ dataarray.push(filter);
+ } else {
+ query = query + ") ORDER BY time DESC LIMIT ?";
+ }
+ dataarray.push(limit);
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
+ var query = "SELECT doc FROM events WHERE (nodeid = ? AND domain = ? AND ((userid = ?) OR (userid IS NULL))";
+ var dataarray = [nodeid, domain, userid];
+ if (filter != null) {
+ query = query + " AND action = ?) ORDER BY time DESC LIMIT ?";
+ dataarray.push(filter);
+ } else {
+ query = query + ") ORDER BY time DESC LIMIT ?";
+ }
+ dataarray.push(limit);
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.RemoveAllEvents = function (domain) { sqlDbQuery('DELETE FROM events', null, function (err, docs) { }); };
+ obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = ? AND nodeid = ?', [domain, nodeid], function (err, docs) { }); };
+ obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = ? AND userid = ?', [domain, userid], function (err, docs) { }); };
+ obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) { sqlDbExec('SELECT COUNT(id) FROM events WHERE action = "authfail" AND domain = ? AND userid = ? AND time > ?', [domainid, userid, lastlogin], function (err, response) { func(err == null ? response['COUNT(id)'] : 0); }); }
+
+ // Database actions on the power collection
+ obj.getAllPower = function (func) { sqlDbQuery('SELECT doc FROM power', null, func); };
+ obj.storePowerEvent = function (event, multiServer, func) { obj.dbCounters.powerSet++; if (multiServer != null) { event.server = multiServer.serverid; } sqlDbQuery('INSERT INTO power VALUE (?, ?, ?, ?)', [null, event.time, event.nodeid ? event.nodeid : null, JSON.stringify(event)], func); };
+ obj.getPowerTimeline = function (nodeid, func) { sqlDbQuery('SELECT doc FROM power WHERE ((nodeid = ?) OR (nodeid = "*")) ORDER BY time ASC', [nodeid], func); };
+ obj.removeAllPowerEvents = function () { sqlDbQuery('DELETE FROM power', null, function (err, docs) { }); };
+ obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; sqlDbQuery('DELETE FROM power WHERE nodeid = ?', [nodeid], function (err, docs) { }); };
+
+ // Database actions on the SMBIOS collection
+ obj.GetAllSMBIOS = function (func) { sqlDbQuery('SELECT doc FROM smbios', null, func); };
+ obj.SetSMBIOS = function (smbios, func) { var expire = new Date(smbios.time); expire.setMonth(expire.getMonth() + 6); sqlDbQuery('REPLACE INTO smbios VALUE (?, ?, ?, ?)', [smbios._id, smbios.time, expire, JSON.stringify(smbios)], func); };
+ obj.RemoveSMBIOS = function (id) { sqlDbQuery('DELETE FROM smbios WHERE id = ?', [id], function (err, docs) { }); };
+ obj.GetSMBIOS = function (id, func) { sqlDbQuery('SELECT doc FROM smbios WHERE id = ?', [id], func); };
+
+ // Database actions on the Server Stats collection
+ obj.SetServerStats = function (data, func) { sqlDbQuery('REPLACE INTO serverstats VALUE (?, ?, ?)', [data.time, data.expire, JSON.stringify(data)], func); };
+ obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); sqlDbQuery('SELECT doc FROM serverstats WHERE time > ?', [t], func); };
+
+ // Read a configuration file from the database
+ obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
+
+ // Write a configuration file to the database
+ obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
+
+ // List all configuration files
+ obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM main WHERE type = "cfile" ORDER BY id', func); }
+
+ // Get database information (TODO: Complete this)
+ obj.getDbStats = function (func) {
+ obj.stats = { c: 4 };
+ sqlDbExec('SELECT COUNT(id) FROM main', null, function (err, response) { obj.stats.meshcentral = Number(response['COUNT(id)']); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ sqlDbExec('SELECT COUNT(time) FROM serverstats', null, function (err, response) { obj.stats.serverstats = Number(response['COUNT(time)']); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ sqlDbExec('SELECT COUNT(id) FROM power', null, function (err, response) { obj.stats.power = Number(response['COUNT(id)']); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ sqlDbExec('SELECT COUNT(id) FROM smbios', null, function (err, response) { obj.stats.smbios = Number(response['COUNT(id)']); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ }
+
+ // Plugin operations
+ if (obj.pluginsActive) {
+ obj.addPlugin = function (plugin, func) { sqlDbQuery('INSERT INTO plugin VALUE (?, ?)', [null, JSON.stringify(plugin)], func); }; // Add a plugin
+ obj.getPlugins = function (func) { sqlDbQuery('SELECT JSON_INSERT(doc, "$._id", id) as doc FROM plugin', null, func); }; // Get all plugins
+ obj.getPlugin = function (id, func) { sqlDbQuery('SELECT JSON_INSERT(doc, "$._id", id) as doc FROM plugin WHERE id = ?', [id], func); }; // Get plugin
+ obj.deletePlugin = function (id, func) { sqlDbQuery('DELETE FROM plugin WHERE id = ?', [id], func); }; // Delete plugin
+ obj.setPluginStatus = function (id, status, func) { sqlDbQuery('UPDATE meshcentral.plugin SET doc=JSON_SET(doc,"$.status",?) WHERE id=?', [status,id], func); };
+ obj.updatePlugin = function (id, args, func) { delete args._id; sqlDbQuery('UPDATE meshcentral.plugin SET doc=JSON_MERGE_PATCH(doc,?) WHERE id=?', [JSON.stringify(args),id], func); };
+ }
+ } else if (obj.databaseType == DB_MONGODB) {
+ // Database actions on the main collection (MongoDB)
+
+ // Bulk operations
+ if (parent.config.settings.mongodbbulkoperations) {
+ obj.Set = function (data, func) { // Fast Set operation using bulkWrite(), this is much faster then using replaceOne()
+ if (obj.filePendingSet == false) {
+ // Perform the operation now
+ obj.dbCounters.fileSet++;
+ obj.filePendingSet = true; obj.filePendingSets = null;
+ if (func != null) { obj.filePendingCbs = [func]; }
+ obj.file.bulkWrite([{ replaceOne: { filter: { _id: data._id }, replacement: performTypedRecordEncrypt(common.escapeLinksFieldNameEx(data)), upsert: true } }], fileBulkWriteCompleted);
+ } else {
+ // Add this operation to the pending list
+ obj.dbCounters.fileSetPending++;
+ if (obj.filePendingSets == null) { obj.filePendingSets = {} }
+ obj.filePendingSets[data._id] = data;
+ if (func != null) { if (obj.filePendingCb == null) { obj.filePendingCb = [func]; } else { obj.filePendingCb.push(func); } }
+ }
+ };
+
+ obj.Get = function (id, func) { // Fast Get operation using a bulk find() to reduce round trips to the database.
+ // Encode arguments into return function if any are present.
+ var func2 = func;
+ if (arguments.length > 2) {
+ var parms = [func];
+ for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
+ var func2 = function _func2(arg1, arg2) {
+ var userCallback = _func2.userArgs.shift();
+ _func2.userArgs.unshift(arg2);
+ _func2.userArgs.unshift(arg1);
+ userCallback.apply(obj, _func2.userArgs);
+ };
+ func2.userArgs = parms;
+ }
+
+ if (obj.filePendingGets == null) {
+ // No pending gets, perform the operation now.
+ console.log("No pending gets, perform the operation now.");
+ obj.filePendingGets = {};
+ obj.filePendingGets[id] = [func2];
+ obj.file.find({ _id: id }).toArray(fileBulkReadCompleted);
+ } else {
+ // Add get to pending list.
+ console.log("Add get to pending list.");
+ if (obj.filePendingGet == null) { obj.filePendingGet = {}; }
+ if (obj.filePendingGet[id] == null) { obj.filePendingGet[id] = [func2]; } else { obj.filePendingGet[id].push(func2); }
+ }
+ };
+ } else {
+ obj.Set = function (data, func) {
+ obj.dbCounters.fileSet++;
+ data = common.escapeLinksFieldNameEx(data);
+ obj.file.replaceOne({ _id: data._id }, performTypedRecordEncrypt(data), { upsert: true }, func);
+ };
+ obj.Get = function (id, func) {
+ if (arguments.length > 2) {
+ var parms = [func];
+ for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
+ var func2 = function _func2(arg1, arg2) {
+ var userCallback = _func2.userArgs.shift();
+ _func2.userArgs.unshift(arg2);
+ _func2.userArgs.unshift(arg1);
+ userCallback.apply(obj, _func2.userArgs);
+ };
+ func2.userArgs = parms;
+ obj.file.find({ _id: id }).toArray(function (err, docs) {
+ console.log("1");
+ if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
+ func2(err, performTypedRecordDecrypt(docs));
+ });
+ } else {
+ //console.log("ID: " + id);
+ obj.file.find({ _id: id }).toArray(function (err, docs) {
+ //console.log("Docs: " + docs);
+ if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ }
+ };
+ }
+ obj.GetAll = function (func) { obj.file.find({}).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetHash = function (id, func) { obj.file.find({ _id: id }).project({ _id: 0, hash: 1 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }).project({ type: 0 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
+ if (extrasids == null) {
+ const x = { type: type, domain: domain, meshid: { $in: meshes } };
+ if (id) { x._id = id; }
+ var f = obj.file.find(x, { type: 0 });
+ if (skip > 0) f = f.skip(skip); // Skip records
+ if (limit > 0) f = f.limit(limit); // Limit records
+ f.toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
+ } else {
+ const x = { type: type, domain: domain, $or: [ { meshid: { $in: meshes } }, { _id: { $in: extrasids } } ] };
+ if (id) { x._id = id; }
+ var f = obj.file.find(x, { type: 0 });
+ if (skip > 0) f = f.skip(skip); // Skip records
+ if (limit > 0) f = f.limit(limit); // Limit records
+ f.toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
+ }
+ };
+ obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
+ if (extrasids == null) {
+ const x = { type: type, domain: domain, meshid: { $in: meshes } };
+ if (id) { x._id = id; }
+ var f = obj.file.find(x, { type: 0 });
+ f.count(function (err, count) { func(err, count); });
+ } else {
+ const x = { type: type, domain: domain, $or: [{ meshid: { $in: meshes } }, { _id: { $in: extrasids } }] };
+ if (id) { x._id = id; }
+ var f = obj.file.find(x, { type: 0 });
+ f.count(function (err, count) { func(err, count); });
+ }
+ };
+ obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
+ var x = { type: type, domain: domain, nodeid: { $in: nodes } };
+ if (id) { x._id = id; }
+ obj.file.find(x, { type: 0 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
+ };
+ obj.GetAllType = function (type, func) { obj.file.find({ type: type }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.find({ type: type, domain: domain, _id: { $in: ids } }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetUserWithEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email, emailVerified: true }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetNodeByComputerName = function (domain, rname, func) { obj.file.find({ type: 'node', domain: domain, rname: rname }).sort({ lastbootuptime: -1 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+
+ // Bulk operations
+ if (parent.config.settings.mongodbbulkoperations) {
+ obj.Remove = function (id, func) { // Fast remove operation using a bulk find() to reduce round trips to the database.
+ if (obj.filePendingRemoves == null) {
+ // No pending removes, perform the operation now.
+ obj.dbCounters.fileRemove++;
+ obj.filePendingRemoves = {};
+ obj.filePendingRemoves[id] = [func];
+ obj.file.deleteOne({ _id: id }, fileBulkRemoveCompleted);
+ } else {
+ // Add remove to pending list.
+ obj.dbCounters.fileRemovePending++;
+ if (obj.filePendingRemove == null) { obj.filePendingRemove = {}; }
+ if (obj.filePendingRemove[id] == null) { obj.filePendingRemove[id] = [func]; } else { obj.filePendingRemove[id].push(func); }
+ }
+ };
+ } else {
+ obj.Remove = function (id, func) { obj.dbCounters.fileRemove++; obj.file.deleteOne({ _id: id }, func); };
+ }
+
+ obj.RemoveAll = function (func) { obj.file.deleteMany({}, { multi: true }, func); };
+ obj.RemoveAllOfType = function (type, func) { obj.file.deleteMany({ type: type }, { multi: true }, func); };
+ obj.InsertMany = function (data, func) { obj.file.insertMany(data, func); }; // Insert records directly, no link escaping
+ obj.RemoveMeshDocuments = function (id) { obj.file.deleteMany({ meshid: id }, { multi: true }); obj.file.deleteOne({ _id: 'nt' + id }); };
+ obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
+ obj.DeleteDomain = function (domain, func) { obj.file.deleteMany({ domain: domain }, { multi: true }, func); };
+ obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
+ obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
+ obj.getLocalAmtNodes = function (func) { obj.file.find({ type: 'node', host: { $exists: true, $ne: null }, intelamt: { $exists: true } }).toArray(func); };
+ obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { obj.file.find({ type: 'node', domain: domainid, mtype: mtype, 'intelamt.uuid': uuid }).toArray(func); };
+
+ // TODO: Starting in MongoDB 4.0.3, you should use countDocuments() instead of count() that is deprecated. We should detect MongoDB version and switch.
+ // https://docs.mongodb.com/manual/reference/method/db.collection.countDocuments/
+ //obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { obj.file.countDocuments({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max)); }); } }
+ obj.isMaxType = function (max, type, domainid, func) {
+ if (obj.file.countDocuments) {
+ if (max == null) { func(false); } else { obj.file.countDocuments({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); }
+ } else {
+ if (max == null) { func(false); } else { obj.file.count({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); }
+ }
+ }
+
+ // Database actions on the events collection
+ obj.GetAllEvents = function (func) { obj.eventsfile.find({}).toArray(func); };
+
+ // Bulk operations
+ if (parent.config.settings.mongodbbulkoperations) {
+ obj.StoreEvent = function (event, func) { // Fast MongoDB event store using bulkWrite()
+ if (obj.eventsFilePendingSet == false) {
+ // Perform the operation now
+ obj.dbCounters.eventsSet++;
+ obj.eventsFilePendingSet = true; obj.eventsFilePendingSets = null;
+ if (func != null) { obj.eventsFilePendingCbs = [func]; }
+ obj.eventsfile.bulkWrite([{ insertOne: { document: event } }], eventsFileBulkWriteCompleted);
+ } else {
+ // Add this operation to the pending list
+ obj.dbCounters.eventsSetPending++;
+ if (obj.eventsFilePendingSets == null) { obj.eventsFilePendingSets = [] }
+ obj.eventsFilePendingSets.push(event);
+ if (func != null) { if (obj.eventsFilePendingCb == null) { obj.eventsFilePendingCb = [func]; } else { obj.eventsFilePendingCb.push(func); } }
+ }
+ };
+ } else {
+ obj.StoreEvent = function (event, func) { obj.dbCounters.eventsSet++; obj.eventsfile.insertOne(event, func); };
+ }
+
+ obj.GetEvents = function (ids, domain, filter, func) {
+ var finddata = { domain: domain, ids: { $in: ids } };
+ if (filter != null) finddata.action = filter;
+ obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).toArray(func);
+ };
+ obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
+ var finddata = { domain: domain, ids: { $in: ids } };
+ if (filter != null) finddata.action = filter;
+ obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).toArray(func);
+ };
+ obj.GetUserEvents = function (ids, domain, userid, filter, func) {
+ var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
+ if (filter != null) finddata.action = filter;
+ obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).toArray(func);
+ };
+ obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
+ var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
+ if (filter != null) finddata.action = filter;
+ obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).toArray(func);
+ };
+ obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) { obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }], msgid: { $in: msgids }, time: { $gte: start, $lte: end } }).project({ type: 0, _id: 0, domain: 0, node: 0 }).sort({ time: 1 }).toArray(func); };
+ obj.GetUserLoginEvents = function (domain, userid, func) { obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }).project({ action: 1, time: 1, msgid: 1, msgArgs: 1, tokenName: 1 }).sort({ time: -1 }).toArray(func); };
+ obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
+ var finddata = { domain: domain, nodeid: nodeid };
+ if (filter != null) finddata.action = filter;
+ obj.eventsfile.find(finddata).project({ type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).toArray(func);
+ };
+ obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
+ var finddata = { domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } };
+ if (filter != null) finddata.action = filter;
+ obj.eventsfile.find(finddata).project({ type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).toArray(func);
+ };
+ obj.RemoveAllEvents = function (domain) { obj.eventsfile.deleteMany({ domain: domain }, { multi: true }); };
+ obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; obj.eventsfile.deleteMany({ domain: domain, nodeid: nodeid }, { multi: true }); };
+ obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; obj.eventsfile.deleteMany({ domain: domain, userid: userid }, { multi: true }); };
+ obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) {
+ if (obj.eventsfile.countDocuments) {
+ obj.eventsfile.countDocuments({ action: 'authfail', userid: userid, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null) ? count : 0); });
+ } else {
+ obj.eventsfile.count({ action: 'authfail', userid: userid, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null) ? count : 0); });
+ }
+ }
+
+ // Database actions on the power collection
+ obj.getAllPower = function (func) { obj.powerfile.find({}).toArray(func); };
+
+ // Bulk operations
+ if (parent.config.settings.mongodbbulkoperations) {
+ obj.storePowerEvent = function (event, multiServer, func) { // Fast MongoDB event store using bulkWrite()
+ if (multiServer != null) { event.server = multiServer.serverid; }
+ if (obj.powerFilePendingSet == false) {
+ // Perform the operation now
+ obj.dbCounters.powerSet++;
+ obj.powerFilePendingSet = true; obj.powerFilePendingSets = null;
+ if (func != null) { obj.powerFilePendingCbs = [func]; }
+ obj.powerfile.bulkWrite([{ insertOne: { document: event } }], powerFileBulkWriteCompleted);
+ } else {
+ // Add this operation to the pending list
+ obj.dbCounters.powerSetPending++;
+ if (obj.powerFilePendingSets == null) { obj.powerFilePendingSets = [] }
+ obj.powerFilePendingSets.push(event);
+ if (func != null) { if (obj.powerFilePendingCb == null) { obj.powerFilePendingCb = [func]; } else { obj.powerFilePendingCb.push(func); } }
+ }
+ };
+ } else {
+ obj.storePowerEvent = function (event, multiServer, func) { obj.dbCounters.powerSet++; if (multiServer != null) { event.server = multiServer.serverid; } obj.powerfile.insertOne(event, func); };
+ }
+
+ obj.getPowerTimeline = function (nodeid, func) { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }).project({ _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }).toArray(func); };
+ obj.removeAllPowerEvents = function () { obj.powerfile.deleteMany({}, { multi: true }); };
+ obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; obj.powerfile.deleteMany({ nodeid: nodeid }, { multi: true }); };
+
+ // Database actions on the SMBIOS collection
+ obj.GetAllSMBIOS = function (func) { obj.smbiosfile.find({}).toArray(func); };
+ obj.SetSMBIOS = function (smbios, func) { obj.smbiosfile.updateOne({ _id: smbios._id }, { $set: smbios }, { upsert: true }, func); };
+ obj.RemoveSMBIOS = function (id) { obj.smbiosfile.deleteOne({ _id: id }); };
+ obj.GetSMBIOS = function (id, func) { obj.smbiosfile.find({ _id: id }).toArray(func); };
+
+ // Database actions on the Server Stats collection
+ obj.SetServerStats = function (data, func) { obj.serverstatsfile.insertOne(data, func); };
+ obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); obj.serverstatsfile.find({ time: { $gt: t } }, { _id: 0, cpu: 0 }).toArray(func); };
+
+ // Read a configuration file from the database
+ obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
+
+ // Write a configuration file to the database
+ obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
+
+ // List all configuration files
+ obj.listConfigFiles = function (func) { obj.file.find({ type: 'cfile' }).sort({ _id: 1 }).toArray(func); }
+
+ // Get database information
+ obj.getDbStats = function (func) {
+ obj.stats = { c: 6 };
+ obj.getStats(function (r) { obj.stats.recordTypes = r; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } })
+ obj.file.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
+ obj.eventsfile.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
+ obj.powerfile.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
+ obj.smbiosfile.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
+ obj.serverstatsfile.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
+ }
+
+ // Correct database information of obj.getDbStats before returning it
+ function getDbStatsEx(data) {
+ var r = {};
+ if (data.recordTypes != null) { r = data.recordTypes; }
+ try { r.smbios = data['meshcentral.smbios'].count; } catch (ex) { }
+ try { r.power = data['meshcentral.power'].count; } catch (ex) { }
+ try { r.events = data['meshcentral.events'].count; } catch (ex) { }
+ try { r.serverstats = data['meshcentral.serverstats'].count; } catch (ex) { }
+ return r;
+ }
+
+ // Plugin operations
+ if (obj.pluginsActive) {
+ obj.addPlugin = function (plugin, func) { plugin.type = 'plugin'; obj.pluginsfile.insertOne(plugin, func); }; // Add a plugin
+ obj.getPlugins = function (func) { obj.pluginsfile.find({ type: 'plugin' }).project({ type: 0 }).sort({ name: 1 }).toArray(func); }; // Get all plugins
+ obj.getPlugin = function (id, func) { id = require('mongodb').ObjectId(id); obj.pluginsfile.find({ _id: id }).sort({ name: 1 }).toArray(func); }; // Get plugin
+ obj.deletePlugin = function (id, func) { id = require('mongodb').ObjectId(id); obj.pluginsfile.deleteOne({ _id: id }, func); }; // Delete plugin
+ obj.setPluginStatus = function (id, status, func) { id = require('mongodb').ObjectId(id); obj.pluginsfile.updateOne({ _id: id }, { $set: { status: status } }, func); };
+ obj.updatePlugin = function (id, args, func) { delete args._id; id = require('mongodb').ObjectId(id); obj.pluginsfile.updateOne({ _id: id }, { $set: args }, func); };
+ }
+
+ } else {
+ // Database actions on the main collection (NeDB and MongoJS)
+ obj.Set = function (data, func) {
+ obj.dbCounters.fileSet++;
+ data = common.escapeLinksFieldNameEx(data);
+ var xdata = performTypedRecordEncrypt(data); obj.file.update({ _id: xdata._id }, xdata, { upsert: true }, func);
+ };
+ obj.Get = function (id, func) {
+ if (arguments.length > 2) {
+ var parms = [func];
+ for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
+ var func2 = function _func2(arg1, arg2) {
+ var userCallback = _func2.userArgs.shift();
+ _func2.userArgs.unshift(arg2);
+ _func2.userArgs.unshift(arg1);
+ userCallback.apply(obj, _func2.userArgs);
+ };
+ func2.userArgs = parms;
+ obj.file.find({ _id: id }, function (err, docs) {
+ if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
+ func2(err, performTypedRecordDecrypt(docs));
+ });
+ } else {
+ obj.file.find({ _id: id }, function (err, docs) {
+ if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ }
+ };
+ obj.GetAll = function (func) { obj.file.find({}, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetHash = function (id, func) { obj.file.find({ _id: id }, { _id: 0, hash: 1 }, func); };
+ obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ //obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, domain, type, id, skip, limit, func) {
+ //var x = { type: type, domain: domain, meshid: { $in: meshes } };
+ //if (id) { x._id = id; }
+ //obj.file.find(x, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
+ //};
+ obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
+ if (extrasids == null) {
+ const x = { type: type, domain: domain, meshid: { $in: meshes } };
+ if (id) { x._id = id; }
+ obj.file.find(x, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
+ } else {
+ const x = { type: type, domain: domain, $or: [{ meshid: { $in: meshes } }, { _id: { $in: extrasids } }] };
+ if (id) { x._id = id; }
+ obj.file.find(x, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
+ }
+ };
+ obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
+ var x = { type: type, domain: domain, nodeid: { $in: nodes } };
+ if (id) { x._id = id; }
+ obj.file.find(x, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
+ };
+ obj.GetAllType = function (type, func) { obj.file.find({ type: type }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.find({ type: type, domain: domain, _id: { $in: ids } }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetUserWithEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email }, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email, emailVerified: true }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetNodeByComputerName = function (domain, rname, func) { obj.file.find({ type: 'node', domain: domain, rname: rname }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.Remove = function (id, func) { obj.file.remove({ _id: id }, func); };
+ obj.RemoveAll = function (func) { obj.file.remove({}, { multi: true }, func); };
+ obj.RemoveAllOfType = function (type, func) { obj.file.remove({ type: type }, { multi: true }, func); };
+ obj.InsertMany = function (data, func) { obj.file.insert(data, func); }; // Insert records directly, no link escaping
+ obj.RemoveMeshDocuments = function (id) { obj.file.remove({ meshid: id }, { multi: true }); obj.file.remove({ _id: 'nt' + id }); };
+ obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
+ obj.DeleteDomain = function (domain, func) { obj.file.remove({ domain: domain }, { multi: true }, func); };
+ obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
+ obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
+ obj.getLocalAmtNodes = function (func) { obj.file.find({ type: 'node', host: { $exists: true, $ne: null }, intelamt: { $exists: true } }, func); };
+ obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { obj.file.find({ type: 'node', domain: domainid, mtype: mtype, 'intelamt.uuid': uuid }, func); };
+ obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { obj.file.count({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); } }
+
+ // Database actions on the events collection
+ obj.GetAllEvents = function (func) { obj.eventsfile.find({}, func); };
+ obj.StoreEvent = function (event, func) { obj.eventsfile.insert(event, func); };
+ obj.GetEvents = function (ids, domain, filter, func) {
+ var finddata = { domain: domain, ids: { $in: ids } };
+ if (filter != null) finddata.action = filter;
+ if (obj.databaseType == DB_NEDB) {
+ obj.eventsfile.find(finddata, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func);
+ } else {
+ obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func);
+ }
+ };
+ obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
+ var finddata = { domain: domain, ids: { $in: ids } };
+ if (filter != null) finddata.action = filter;
+ if (obj.databaseType == DB_NEDB) {
+ obj.eventsfile.find(finddata, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func);
+ } else {
+ obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func);
+ }
+ };
+ obj.GetUserEvents = function (ids, domain, userid, filter, func) {
+ var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
+ if (filter != null) finddata.action = filter;
+ if (obj.databaseType == DB_NEDB) {
+ obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func);
+ } else {
+ obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func);
+ }
+ };
+ obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
+ var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
+ if (filter != null) finddata.action = filter;
+ if (obj.databaseType == DB_NEDB) {
+ obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func);
+ } else {
+ obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func);
+ }
+ };
+ obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
+ if (obj.databaseType == DB_NEDB) {
+ obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }], msgid: { $in: msgids }, time: { $gte: start, $lte: end } }, { type: 0, _id: 0, domain: 0, node: 0 }).sort({ time: 1 }).exec(func);
+ } else {
+ obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }], msgid: { $in: msgids }, time: { $gte: start, $lte: end } }, { type: 0, _id: 0, domain: 0, node: 0 }).sort({ time: 1 }, func);
+ }
+ };
+ obj.GetUserLoginEvents = function (domain, userid, func) {
+ if (obj.databaseType == DB_NEDB) {
+ obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }, { action: 1, time: 1, msgid: 1, msgArgs: 1, tokenName: 1 }).sort({ time: -1 }).exec(func);
+ } else {
+ obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }, { action: 1, time: 1, msgid: 1, msgArgs: 1, tokenName: 1 }).sort({ time: -1 }, func);
+ }
+ };
+ obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
+ var finddata = { domain: domain, nodeid: nodeid };
+ if (filter != null) finddata.action = filter;
+ if (obj.databaseType == DB_NEDB) {
+ obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).exec(func);
+ } else {
+ obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit, func);
+ }
+ };
+ obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
+ var finddata = { domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } };
+ if (filter != null) finddata.action = filter;
+ if (obj.databaseType == DB_NEDB) {
+ obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).exec(func);
+ } else {
+ obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit, func);
+ }
+ };
+ obj.RemoveAllEvents = function (domain) { obj.eventsfile.remove({ domain: domain }, { multi: true }); };
+ obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; obj.eventsfile.remove({ domain: domain, nodeid: nodeid }, { multi: true }); };
+ obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; obj.eventsfile.remove({ domain: domain, userid: userid }, { multi: true }); };
+ obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) { obj.eventsfile.count({ action: 'authfail', userid: userid, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null) ? count : 0); }); }
+
+ // Database actions on the power collection
+ obj.getAllPower = function (func) { obj.powerfile.find({}, func); };
+ obj.storePowerEvent = function (event, multiServer, func) { if (multiServer != null) { event.server = multiServer.serverid; } obj.powerfile.insert(event, func); };
+ obj.getPowerTimeline = function (nodeid, func) { if (obj.databaseType == DB_NEDB) { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }, { _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }).exec(func); } else { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }, { _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }, func); } };
+ obj.removeAllPowerEvents = function () { obj.powerfile.remove({}, { multi: true }); };
+ obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; obj.powerfile.remove({ nodeid: nodeid }, { multi: true }); };
+
+ // Database actions on the SMBIOS collection
+ if (obj.smbiosfile != null) {
+ obj.GetAllSMBIOS = function (func) { obj.smbiosfile.find({}, func); };
+ obj.SetSMBIOS = function (smbios, func) { obj.smbiosfile.update({ _id: smbios._id }, smbios, { upsert: true }, func); };
+ obj.RemoveSMBIOS = function (id) { obj.smbiosfile.remove({ _id: id }); };
+ obj.GetSMBIOS = function (id, func) { obj.smbiosfile.find({ _id: id }, func); };
+ }
+
+ // Database actions on the Server Stats collection
+ obj.SetServerStats = function (data, func) { obj.serverstatsfile.insert(data, func); };
+ obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); obj.serverstatsfile.find({ time: { $gt: t } }, { _id: 0, cpu: 0 }, func); };
+
+ // Read a configuration file from the database
+ obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
+
+ // Write a configuration file to the database
+ obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
+
+ // List all configuration files
+ obj.listConfigFiles = function (func) { obj.file.find({ type: 'cfile' }).sort({ _id: 1 }).exec(func); }
+
+ // Get database information
+ obj.getDbStats = function (func) {
+ obj.stats = { c: 5 };
+ obj.getStats(function (r) { obj.stats.recordTypes = r; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } })
+ obj.file.count({}, function (err, count) { obj.stats.meshcentral = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
+ obj.eventsfile.count({}, function (err, count) { obj.stats.events = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
+ obj.powerfile.count({}, function (err, count) { obj.stats.power = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
+ obj.serverstatsfile.count({}, function (err, count) { obj.stats.serverstats = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
+ }
+
+ // Correct database information of obj.getDbStats before returning it
+ function getDbStatsEx(data) {
+ var r = {};
+ if (data.recordTypes != null) { r = data.recordTypes; }
+ try { r.smbios = data['smbios'].count; } catch (ex) { }
+ try { r.power = data['power'].count; } catch (ex) { }
+ try { r.events = data['events'].count; } catch (ex) { }
+ try { r.serverstats = data['serverstats'].count; } catch (ex) { }
+ return r;
+ }
+
+ // Plugin operations
+ if (obj.pluginsActive) {
+ obj.addPlugin = function (plugin, func) { plugin.type = 'plugin'; obj.pluginsfile.insert(plugin, func); }; // Add a plugin
+ obj.getPlugins = function (func) { obj.pluginsfile.find({ 'type': 'plugin' }, { 'type': 0 }).sort({ name: 1 }).exec(func); }; // Get all plugins
+ obj.getPlugin = function (id, func) { obj.pluginsfile.find({ _id: id }).sort({ name: 1 }).exec(func); }; // Get plugin
+ obj.deletePlugin = function (id, func) { obj.pluginsfile.remove({ _id: id }, func); }; // Delete plugin
+ obj.setPluginStatus = function (id, status, func) { obj.pluginsfile.update({ _id: id }, { $set: { status: status } }, func); };
+ obj.updatePlugin = function (id, args, func) { delete args._id; obj.pluginsfile.update({ _id: id }, { $set: args }, func); };
+ }
+
+ }
+
+ // Get all configuration files
+ obj.getAllConfigFiles = function (password, func) {
+ obj.GetAllType('cfile', function (err, docs) {
+ if (err != null) { func(null); return; }
+ var r = null;
+ for (var i = 0; i < docs.length; i++) {
+ var name = docs[i]._id.split('/')[1];
+ var data = obj.decryptData(password, docs[i].data);
+ if (data != null) { if (r == null) { r = {}; } r[name] = data; }
+ }
+ func(r);
+ });
+ }
+
+ func(obj); // Completed function setup
+ }
+
+ // Return a human readable string with current backup configuration
+ obj.getBackupConfig = function () {
+ var r = '', backupPath = parent.backuppath;
+
+ let dbname = 'meshcentral';
+ if (parent.args.mongodbname) { dbname = parent.args.mongodbname; }
+ else if ((typeof parent.args.mariadb == 'object') && (typeof parent.args.mariadb.database == 'string')) { dbname = parent.args.mariadb.database; }
+ else if ((typeof parent.args.mysql == 'object') && (typeof parent.args.mysql.database == 'string')) { dbname = parent.args.mysql.database; }
+ else if ((typeof parent.args.postgres == 'object') && (typeof parent.args.postgres.database == 'string')) { dbname = parent.args.postgres.database; }
+ else if (typeof parent.config.settings.sqlite3 == 'string') {dbname = parent.config.settings.sqlite3 + '.sqlite'};
+
+ const currentDate = new Date();
+ const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2);
+ obj.newAutoBackupFile = parent.config.settings.autobackup.backupname + fileSuffix;
+
+ r += 'DB Name: ' + dbname + '\r\n';
+ r += 'DB Type: ' + DB_LIST[obj.databaseType] + '\r\n';
+
+ if (parent.config.settings.autobackup.backupintervalhours == -1) {
+ r += 'Backup disabled\r\n';
+ } else {
+ r += 'BackupPath: ' + backupPath + '\r\n';
+ r += 'BackupFile: ' + obj.newAutoBackupFile + '.zip\r\n';
+
+ if (parent.config.settings.autobackup.backuphour != null && parent.config.settings.autobackup.backuphour != -1) {
+ r += 'Backup between: ' + parent.config.settings.autobackup.backuphour + 'H-' + (parent.config.settings.autobackup.backuphour + 1) + 'H\r\n';
+ }
+ r += 'Backup Interval (Hours): ' + parent.config.settings.autobackup.backupintervalhours + '\r\n';
+ if (parent.config.settings.autobackup.keeplastdaysbackup != null) {
+ r += 'Keep Last Backups (Days): ' + parent.config.settings.autobackup.keeplastdaysbackup + '\r\n';
+ }
+ if (parent.config.settings.autobackup.zippassword != null) {
+ r += 'ZIP Password: ';
+ if (typeof parent.config.settings.autobackup.zippassword != 'string') { r += 'Bad zippassword type, Backups will not be encrypted\r\n'; }
+ else if (parent.config.settings.autobackup.zippassword == "") { r += 'Blank zippassword, Backups will fail\r\n'; }
+ else { r += 'Set\r\n'; }
+ }
+ if (parent.config.settings.autobackup.mongodumppath != null) {
+ r += 'MongoDump Path: ';
+ if (typeof parent.config.settings.autobackup.mongodumppath != 'string') { r += 'Bad mongodumppath type\r\n'; }
+ else { r += parent.config.settings.autobackup.mongodumppath + '\r\n'; }
+ }
+ if (parent.config.settings.autobackup.mysqldumppath != null) {
+ r += 'MySqlDump Path: ';
+ if (typeof parent.config.settings.autobackup.mysqldumppath != 'string') { r += 'Bad mysqldump type\r\n'; }
+ else { r += parent.config.settings.autobackup.mysqldumppath + '\r\n'; }
+ }
+ if (parent.config.settings.autobackup.pgdumppath != null) {
+ r += 'pgDump Path: ';
+ if (typeof parent.config.settings.autobackup.pgdumppath != 'string') { r += 'Bad pgdump type\r\n'; }
+ else { r += parent.config.settings.autobackup.pgdumppath + '\r\n'; }
+ }
+ if (parent.config.settings.autobackup.backupotherfolders) {
+ r += 'Backup other folders: ';
+ r += parent.filespath + ', ' + parent.recordpath + '\r\n';
+ }
+ if (parent.config.settings.autobackup.backupwebfolders) {
+ r += 'Backup webfolders: ';
+ if (parent.webViewsOverridePath) {r += parent.webViewsOverridePath };
+ if (parent.webPublicOverridePath) {r += ', '+ parent.webPublicOverridePath};
+ if (parent.webEmailsOverridePath) {r += ',' + parent.webEmailsOverridePath};
+ r+= '\r\n';
+ }
+ if (parent.config.settings.autobackup.backupignorefilesglob != []) {
+ r += 'Backup IgnoreFilesGlob: ';
+ { r += parent.config.settings.autobackup.backupignorefilesglob + '\r\n'; }
+ }
+ if (parent.config.settings.autobackup.backupskipfoldersglob != []) {
+ r += 'Backup SkipFoldersGlob: ';
+ { r += parent.config.settings.autobackup.backupskipfoldersglob + '\r\n'; }
+ }
+
+ if (typeof parent.config.settings.autobackup.s3 == 'object') {
+ r += 'S3 Backups: Enabled\r\n';
+ }
+ if (typeof parent.config.settings.autobackup.webdav == 'object') {
+ r += 'WebDAV Backups: Enabled\r\n';
+ r += 'WebDAV backup path: ' + ((typeof parent.config.settings.autobackup.webdav.foldername == 'string') ? parent.config.settings.autobackup.webdav.foldername : 'MeshCentral-Backups') + '\r\n';
+ r += 'WebDAV maximum files: '+ ((typeof parent.config.settings.autobackup.webdav.maxfiles == 'number') ? parent.config.settings.autobackup.webdav.maxfiles : 'no limit') + '\r\n';
+ }
+ if (typeof parent.config.settings.autobackup.googledrive == 'object') {
+ r += 'Google Drive Backups: Enabled\r\n';
+ }
+
+
+ }
+
+ return r;
+ }
+
+ function buildSqlDumpCommand() {
+ var props = (obj.databaseType == DB_MARIADB) ? parent.args.mariadb : parent.args.mysql;
+
+ var mysqldumpPath = 'mysqldump';
+ if (parent.config.settings.autobackup && parent.config.settings.autobackup.mysqldumppath) {
+ mysqldumpPath = path.normalize(parent.config.settings.autobackup.mysqldumppath);
+ }
+
+ var cmd = '\"' + mysqldumpPath + '\" --user=\'' + props.user + '\'';
+ // Windows will treat ' as part of the pw. Linux/Unix requires it to escape.
+ cmd += (parent.platform == 'win32') ? ' --password=\"' + props.password + '\"' : ' --password=\'' + props.password + '\'';
+ if (props.host) { cmd += ' -h ' + props.host; }
+ if (props.port) { cmd += ' -P ' + props.port; }
+
+ if (props.awsrds) { cmd += ' --single-transaction'; }
+
+ // SSL options different on mariadb/mysql
+ var sslOptions = '';
+ if (obj.databaseType == DB_MARIADB) {
+ if (props.ssl) {
+ sslOptions = ' --ssl';
+ if (props.ssl.cacertpath) sslOptions = ' --ssl-ca=' + props.ssl.cacertpath;
+ if (props.ssl.dontcheckserveridentity != true) {sslOptions += ' --ssl-verify-server-cert'} else {sslOptions += ' --ssl-verify-server-cert=false'};
+ if (props.ssl.clientcertpath) sslOptions += ' --ssl-cert=' + props.ssl.clientcertpath;
+ if (props.ssl.clientkeypath) sslOptions += ' --ssl-key=' + props.ssl.clientkeypath;
+ }
+ } else {
+ if (props.ssl) {
+ sslOptions = ' --ssl-mode=required';
+ if (props.ssl.cacertpath) sslOptions = ' --ssl-ca=' + props.ssl.cacertpath;
+ if (props.ssl.dontcheckserveridentity != true) sslOptions += ' --ssl-mode=verify_identity';
+ else sslOptions += ' --ssl-mode=required';
+ if (props.ssl.clientcertpath) sslOptions += ' --ssl-cert=' + props.ssl.clientcertpath;
+ if (props.ssl.clientkeypath) sslOptions += ' --ssl-key=' + props.ssl.clientkeypath;
+ }
+ }
+ cmd += sslOptions;
+
+ var dbname = (props.database) ? props.database : 'meshcentral';
+ cmd += ' ' + dbname
+
+ return cmd;
+ }
+
+ function buildMongoDumpCommand() {
+ const dburl = parent.args.mongodb;
+
+ var mongoDumpPath = 'mongodump';
+ if (parent.config.settings.autobackup && parent.config.settings.autobackup.mongodumppath) {
+ mongoDumpPath = path.normalize(parent.config.settings.autobackup.mongodumppath);
+ }
+
+ var cmd = '"' + mongoDumpPath + '"';
+ if (dburl) { cmd = '\"' + mongoDumpPath + '\" --uri=\"' + dburl + '\"'; }
+ if (parent.config.settings.autobackup?.mongodumpargs) {
+ cmd = '\"' + mongoDumpPath + '\" ' + parent.config.settings.autobackup.mongodumpargs;
+ if (!parent.config.settings.autobackup.mongodumpargs.includes("--db=")) {cmd += ' --db=' + (parent.config.settings.mongodbname ? parent.config.settings.mongodbname : 'meshcentral')};
+ }
+ return cmd;
+ }
+
+ // Check that the server is capable of performing a backup
+ // Tries configured custom location with fallback to default location
+ // Now runs after autobackup config init in meshcentral.js so config options are checked
+ obj.checkBackupCapability = function (func) {
+ if (parent.config.settings.autobackup.backupintervalhours == -1) { return; };
+ //block backup until validated. Gets put back if all checks are ok.
+ let backupInterval = parent.config.settings.autobackup.backupintervalhours;
+ parent.config.settings.autobackup.backupintervalhours = -1;
+ let backupPath = parent.backuppath;
+
+ if (backupPath.startsWith(parent.datapath)) {
+ func(1, "Backup path can't be set within meshcentral-data folder. No backups will be made.");
+ return;
+ }
+ // Check create/write backupdir
+ try { fs.mkdirSync(backupPath); }
+ catch (e) {
+ // EEXIST error = dir already exists
+ if (e.code != 'EEXIST' ) {
+ //Unable to create backuppath
+ console.error(e.message);
+ func(1, 'Unable to create ' + backupPath + '. No backups will be made. Error: ' + e.message);
+ return;
+ }
+ }
+ const currentDate = new Date();
+ const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2);
+ const testFile = path.join(backupPath, parent.config.settings.autobackup.backupname + fileSuffix + '.zip');
+ try { fs.writeFileSync( testFile, "DeleteMe"); }
+ catch (e) {
+ //Unable to create file
+ console.error (e.message);
+ func(1, "Backuppath (" + backupPath + ") can't be written to. No backups will be made. Error: " + e.message);
+ return;
+ }
+ try { fs.unlinkSync(testFile); parent.debug('backup', 'Backuppath ' + backupPath + ' accesscheck successful');}
+ catch (e) {
+ console.error (e.message);
+ func(1, "Backuppathtestfile (" + testFile + ") can't be deleted, check filerights. Error: " + e.message);
+ // Assume write rights, no delete rights. Continue with warning.
+ //return;
+ }
+
+ // Check database dumptools
+ if ((obj.databaseType == DB_MONGOJS) || (obj.databaseType == DB_MONGODB)) {
+ // Check that we have access to MongoDump
+ var cmd = buildMongoDumpCommand();
+ cmd += (parent.platform == 'win32') ? ' --archive=\"nul\"' : ' --archive=\"/dev/null\"';
+ const child_process = require('child_process');
+ child_process.exec(cmd, { cwd: backupPath }, function (error, stdout, stderr) {
+ if ((error != null) && (error != '')) {
+ func(1, "Mongodump error, backup will not be performed. Check path or use mongodumppath & mongodumpargs");
+ return;
+ } else {parent.config.settings.autobackup.backupintervalhours = backupInterval;}
+ });
+ } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
+ // Check that we have access to mysqldump
+ var cmd = buildSqlDumpCommand();
+ cmd += ' > ' + ((parent.platform == 'win32') ? '\"nul\"' : '\"/dev/null\"');
+ const child_process = require('child_process');
+ child_process.exec(cmd, { cwd: backupPath, timeout: 1000*30 }, function(error, stdout, stdin) {
+ if ((error != null) && (error != '')) {
+ func(1, "mysqldump error, backup will not be performed. Check path or use mysqldumppath");
+ return;
+ } else {parent.config.settings.autobackup.backupintervalhours = backupInterval;}
+
+ });
+ } else if (obj.databaseType == DB_POSTGRESQL) {
+ // Check that we have access to pg_dump
+ parent.config.settings.autobackup.pgdumppath = path.normalize(parent.config.settings.autobackup.pgdumppath ? parent.config.settings.autobackup.pgdumppath : 'pg_dump');
+ let cmd = '"' + parent.config.settings.autobackup.pgdumppath + '"'
+ + ' --dbname=postgresql://' + parent.config.settings.postgres.user + ":" +parent.config.settings.postgres.password
+ + "@" + parent.config.settings.postgres.host + ":" + parent.config.settings.postgres.port + "/" + databaseName
+ + ' > ' + ((parent.platform == 'win32') ? '\"nul\"' : '\"/dev/null\"');
+ const child_process = require('child_process');
+ child_process.exec(cmd, { cwd: backupPath }, function(error, stdout, stdin) {
+ if ((error != null) && (error != '')) {
+ func(1, "pg_dump error, backup will not be performed. Check path or use pgdumppath.");
+ return;
+ } else {parent.config.settings.autobackup.backupintervalhours = backupInterval;}
+ });
+ } else {
+ //all ok, enable backup
+ parent.config.settings.autobackup.backupintervalhours = backupInterval;}
+ }
+
+ // MongoDB pending bulk read operation, perform fast bulk document reads.
+ function fileBulkReadCompleted(err, docs) {
+ // Send out callbacks with results
+ if (docs != null) {
+ for (var i in docs) {
+ if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); }
+ const id = docs[i]._id;
+ if (obj.filePendingGets[id] != null) {
+ for (var j in obj.filePendingGets[id]) {
+ if (typeof obj.filePendingGets[id][j] == 'function') { obj.filePendingGets[id][j](err, performTypedRecordDecrypt([docs[i]])); }
+ }
+ delete obj.filePendingGets[id];
+ }
+ }
+ }
+
+ // If there are not results, send out a null callback
+ for (var i in obj.filePendingGets) { for (var j in obj.filePendingGets[i]) { obj.filePendingGets[i][j](err, []); } }
+
+ // Move on to process any more pending get operations
+ obj.filePendingGets = obj.filePendingGet;
+ obj.filePendingGet = null;
+ if (obj.filePendingGets != null) {
+ var findlist = [];
+ for (var i in obj.filePendingGets) { findlist.push(i); }
+ obj.file.find({ _id: { $in: findlist } }).toArray(fileBulkReadCompleted);
+ }
+ }
+
+ // MongoDB pending bulk remove operation, perform fast bulk document removes.
+ function fileBulkRemoveCompleted(err) {
+ // Send out callbacks
+ for (var i in obj.filePendingRemoves) {
+ for (var j in obj.filePendingRemoves[i]) {
+ if (typeof obj.filePendingRemoves[i][j] == 'function') { obj.filePendingRemoves[i][j](err); }
+ }
+ }
+
+ // Move on to process any more pending get operations
+ obj.filePendingRemoves = obj.filePendingRemove;
+ obj.filePendingRemove = null;
+ if (obj.filePendingRemoves != null) {
+ obj.dbCounters.fileRemoveBulk++;
+ var findlist = [], count = 0;
+ for (var i in obj.filePendingRemoves) { findlist.push(i); count++; }
+ obj.file.deleteMany({ _id: { $in: findlist } }, { multi: true }, fileBulkRemoveCompleted);
+ }
+ }
+
+ // MongoDB pending bulk write operation, perform fast bulk document replacement.
+ function fileBulkWriteCompleted() {
+ // Callbacks
+ if (obj.filePendingCbs != null) {
+ for (var i in obj.filePendingCbs) { if (typeof obj.filePendingCbs[i] == 'function') { obj.filePendingCbs[i](); } }
+ obj.filePendingCbs = null;
+ }
+ if (obj.filePendingSets != null) {
+ // Perform pending operations
+ obj.dbCounters.fileSetBulk++;
+ var ops = [];
+ obj.filePendingCbs = obj.filePendingCb;
+ obj.filePendingCb = null;
+ for (var i in obj.filePendingSets) { ops.push({ replaceOne: { filter: { _id: i }, replacement: performTypedRecordEncrypt(common.escapeLinksFieldNameEx(obj.filePendingSets[i])), upsert: true } }); }
+ obj.file.bulkWrite(ops, fileBulkWriteCompleted);
+ obj.filePendingSets = null;
+ } else {
+ // All done, no pending operations.
+ obj.filePendingSet = false;
+ }
+ }
+
+ // MongoDB pending bulk write operation, perform fast bulk document replacement.
+ function eventsFileBulkWriteCompleted() {
+ // Callbacks
+ if (obj.eventsFilePendingCbs != null) { for (var i in obj.eventsFilePendingCbs) { obj.eventsFilePendingCbs[i](); } obj.eventsFilePendingCbs = null; }
+ if (obj.eventsFilePendingSets != null) {
+ // Perform pending operations
+ obj.dbCounters.eventsSetBulk++;
+ var ops = [];
+ for (var i in obj.eventsFilePendingSets) { ops.push({ insertOne: { document: obj.eventsFilePendingSets[i] } }); }
+ obj.eventsFilePendingCbs = obj.eventsFilePendingCb;
+ obj.eventsFilePendingCb = null;
+ obj.eventsFilePendingSets = null;
+ obj.eventsfile.bulkWrite(ops, eventsFileBulkWriteCompleted);
+ } else {
+ // All done, no pending operations.
+ obj.eventsFilePendingSet = false;
+ }
+ }
+
+ // MongoDB pending bulk write operation, perform fast bulk document replacement.
+ function powerFileBulkWriteCompleted() {
+ // Callbacks
+ if (obj.powerFilePendingCbs != null) { for (var i in obj.powerFilePendingCbs) { obj.powerFilePendingCbs[i](); } obj.powerFilePendingCbs = null; }
+ if (obj.powerFilePendingSets != null) {
+ // Perform pending operations
+ obj.dbCounters.powerSetBulk++;
+ var ops = [];
+ for (var i in obj.powerFilePendingSets) { ops.push({ insertOne: { document: obj.powerFilePendingSets[i] } }); }
+ obj.powerFilePendingCbs = obj.powerFilePendingCb;
+ obj.powerFilePendingCb = null;
+ obj.powerFilePendingSets = null;
+ obj.powerfile.bulkWrite(ops, powerFileBulkWriteCompleted);
+ } else {
+ // All done, no pending operations.
+ obj.powerFilePendingSet = false;
+ }
+ }
+
+ // Perform a server backup
+ obj.performBackup = function (func) {
+ parent.debug('backup','Entering performBackup');
+ try {
+ if (obj.performingBackup) return 'Backup alreay in progress.';
+ if (parent.config.settings.autobackup.backupintervalhours == -1) { if (func) { func('Backup disabled.'); return 'Backup disabled.' }};
+ obj.performingBackup = true;
+ let backupPath = parent.backuppath;
+ let dataPath = parent.datapath;
+
+ const currentDate = new Date();
+ const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2);
+ obj.newAutoBackupFile = path.join(backupPath, parent.config.settings.autobackup.backupname + fileSuffix + '.zip');
+ parent.debug('backup','newAutoBackupFile=' + obj.newAutoBackupFile);
+
+ if ((obj.databaseType == DB_MONGOJS) || (obj.databaseType == DB_MONGODB)) {
+ // Perform a MongoDump
+ const dbname = (parent.args.mongodbname) ? (parent.args.mongodbname) : 'meshcentral';
+ const dburl = parent.args.mongodb;
+
+ obj.newDBDumpFile = path.join(backupPath, (dbname + '-mongodump-' + fileSuffix + '.archive'));
+
+ var cmd = buildMongoDumpCommand();
+ cmd += (dburl) ? ' --archive=\"' + obj.newDBDumpFile + '\"' :
+ ' --db=\"' + dbname + '\" --archive=\"' + obj.newDBDumpFile + '\"';
+ parent.debug('backup','Mongodump cmd: ' + cmd);
+ const child_process = require('child_process');
+ const dumpProcess = child_process.exec(
+ cmd,
+ { cwd: parent.parentpath },
+ (error)=> {if (error) {obj.backupStatus |= BACKUPFAIL_DBDUMP; console.error('ERROR: Unable to perform MongoDB backup: ' + error + '\r\n'); obj.createBackupfile(func);}}
+ );
+
+ dumpProcess.on('exit', (code) => {
+ if (code != 0) {console.log(`Mongodump child process exited with code ${code}`); obj.backupStatus |= BACKUPFAIL_DBDUMP;}
+ obj.createBackupfile(func);
+ });
+
+ } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
+ // Perform a MySqlDump backup
+ const newBackupFile = 'mysqldump-' + fileSuffix;
+ obj.newDBDumpFile = path.join(backupPath, newBackupFile + '.sql');
+
+ var cmd = buildSqlDumpCommand();
+ cmd += ' --result-file=\"' + obj.newDBDumpFile + '\"';
+ parent.debug('backup','Maria/MySQLdump cmd: ' + cmd);
+
+ const child_process = require('child_process');
+ const dumpProcess = child_process.exec(
+ cmd,
+ { cwd: parent.parentpath },
+ (error)=> {if (error) {obj.backupStatus |= BACKUPFAIL_DBDUMP; console.error('ERROR: Unable to perform MySQL backup: ' + error + '\r\n'); obj.createBackupfile(func);}}
+ );
+ dumpProcess.on('exit', (code) => {
+ if (code != 0) {console.error(`MySQLdump child process exited with code ${code}`); obj.backupStatus |= BACKUPFAIL_DBDUMP;}
+ obj.createBackupfile(func);
+ });
+
+ } else if (obj.databaseType == DB_SQLITE) {
+ //.db3 suffix to escape escape backupfile glob to exclude the sqlite db files
+ obj.newDBDumpFile = path.join(backupPath, databaseName + '-sqlitedump-' + fileSuffix + '.db3');
+ // do a VACUUM INTO in favor of the backup API to compress the export, see https://www.sqlite.org/backup.html
+ parent.debug('backup','SQLitedump: VACUUM INTO ' + obj.newDBDumpFile);
+ obj.file.exec('VACUUM INTO \'' + obj.newDBDumpFile + '\'', function (err) {
+ if (err) { console.error('SQLite backup error: ' + err); obj.backupStatus |=BACKUPFAIL_DBDUMP;};
+ //always finish/clean up
+ obj.createBackupfile(func);
+ });
+ } else if (obj.databaseType == DB_POSTGRESQL) {
+ // Perform a PostgresDump backup
+ const newBackupFile = databaseName + '-pgdump-' + fileSuffix + '.sql';
+ obj.newDBDumpFile = path.join(backupPath, newBackupFile);
+ let cmd = '"' + parent.config.settings.autobackup.pgdumppath + '"'
+ + ' --dbname=postgresql://' + parent.config.settings.postgres.user + ":" +parent.config.settings.postgres.password
+ + "@" + parent.config.settings.postgres.host + ":" + parent.config.settings.postgres.port + "/" + databaseName
+ + " --file=" + obj.newDBDumpFile;
+ parent.debug('backup','Postgresqldump cmd: ' + cmd);
+ const child_process = require('child_process');
+ const dumpProcess = child_process.exec(
+ cmd,
+ { cwd: dataPath },
+ (error)=> {if (error) {obj.backupStatus |= BACKUPFAIL_DBDUMP; console.log('ERROR: Unable to perform PostgreSQL dump: ' + error.message + '\r\n'); obj.createBackupfile(func);}}
+ );
+ dumpProcess.on('exit', (code) => {
+ if (code != 0) {console.log(`PostgreSQLdump child process exited with code: ` + code); obj.backupStatus |= BACKUPFAIL_DBDUMP;}
+ obj.createBackupfile(func);
+ });
+ } else {
+ // NeDB/Acebase backup, no db dump needed, just make a file backup
+ obj.createBackupfile(func);
+ }
+ } catch (ex) { console.error(ex); parent.addServerWarning( 'Something went wrong during performBackup, check errorlog: ' +ex.message, true); };
+ return 'Starting auto-backup...';
+ };
+
+ obj.createBackupfile = function(func) {
+ parent.debug('backup', 'Entering createBackupfile');
+ let archiver = require('archiver');
+ let archive = null;
+ let zipLevel = Math.min(Math.max(Number(parent.config.settings.autobackup.zipcompression ? parent.config.settings.autobackup.zipcompression : 5),1),9);
+
+ //if password defined, create encrypted zip
+ if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.zippassword == 'string')) {
+ try {
+ //Only register format once, otherwise it triggers an error
+ if (archiver.isRegisteredFormat('zip-encrypted') == false) { archiver.registerFormat('zip-encrypted', require('archiver-zip-encrypted')); }
+ archive = archiver.create('zip-encrypted', { zlib: { level: zipLevel }, encryptionMethod: 'aes256', password: parent.config.settings.autobackup.zippassword });
+ if (func) { func('Creating encrypted ZIP'); }
+ } catch (ex) { // registering encryption failed, do not fall back to non-encrypted, fail backup and skip old backup removal as a precaution to not lose any backups
+ obj.backupStatus |= BACKUPFAIL_ZIPMODULE;
+ if (func) { func('Zipencryptionmodule failed, aborting');}
+ console.error('Zipencryptionmodule failed, aborting');
+ }
+ } else {
+ if (func) { func('Creating a NON-ENCRYPTED ZIP'); }
+ archive = archiver('zip', { zlib: { level: zipLevel } });
+ }
+
+ //original behavior, just a filebackup if dbdump fails : (obj.backupStatus == 0 || obj.backupStatus == BACKUPFAIL_DBDUMP)
+ if (obj.backupStatus == 0) {
+ // Zip the data directory with the dbdump|NeDB files
+ let output = fs.createWriteStream(obj.newAutoBackupFile);
+
+ // Archive finalized and closed
+ output.on('close', function () {
+ if (obj.backupStatus == 0) {
+ let mesg = 'Auto-backup completed: ' + obj.newAutoBackupFile + ', backup-size: ' + ((archive.pointer() / 1048576).toFixed(2)) + "Mb";
+ console.log(mesg);
+ if (func) { func(mesg); };
+ obj.performCloudBackup(obj.newAutoBackupFile, func);
+ obj.removeExpiredBackupfiles(func);
+
+ } else {
+ let mesg = 'Zipbackup failed (' + obj.backupStatus.toString(2).slice(-8) + '), deleting incomplete backup: ' + obj.newAutoBackupFile;
+ if (func) { func(mesg) }
+ else { parent.addServerWarning(mesg, true ) };
+ if (fs.existsSync(obj.newAutoBackupFile)) { fs.unlink(obj.newAutoBackupFile, function (err) { if (err) {console.error('Failed to clean up backupfile: ' + err.message)} }) };
+ };
+ if (obj.databaseType != DB_NEDB) {
+ //remove dump archive file, because zipped and otherwise fills up
+ if (fs.existsSync(obj.newDBDumpFile)) { fs.unlink(obj.newDBDumpFile, function (err) { if (err) {console.error('Failed to clean up dbdump file: ' + err.message) } }) };
+ };
+ obj.performingBackup = false;
+ obj.backupStatus = 0x0;
+ }
+ );
+ output.on('end', function () { });
+ output.on('error', function (err) {
+ if ((obj.backupStatus & BACKUPFAIL_ZIPCREATE) == 0) {
+ console.error('Output error: ' + err.message);
+ if (func) { func('Output error: ' + err.message); };
+ obj.backupStatus |= BACKUPFAIL_ZIPCREATE;
+ archive.abort();
+ };
+ });
+ archive.on('warning', function (err) {
+ //if files added to the archiver object aren't reachable anymore (e.g. sqlite-journal files)
+ //an ENOENT warning is given, but the archiver module has no option to/does not skip/resume
+ //so the backup needs te be aborted as it otherwise leaves an incomplete zip and never 'ends'
+ if ((obj.backupStatus & BACKUPFAIL_ZIPCREATE) == 0) {
+ console.log('Zip warning: ' + err.message);
+ if (func) { func('Zip warning: ' + err.message); };
+ obj.backupStatus |= BACKUPFAIL_ZIPCREATE;
+ archive.abort();
+ };
+ });
+ archive.on('error', function (err) {
+ if ((obj.backupStatus & BACKUPFAIL_ZIPCREATE) == 0) {
+ console.error('Zip error: ' + err.message);
+ if (func) { func('Zip error: ' + err.message); };
+ obj.backupStatus |= BACKUPFAIL_ZIPCREATE;
+ archive.abort();
+ }
+ });
+ archive.pipe(output);
+
+ let globIgnoreFiles;
+ //slice in case exclusion gets pushed
+ globIgnoreFiles = parent.config.settings.autobackup.backupignorefilesglob ? parent.config.settings.autobackup.backupignorefilesglob.slice() : [];
+ if (parent.config.settings.sqlite3) { globIgnoreFiles.push (datapathFoldername + '/' + databaseName + '.sqlite*'); }; //skip sqlite database file, and temp files with ext -journal, -wal & -shm
+ //archiver.glob doesn't seem to use the third param, archivesubdir. Bug?
+ //workaround: go up a dir and add data dir explicitly to keep the zip tidy
+ archive.glob((datapathFoldername + '/**'), {
+ cwd: datapathParentPath,
+ ignore: globIgnoreFiles,
+ skip: (parent.config.settings.autobackup.backupskipfoldersglob ? parent.config.settings.autobackup.backupskipfoldersglob : [])
+ });
+
+ if (parent.config.settings.autobackup.backupwebfolders) {
+ if (parent.webViewsOverridePath) { archive.directory(parent.webViewsOverridePath, 'meshcentral-views'); }
+ if (parent.webPublicOverridePath) { archive.directory(parent.webPublicOverridePath, 'meshcentral-public'); }
+ if (parent.webEmailsOverridePath) { archive.directory(parent.webEmailsOverridePath, 'meshcentral-emails'); }
+ };
+ if (parent.config.settings.autobackup.backupotherfolders) {
+ archive.directory(parent.filespath, 'meshcentral-files');
+ archive.directory(parent.recordpath, 'meshcentral-recordings');
+ };
+ //add dbdump to the root of the zip
+ if (obj.newDBDumpFile != null) archive.file(obj.newDBDumpFile, { name: path.basename(obj.newDBDumpFile) });
+ archive.finalize();
+ } else {
+ //failed somewhere before zipping
+ console.error('Backup failed ('+ obj.backupStatus.toString(2).slice(-8) + ')');
+ if (func) { func('Backup failed ('+ obj.backupStatus.toString(2).slice(-8) + ')') }
+ else {
+ parent.addServerWarning('Backup failed ('+ obj.backupStatus.toString(2).slice(-8) + ')', true);
+ }
+ //Just in case something's there
+ if (fs.existsSync(obj.newDBDumpFile)) { fs.unlink(obj.newDBDumpFile, function (err) { if (err) {console.error('Failed to clean up dbdump file: ' + err.message) } }); };
+ obj.backupStatus = 0x0;
+ obj.performingBackup = false;
+ };
+ };
+
+ // Remove expired backupfiles by filenamedate
+ obj.removeExpiredBackupfiles = function (func) {
+ if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.keeplastdaysbackup == 'number')) {
+ let cutoffDate = new Date();
+ cutoffDate.setDate(cutoffDate.getDate() - parent.config.settings.autobackup.keeplastdaysbackup);
+ fs.readdir(parent.backuppath, function (err, dir) {
+ try {
+ if (err == null) {
+ if (dir.length > 0) {
+ let fileName = parent.config.settings.autobackup.backupname;
+ let checked = 0;
+ let removed = 0;
+ for (var i in dir) {
+ var name = dir[i];
+ parent.debug('backup', "checking file: ", path.join(parent.backuppath, name));
+ if (name.startsWith(fileName) && name.endsWith('.zip')) {
+ var timex = name.substring(fileName.length, name.length - 4).split('-');
+ if (timex.length == 5) {
+ checked++;
+ var fileDate = new Date(parseInt(timex[0]), parseInt(timex[1]) - 1, parseInt(timex[2]), parseInt(timex[3]), parseInt(timex[4]));
+ if (fileDate && (cutoffDate > fileDate)) {
+ console.log("Removing expired backup file: ", path.join(parent.backuppath, name));
+ fs.unlink(path.join(parent.backuppath, name), function (err) { if (err) { console.error(err.message); if (func) {func('Error removing: ' + err.message); } } });
+ removed++;
+ }
+ }
+ else { parent.debug('backup', "file: " + name + " timestamp failure: ", timex); }
+ }
+ }
+ let mesg= 'Checked ' + checked + ' candidates in ' + parent.backuppath + '. Removed ' + removed + ' expired backupfiles using cutoffDate: '+ cutoffDate.toLocaleString('default', { dateStyle: 'short', timeStyle: 'short' });
+ parent.debug (mesg);
+ if (func) { func(mesg); }
+ } else { console.error('No files found in ' + parent.backuppath + '. There should be at least one.')}
+ }
+ else
+ { console.error(err); parent.addServerWarning( 'Reading files in backup directory ' + parent.backuppath + ' failed, check errorlog: ' + err.message, true); }
+ } catch (ex) { console.error(ex); parent.addServerWarning( 'Something went wrong during removeExpiredBackupfiles, check errorlog: ' +ex.message, true); }
+ });
+ }
+ }
+
+ async function webDAVBackup(filename, func) {
+ try {
+ const webDAV = await import ('webdav');
+ const wdConfig = parent.config.settings.autobackup.webdav;
+ const client = webDAV.createClient(wdConfig.url, {
+ username: wdConfig.username,
+ password: wdConfig.password,
+ maxContentLength: Infinity,
+ maxBodyLength: Infinity
+ });
+ if (await client.exists(wdConfig.foldername) === false) {
+ await client.createDirectory(wdConfig.foldername, { recursive: true});
+ } else {
+ // Clean up our WebDAV folder
+ if ((typeof wdConfig.maxfiles == 'number') && (wdConfig.maxfiles > 1)) {
+ const fileName = parent.config.settings.autobackup.backupname;
+ //only files matching our backupfilename
+ let files = await client.getDirectoryContents(wdConfig.foldername, { deep: false, glob: "/**/" + fileName + "*.zip" });
+ const xdateTimeSort = function (a, b) { if (a.xdate > b.xdate) return 1; if (a.xdate < b.xdate) return -1; return 0; }
+ for (const i in files) { files[i].xdate = new Date(files[i].lastmod); }
+ files.sort(xdateTimeSort);
+ while (files.length >= wdConfig.maxfiles) {
+ let delFile = files.shift().filename;
+ await client.deleteFile(delFile);
+ console.log('WebDAV file deleted: ' + delFile); if (func) { func('WebDAV file deleted: ' + delFile); }
+ }
+ }
+ }
+ // Upload to the WebDAV folder
+ const { pipeline } = require('stream/promises');
+ await pipeline(fs.createReadStream(filename), client.createWriteStream( wdConfig.foldername + path.basename(filename)));
+ console.log('WebDAV upload completed: ' + wdConfig.foldername + path.basename(filename)); if (func) { func('WebDAV upload completed: ' + wdConfig.foldername + path.basename(filename)); }
+ }
+ catch(err) {
+ console.error('WebDAV error: ' + err.message); if (func) { func('WebDAV error: ' + err.message);}
+ }
+ }
+
+ // Perform cloud backup
+ obj.performCloudBackup = function (filename, func) {
+ // WebDAV Backup
+ if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.webdav == 'object')) {
+ parent.debug( 'backup', 'Entering WebDAV backup'); if (func) { func('Entering WebDAV backup.'); }
+ webDAVBackup(filename, func);
+ }
+
+ // Google Drive Backup
+ if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.googledrive == 'object')) {
+ parent.debug( 'backup', 'Entering Google Drive backup');
+ obj.Get('GoogleDriveBackup', function (err, docs) {
+ if ((err != null) || (docs.length != 1) || (docs[0].state != 3)) return;
+ if (func) { func('Attempting Google Drive upload...'); }
+ const {google} = require('googleapis');
+ const oAuth2Client = new google.auth.OAuth2(docs[0].clientid, docs[0].clientsecret, "urn:ietf:wg:oauth:2.0:oob");
+ oAuth2Client.on('tokens', function (tokens) { if (tokens.refresh_token) { docs[0].token = tokens.refresh_token; parent.db.Set(docs[0]); } }); // Update the token in the database
+ oAuth2Client.setCredentials(docs[0].token);
+ const drive = google.drive({ version: 'v3', auth: oAuth2Client });
+ const createdTimeSort = function (a, b) { if (a.createdTime > b.createdTime) return 1; if (a.createdTime < b.createdTime) return -1; return 0; }
+
+ // Called once we know our folder id, clean up and upload a backup.
+ var useGoogleDrive = function (folderid) {
+ // List files to see if we need to delete older ones
+ if (typeof parent.config.settings.autobackup.googledrive.maxfiles == 'number') {
+ drive.files.list({
+ q: 'trashed = false and \'' + folderid + '\' in parents',
+ fields: 'nextPageToken, files(id, name, size, createdTime)',
+ }, function (err, res) {
+ if (err) {
+ console.log('GoogleDrive (files.list) error: ' + err);
+ if (func) { func('GoogleDrive (files.list) error: ' + err); }
+ return;
+ }
+ // Delete any old files if more than 10 files are present in the backup folder.
+ res.data.files.sort(createdTimeSort);
+ while (res.data.files.length >= parent.config.settings.autobackup.googledrive.maxfiles) { drive.files.delete({ fileId: res.data.files.shift().id }, function (err, res) { }); }
+ });
+ }
+
+ //console.log('Uploading...');
+ if (func) { func('Uploading to Google Drive...'); }
+
+ // Upload the backup
+ drive.files.create({
+ requestBody: { name: require('path').basename(filename), mimeType: 'text/plain', parents: [folderid] },
+ media: { mimeType: 'application/zip', body: require('fs').createReadStream(filename) },
+ }, function (err, res) {
+ if (err) {
+ console.log('GoogleDrive (files.create) error: ' + err);
+ if (func) { func('GoogleDrive (files.create) error: ' + err); }
+ return;
+ }
+ //console.log('Upload done.');
+ if (func) { func('Google Drive upload completed.'); }
+ });
+ }
+
+ // Fetch the folder name
+ var folderName = 'MeshCentral-Backups';
+ if (typeof parent.config.settings.autobackup.googledrive.foldername == 'string') { folderName = parent.config.settings.autobackup.googledrive.foldername; }
+
+ // Find our backup folder, create one if needed.
+ drive.files.list({
+ q: 'mimeType = \'application/vnd.google-apps.folder\' and name=\'' + folderName + '\' and trashed = false',
+ fields: 'nextPageToken, files(id, name)',
+ }, function (err, res) {
+ if (err) {
+ console.log('GoogleDrive error: ' + err);
+ if (func) { func('GoogleDrive error: ' + err); }
+ return;
+ }
+ if (res.data.files.length == 0) {
+ // Create a folder
+ drive.files.create({ resource: { 'name': folderName, 'mimeType': 'application/vnd.google-apps.folder' }, fields: 'id' }, function (err, file) {
+ if (err) {
+ console.log('GoogleDrive (folder.create) error: ' + err);
+ if (func) { func('GoogleDrive (folder.create) error: ' + err); }
+ return;
+ }
+ useGoogleDrive(file.data.id);
+ });
+ } else { useGoogleDrive(res.data.files[0].id); }
+ });
+ });
+ }
+
+ // S3 Backup
+ if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.s3 == 'object')) {
+ parent.debug( 'backup', 'Entering S3 backup');
+ var s3folderName = 'MeshCentral-Backups';
+ if (typeof parent.config.settings.autobackup.s3.foldername == 'string') { s3folderName = parent.config.settings.autobackup.s3.foldername; }
+ // Construct the config object
+ var accessKey = parent.config.settings.autobackup.s3.accesskey,
+ secretKey = parent.config.settings.autobackup.s3.secretkey,
+ endpoint = parent.config.settings.autobackup.s3.endpoint ? parent.config.settings.autobackup.s3.endpoint : 's3.amazonaws.com',
+ port = parent.config.settings.autobackup.s3.port ? parent.config.settings.autobackup.s3.port : 443,
+ useSsl = parent.config.settings.autobackup.s3.ssl ? parent.config.settings.autobackup.s3.ssl : true,
+ bucketName = parent.config.settings.autobackup.s3.bucketname,
+ pathPrefix = s3folderName,
+ threshold = parent.config.settings.autobackup.s3.maxfiles ? parent.config.settings.autobackup.s3.maxfiles : 0,
+ fileToUpload = filename;
+ // Create a MinIO client
+ const Minio = require('minio');
+ var minioClient = new Minio.Client({
+ endPoint: endpoint,
+ port: port,
+ useSSL: useSsl,
+ accessKey: accessKey,
+ secretKey: secretKey
+ });
+ // List objects in the specified bucket and path prefix
+ var listObjectsPromise = new Promise(function(resolve, reject) {
+ var items = [];
+ var stream = minioClient.listObjects(bucketName, pathPrefix, true);
+ stream.on('data', function(item) {
+ if (!item.name.endsWith('/')) { // Exclude directories
+ items.push(item);
+ }
+ });
+ stream.on('end', function() {
+ resolve(items);
+ });
+ stream.on('error', function(err) {
+ reject(err);
+ });
+ });
+ listObjectsPromise.then(function(objects) {
+ // Count the number of files
+ var fileCount = objects.length;
+ // Return if no files to carry on uploading
+ if (fileCount === 0) { return Promise.resolve(); }
+ // Sort the files by LastModified date (oldest first)
+ objects.sort(function(a, b) { return new Date(a.lastModified) - new Date(b.lastModified); });
+ // Check if the threshold is zero and return if
+ if (threshold === 0) { return Promise.resolve(); }
+ // Check if the number of files exceeds the threshold (maxfiles) is 0
+ if (fileCount >= threshold) {
+ // Calculate how many files need to be deleted to make space for the new file
+ var filesToDelete = fileCount - threshold + 1; // +1 to make space for the new file
+ if (func) { func('Deleting ' + filesToDelete + ' older ' + (filesToDelete == 1 ? 'file' : 'files') + ' from S3 ...'); }
+ // Create an array of promises for deleting files
+ var deletePromises = objects.slice(0, filesToDelete).map(function(fileToDelete) {
+ return new Promise(function(resolve, reject) {
+ minioClient.removeObject(bucketName, fileToDelete.name, function(err) {
+ if (err) {
+ reject(err);
+ } else {
+ if (func) { func('Deleted file: ' + fileToDelete.name + ' from S3'); }
+ resolve();
+ }
+ });
+ });
+ });
+ // Wait for all deletions to complete
+ return Promise.all(deletePromises);
+ } else {
+ return Promise.resolve(); // No deletion needed
+ }
+ }).then(function() {
+ // Determine the upload path by combining the pathPrefix with the filename
+ var fileName = require('path').basename(fileToUpload);
+ var uploadPath = require('path').join(pathPrefix, fileName);
+ // Upload a new file
+ var uploadPromise = new Promise(function(resolve, reject) {
+ if (func) { func('Uploading file ' + uploadPath + ' to S3'); }
+ minioClient.fPutObject(bucketName, uploadPath, fileToUpload, function(err, etag) {
+ if (err) {
+ reject(err);
+ } else {
+ if (func) { func('Uploaded file: ' + uploadPath + ' to S3'); }
+ resolve(etag);
+ }
+ });
+ });
+ return uploadPromise;
+ }).catch(function(error) {
+ if (func) { func('Error managing files in S3: ' + error); }
+ });
+ }
+ }
+
+ // Transfer NeDB data into the current database
+ obj.nedbtodb = function (func) {
+ var nedbDatastore = null;
+ try { nedbDatastore = require('@seald-io/nedb'); } catch (ex) { } // This is the NeDB with Node 23 support.
+ if (nedbDatastore == null) {
+ try { nedbDatastore = require('@yetzt/nedb'); } catch (ex) { } // This is the NeDB with fixed security dependencies.
+ if (nedbDatastore == null) { nedbDatastore = require('nedb'); } // So not to break any existing installations, if the old NeDB is present, use it.
+ }
+
+ var datastoreOptions = { filename: parent.getConfigFilePath('meshcentral.db'), autoload: true };
+
+ // If a DB encryption key is provided, perform database encryption
+ if ((typeof parent.args.dbencryptkey == 'string') && (parent.args.dbencryptkey.length != 0)) {
+ // Hash the database password into a AES256 key and setup encryption and decryption.
+ var nedbKey = parent.crypto.createHash('sha384').update(parent.args.dbencryptkey).digest('raw').slice(0, 32);
+ datastoreOptions.afterSerialization = function (plaintext) {
+ const iv = parent.crypto.randomBytes(16);
+ const aes = parent.crypto.createCipheriv('aes-256-cbc', nedbKey, iv);
+ var ciphertext = aes.update(plaintext);
+ ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
+ return ciphertext.toString('base64');
+ }
+ datastoreOptions.beforeDeserialization = function (ciphertext) {
+ const ciphertextBytes = Buffer.from(ciphertext, 'base64');
+ const iv = ciphertextBytes.slice(0, 16);
+ const data = ciphertextBytes.slice(16);
+ const aes = parent.crypto.createDecipheriv('aes-256-cbc', nedbKey, iv);
+ var plaintextBytes = Buffer.from(aes.update(data));
+ plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
+ return plaintextBytes.toString();
+ }
+ }
+
+ // Setup all NeDB collections
+ var nedbfile = new nedbDatastore(datastoreOptions);
+ var nedbeventsfile = new nedbDatastore({ filename: parent.getConfigFilePath('meshcentral-events.db'), autoload: true, corruptAlertThreshold: 1 });
+ var nedbpowerfile = new nedbDatastore({ filename: parent.getConfigFilePath('meshcentral-power.db'), autoload: true, corruptAlertThreshold: 1 });
+ var nedbserverstatsfile = new nedbDatastore({ filename: parent.getConfigFilePath('meshcentral-stats.db'), autoload: true, corruptAlertThreshold: 1 });
+
+ // Transfered record counts
+ var normalRecordsTransferCount = 0;
+ var eventRecordsTransferCount = 0;
+ var powerRecordsTransferCount = 0;
+ var statsRecordsTransferCount = 0;
+ obj.pendingTransfer = 0;
+
+ // Transfer the data from main database
+ nedbfile.find({}, function (err, docs) {
+ if ((err == null) && (docs.length > 0)) {
+ performTypedRecordDecrypt(docs)
+ for (var i in docs) {
+ obj.pendingTransfer++;
+ normalRecordsTransferCount++;
+ obj.Set(common.unEscapeLinksFieldName(docs[i]), function () { obj.pendingTransfer--; });
+ }
+ }
+
+ // Transfer events
+ nedbeventsfile.find({}, function (err, docs) {
+ if ((err == null) && (docs.length > 0)) {
+ for (var i in docs) {
+ obj.pendingTransfer++;
+ eventRecordsTransferCount++;
+ obj.StoreEvent(docs[i], function () { obj.pendingTransfer--; });
+ }
+ }
+
+ // Transfer power events
+ nedbpowerfile.find({}, function (err, docs) {
+ if ((err == null) && (docs.length > 0)) {
+ for (var i in docs) {
+ obj.pendingTransfer++;
+ powerRecordsTransferCount++;
+ obj.storePowerEvent(docs[i], null, function () { obj.pendingTransfer--; });
+ }
+ }
+
+ // Transfer server stats
+ nedbserverstatsfile.find({}, function (err, docs) {
+ if ((err == null) && (docs.length > 0)) {
+ for (var i in docs) {
+ obj.pendingTransfer++;
+ statsRecordsTransferCount++;
+ obj.SetServerStats(docs[i], function () { obj.pendingTransfer--; });
+ }
+ }
+
+ // Only exit when all the records are stored.
+ setInterval(function () {
+ if (obj.pendingTransfer == 0) { func("Done. " + normalRecordsTransferCount + " record(s), " + eventRecordsTransferCount + " event(s), " + powerRecordsTransferCount + " power change(s), " + statsRecordsTransferCount + " stat(s)."); }
+ }, 200)
+ });
+ });
+ });
+ });
+ }
+
+ function padNumber(number, digits) { return Array(Math.max(digits - String(number).length + 1, 0)).join(0) + number; }
+
+ // Called when a node has changed
+ function dbNodeChange(nodeChange, added) {
+ if (parent.webserver == null) return;
+ common.unEscapeLinksFieldName(nodeChange.fullDocument);
+ const node = performTypedRecordDecrypt([nodeChange.fullDocument])[0];
+ parent.DispatchEvent(['*', node.meshid], obj, { etype: 'node', action: (added ? 'addnode' : 'changenode'), node: parent.webserver.CloneSafeNode(node), nodeid: node._id, domain: node.domain, nolog: 1 });
+ }
+
+ // Called when a device group has changed
+ function dbMeshChange(meshChange, added) {
+ if (parent.webserver == null) return;
+ common.unEscapeLinksFieldName(meshChange.fullDocument);
+ const mesh = performTypedRecordDecrypt([meshChange.fullDocument])[0];
+
+ // Update the mesh object in memory
+ const mmesh = parent.webserver.meshes[mesh._id];
+ if (mmesh != null) {
+ // Update an existing device group
+ for (var i in mesh) { mmesh[i] = mesh[i]; }
+ for (var i in mmesh) { if (mesh[i] == null) { delete mmesh[i]; } }
+ } else {
+ // Device group not present, create it.
+ parent.webserver.meshes[mesh._id] = mesh;
+ }
+
+ // Send the mesh update
+ var mesh2 = Object.assign({}, mesh); // Shallow clone
+ if (mesh2.deleted) { mesh2.action = 'deletemesh'; } else { mesh2.action = (added ? 'createmesh' : 'meshchange'); }
+ mesh2.meshid = mesh2._id;
+ mesh2.nolog = 1;
+ delete mesh2.type;
+ delete mesh2._id;
+ parent.DispatchEvent(['*', mesh2.meshid], obj, parent.webserver.CloneSafeMesh(mesh2));
+ }
+
+ // Called when a user account has changed
+ function dbUserChange(userChange, added) {
+ if (parent.webserver == null) return;
+ common.unEscapeLinksFieldName(userChange.fullDocument);
+ const user = performTypedRecordDecrypt([userChange.fullDocument])[0];
+
+ // Update the user object in memory
+ const muser = parent.webserver.users[user._id];
+ if (muser != null) {
+ // Update an existing user
+ for (var i in user) { muser[i] = user[i]; }
+ for (var i in muser) { if (user[i] == null) { delete muser[i]; } }
+ } else {
+ // User not present, create it.
+ parent.webserver.users[user._id] = user;
+ }
+
+ // Send the user update
+ var targets = ['*', 'server-users', user._id];
+ if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
+ parent.DispatchEvent(targets, obj, { etype: 'user', userid: user._id, username: user.name, account: parent.webserver.CloneSafeUser(user), action: (added ? 'accountcreate' : 'accountchange'), domain: user.domain, nolog: 1 });
+ }
+
+ // Called when a user group has changed
+ function dbUGrpChange(ugrpChange, added) {
+ if (parent.webserver == null) return;
+ common.unEscapeLinksFieldName(ugrpChange.fullDocument);
+ const usergroup = ugrpChange.fullDocument;
+
+ // Update the user group object in memory
+ const uusergroup = parent.webserver.userGroups[usergroup._id];
+ if (uusergroup != null) {
+ // Update an existing user group
+ for (var i in usergroup) { uusergroup[i] = usergroup[i]; }
+ for (var i in uusergroup) { if (usergroup[i] == null) { delete uusergroup[i]; } }
+ } else {
+ // Usergroup not present, create it.
+ parent.webserver.userGroups[usergroup._id] = usergroup;
+ }
+
+ // Send the user group update
+ var usergroup2 = Object.assign({}, usergroup); // Shallow clone
+ usergroup2.action = (added ? 'createusergroup' : 'usergroupchange');
+ usergroup2.ugrpid = usergroup2._id;
+ usergroup2.nolog = 1;
+ delete usergroup2.type;
+ delete usergroup2._id;
+ parent.DispatchEvent(['*', usergroup2.ugrpid], obj, usergroup2);
+ }
+
+ function dbMergeSqlArray(arr) {
+ var x = '';
+ for (var i in arr) { if (x != '') { x += ','; } x += '\'' + arr[i] + '\''; }
+ return x;
+ }
+
+ return obj;
+};
diff --git a/meshagent.js b/meshagent.js
index 5ea181d97f..3c9e16fc3e 100644
--- a/meshagent.js
+++ b/meshagent.js
@@ -1,2184 +1,2232 @@
-/**
-* @description MeshCentral MeshAgent communication module
-* @author Ylian Saint-Hilaire & Bryan Roe
-* @copyright Intel Corporation 2018-2022
-* @license Apache-2.0
-* @version v0.0.1
-*/
-
-/*xjslint node: true */
-/*xjslint plusplus: true */
-/*xjslint maxlen: 256 */
-/*jshint node: true */
-/*jshint strict: false */
-/*jshint esversion: 6 */
-"use strict";
-
-// Construct a MeshAgent object, called upon connection
-module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
- const forge = parent.parent.certificateOperations.forge;
- const common = parent.parent.common;
- parent.agentStats.createMeshAgentCount++;
- parent.parent.debug('agent', 'New agent at ' + req.clientIp + ':' + ws._socket.remotePort);
-
- var obj = {};
- obj.domain = domain;
- obj.authenticated = 0;
- obj.receivedCommands = 0;
- obj.agentCoreCheck = 0;
- obj.remoteaddr = req.clientIp;
- obj.remoteaddrport = obj.remoteaddr + ':' + ws._socket.remotePort;
- obj.nonce = parent.crypto.randomBytes(48).toString('binary');
- //ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive, 4 minutes
- if (args.agentidletimeout != 0) { ws._socket.setTimeout(args.agentidletimeout, function () { obj.close(1); }); } // Inactivity timeout of 2:30 minutes, by default agent will WebSocket ping every 2 minutes and server will pong back.
- //obj.nodeid = null;
- //obj.meshid = null;
- //obj.dbNodeKey = null;
- //obj.dbMeshKey = null;
- //obj.connectTime = null;
- //obj.agentInfo = null;
-
- ws._socket.bytesReadEx = 0;
- ws._socket.bytesWrittenEx = 0;
-
- // Perform data accounting
- function dataAccounting() {
- parent.trafficStats.AgentCtrlIn += (ws._socket.bytesRead - ws._socket.bytesReadEx);
- parent.trafficStats.AgentCtrlOut += (ws._socket.bytesWritten - ws._socket.bytesWrittenEx);
- ws._socket.bytesReadEx = ws._socket.bytesRead;
- ws._socket.bytesWrittenEx = ws._socket.bytesWritten;
- }
-
- // Send a message to the mesh agent
- obj.send = function (data, func) { try { if (typeof data == 'string') { ws.send(Buffer.from(data), func); } else { ws.send(data, func); } } catch (e) { } };
- obj.sendBinary = function (data, func) { try { if (typeof data == 'string') { ws.send(Buffer.from(data, 'binary'), func); } else { ws.send(data, func); } } catch (e) { } };
-
- // Disconnect this agent
- obj.close = function (arg) {
- dataAccounting();
-
- if ((arg == 1) || (arg == null)) { try { ws.close(); if (obj.nodeid != null) { parent.parent.debug('agent', 'Soft disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ')'); } } catch (e) { console.log(e); } } // Soft close, close the websocket
- if (arg == 2) {
- try {
- if (ws._socket._parent != null)
- ws._socket._parent.end();
- else
- ws._socket.end();
-
- if (obj.nodeid != null) {
- parent.parent.debug('agent', 'Hard disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ')');
- }
- } catch (e) { console.log(e); }
- }
- // If arg == 2, hard close, close the TCP socket
- // If arg == 3, don't communicate with this agent anymore, but don't disconnect (Duplicate agent).
-
- // Stop any current self-share
- if (obj.guestSharing === true) { removeGuestSharing(); }
-
- // Remove this agent from the webserver list
- if (parent.wsagents[obj.dbNodeKey] == obj) {
- delete parent.wsagents[obj.dbNodeKey];
- parent.parent.ClearConnectivityState(obj.dbMeshKey, obj.dbNodeKey, 1, null, { remoteaddrport: obj.remoteaddrport, name: obj.name });
- }
-
- // Remove this agent from the list of agents with bad web certificates
- if (obj.badWebCert) { delete parent.wsagentsWithBadWebCerts[obj.badWebCert]; }
-
- // Get the current mesh
- const mesh = parent.meshes[obj.dbMeshKey];
-
- // If this is a temporary or recovery agent, or all devices in this group are temporary, remove the agent (0x20 = Temporary, 0x40 = Recovery)
- if (((obj.agentInfo) && (obj.agentInfo.capabilities) && ((obj.agentInfo.capabilities & 0x20) || (obj.agentInfo.capabilities & 0x40))) || ((mesh) && (mesh.flags) && (mesh.flags & 1))) {
- // Delete this node including network interface information and events
- db.Remove(obj.dbNodeKey); // Remove node with that id
- db.Remove('if' + obj.dbNodeKey); // Remove interface information
- db.Remove('nt' + obj.dbNodeKey); // Remove notes
- db.Remove('lc' + obj.dbNodeKey); // Remove last connect time
- db.Remove('si' + obj.dbNodeKey); // Remove system information
- db.Remove('al' + obj.dbNodeKey); // Remove error log last time
- if (db.RemoveSMBIOS) { db.RemoveSMBIOS(obj.dbNodeKey); } // Remove SMBios data
- db.RemoveAllNodeEvents(obj.dbNodeKey); // Remove all events for this node
- db.removeAllPowerEventsForNode(obj.dbNodeKey); // Remove all power events for this node
-
- // Event node deletion
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, { etype: 'node', action: 'removenode', nodeid: obj.dbNodeKey, domain: domain.id, nolog: 1 });
-
- // Disconnect all connections if needed
- const state = parent.parent.GetConnectivityState(obj.dbNodeKey);
- if ((state != null) && (state.connectivity != null)) {
- if ((state.connectivity & 1) != 0) { parent.wsagents[obj.dbNodeKey].close(); } // Disconnect mesh agent
- if ((state.connectivity & 2) != 0) { parent.parent.mpsserver.closeAllForNode(obj.dbNodeKey); } // Disconnect CIRA connection
- }
- }
-
- // Set this agent as no longer authenticated
- obj.authenticated = -1;
-
- // If we where updating the agent using native method, clean that up.
- if (obj.agentUpdate != null) {
- if (obj.agentUpdate.fd) { try { parent.fs.close(obj.agentUpdate.fd); } catch (ex) { } }
- parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete
- delete obj.agentUpdate.buf;
- delete obj.agentUpdate;
- }
-
- // If we where updating the agent meshcore method, clean that up.
- if (obj.agentCoreUpdateTaskId != null) {
- parent.parent.taskLimiter.completed(obj.agentCoreUpdateTaskId);
- delete obj.agentCoreUpdateTaskId;
- }
-
- // Perform timer cleanup
- if (obj.pingtimer) { clearInterval(obj.pingtimer); delete obj.pingtimer; }
- if (obj.pongtimer) { clearInterval(obj.pongtimer); delete obj.pongtimer; }
-
- // Perform aggressive cleanup
- delete obj.name;
- delete obj.nonce;
- delete obj.nodeid;
- delete obj.unauth;
- delete obj.remoteaddr;
- delete obj.remoteaddrport;
- delete obj.meshid;
- delete obj.connectTime;
- delete obj.agentInfo;
- delete obj.agentExeInfo;
- ws.removeAllListeners(['message', 'close', 'error']);
- };
-
- // When data is received from the mesh agent web socket
- ws.on('message', function (msg) {
- dataAccounting();
- if (msg.length < 2) return;
- if (typeof msg == 'object') { msg = msg.toString('binary'); } // TODO: Could change this entire method to use Buffer instead of binary string
- if (obj.authenticated == 2) { // We are authenticated
- if ((obj.agentUpdate == null) && (msg.charCodeAt(0) == 123)) { processAgentData(msg); } // Only process JSON messages if meshagent update is not in progress
- if (msg.length < 2) return;
- const cmdid = common.ReadShort(msg, 0);
- if (cmdid == 11) { // MeshCommand_CoreModuleHash
- if (msg.length == 4) { ChangeAgentCoreInfo({ 'caps': 0 }); } // If the agent indicated that no core is running, clear the core information string.
- // Mesh core hash, sent by agent with the hash of the current mesh core.
-
- // If we are performing an agent update, don't update the core.
- if (obj.agentUpdate != null) { return; }
-
- // If we are using a custom core, don't try to update it.
- if (obj.agentCoreCheck == 1000) {
- obj.sendBinary(common.ShortToStr(16) + common.ShortToStr(0)); // MeshCommand_CoreOk. Indicates to the agent that the core is ok. Start it if it's not already started.
- agentCoreIsStable();
- return;
- }
-
- // Get the current meshcore hash
- const agentMeshCoreHash = (msg.length == 52) ? msg.substring(4, 52) : null;
-
- // If the agent indicates this is a custom core, we are done.
- if ((agentMeshCoreHash != null) && (agentMeshCoreHash == '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0')) {
- obj.agentCoreCheck = 0;
- obj.sendBinary(common.ShortToStr(16) + common.ShortToStr(0)); // MeshCommand_CoreOk. Indicates to the agent that the core is ok. Start it if it's not already started.
- agentCoreIsStable();
- return;
- }
-
- // We need to check if the core is current. Figure out what core we need.
- var corename = null;
- if ((obj.agentInfo != null) && (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId] != null)) {
- if ((obj.agentCoreCheck == 1001) || (obj.agentCoreUpdate == true)) {
- // If the user asked, use the recovery core.
- corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].rcore;
- } else if (obj.agentCoreCheck == 1011) {
- // If the user asked, use the tiny core.
- corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].tcore;
- } else if (obj.agentInfo.capabilities & 0x40) {
- // If this is a recovery agent, use the agent recovery core.
- corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].arcore;
- } else {
- // This is the normal core for this agent type.
- corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].core;
- }
- }
-
- // If we have a core, use it.
- if (corename != null) {
- const meshcorehash = parent.parent.defaultMeshCoresHash[corename];
- if (agentMeshCoreHash != meshcorehash) {
- if ((obj.agentCoreCheck < 5) || (obj.agentCoreCheck == 1001) || (obj.agentCoreCheck == 1011) || (obj.agentCoreUpdate == true)) {
- if (meshcorehash == null) {
- // Clear the core
- obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0)); // MeshCommand_CoreModule, ask mesh agent to clear the core
- parent.agentStats.clearingCoreCount++;
- parent.parent.debug('agent', "Clearing core");
- } else {
- // Setup task limiter options, this system limits how many tasks can run at the same time to spread the server load.
- var taskLimiterOptions = { hash: meshcorehash, core: parent.parent.defaultMeshCores[corename], name: corename };
-
- // If the agent supports compression, sent the core compressed.
- if ((obj.agentInfo.capabilities & 0x100) && (parent.parent.defaultMeshCoresDeflate[corename])) {
- args.core = parent.parent.defaultMeshCoresDeflate[corename];
- }
-
- // Update new core with task limiting so not to flood the server. This is a high priority task.
- obj.agentCoreUpdatePending = true;
- parent.parent.taskLimiter.launch(function (argument, taskid, taskLimiterQueue) {
- if (obj.authenticated == 2) {
- // Send the updated core.
- delete obj.agentCoreUpdatePending;
- obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0) + argument.hash + argument.core.toString('binary'), function () { parent.parent.taskLimiter.completed(taskid); }); // MeshCommand_CoreModule, start core update
- parent.agentStats.updatingCoreCount++;
- parent.parent.debug('agent', "Updating core " + argument.name);
- } else {
- // This agent is probably disconnected, nothing to do.
- parent.parent.taskLimiter.completed(taskid);
- }
- }, taskLimiterOptions, 0);
- }
- obj.agentCoreCheck++;
- }
- } else {
- obj.agentCoreCheck = 0;
- obj.sendBinary(common.ShortToStr(16) + common.ShortToStr(0)); // MeshCommand_CoreOk. Indicates to the agent that the core is ok. Start it if it's not already started.
- agentCoreIsStable(); // No updates needed, agent is ready to go.
- }
- }
-
- /*
- // TODO: Check if we have a mesh specific core. If so, use that.
- var agentMeshCoreHash = null;
- if (msg.length == 52) { agentMeshCoreHash = msg.substring(4, 52); }
- if ((agentMeshCoreHash != parent.parent.defaultMeshCoreHash) && (agentMeshCoreHash != parent.parent.defaultMeshCoreNoMeiHash)) {
- if (obj.agentCoreCheck < 5) { // This check is in place to avoid a looping core update.
- if (parent.parent.defaultMeshCoreHash == null) {
- // Update no core
- obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0)); // Command 10, ask mesh agent to clear the core
- } else {
- // Update new core
- if ((parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId] != null) && (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].amt == true)) {
- obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0) + parent.parent.defaultMeshCoreHash + parent.parent.defaultMeshCore); // Command 10, ask mesh agent to set the core (with MEI support)
- } else {
- obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0) + parent.parent.defaultMeshCoreNoMeiHash + parent.parent.defaultMeshCoreNoMei); // Command 10, ask mesh agent to set the core (No MEI)
- }
- }
- obj.agentCoreCheck++;
- }
- } else {
- obj.agentCoreCheck = 0;
- }
- */
- }
- else if (cmdid == 12) { // MeshCommand_AgentHash
- if ((msg.length == 52) && (obj.agentExeInfo != null) && (obj.agentExeInfo.update == true)) {
- const agenthash = msg.substring(4);
- const agentUpdateMethod = compareAgentBinaryHash(obj.agentExeInfo, agenthash);
- if (agentUpdateMethod === 2) { // Use meshcore agent update system
- // Send the recovery core to the agent, if the agent is capable of running one
- if (((obj.agentInfo.capabilities & 16) != 0) && (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].core != null)) {
- parent.agentStats.agentMeshCoreBinaryUpdate++;
- obj.agentCoreUpdate = true;
- obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0)); // Ask to clear the core
- obj.sendBinary(common.ShortToStr(11) + common.ShortToStr(0)); // Ask for meshcore hash
- }
- } else if (agentUpdateMethod === 1) { // Use native agent update system
- // Mesh agent update required, do it using task limiter so not to flood the network. Medium priority task.
- parent.parent.taskLimiter.launch(function (argument, taskid, taskLimiterQueue) {
- if (obj.authenticated != 2) { parent.parent.taskLimiter.completed(taskid); return; } // If agent disconnection, complete and exit now.
- if (obj.nodeid != null) { parent.parent.debug('agent', "Agent update required, NodeID=0x" + obj.nodeid.substring(0, 16) + ', ' + obj.agentExeInfo.desc); }
- parent.agentStats.agentBinaryUpdate++;
- if ((obj.agentExeInfo.data == null) && (((obj.agentInfo.capabilities & 0x100) == 0) || (obj.agentExeInfo.zdata == null))) {
- // Read the agent from disk
- parent.fs.open(obj.agentExeInfo.path, 'r', function (err, fd) {
- if (obj.agentExeInfo == null) return; // Agent disconnected during this call.
- if (err) { parent.parent.debug('agentupdate', "ERROR: " + err); return console.error(err); }
- obj.agentUpdate = { ptr: 0, buf: Buffer.alloc(parent.parent.agentUpdateBlockSize + 4), fd: fd, taskid: taskid };
-
- // MeshCommand_CoreModule, ask mesh agent to clear the core.
- // The new core will only be sent after the agent updates.
- obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0));
-
- // We got the agent file open on the server side, tell the agent we are sending an update ending with the SHA384 hash of the result
- //console.log("Agent update file open.");
- obj.sendBinary(common.ShortToStr(13) + common.ShortToStr(0)); // Command 13, start mesh agent download
-
- // Send the first mesh agent update data block
- obj.agentUpdate.buf[0] = 0;
- obj.agentUpdate.buf[1] = 14;
- obj.agentUpdate.buf[2] = 0;
- obj.agentUpdate.buf[3] = 1;
- parent.fs.read(obj.agentUpdate.fd, obj.agentUpdate.buf, 4, parent.parent.agentUpdateBlockSize, obj.agentUpdate.ptr, function (err, bytesRead, buffer) {
- if (obj.agentUpdate == null) return;
- if ((err != null) || (bytesRead == 0)) {
- // Error reading the agent file, stop here.
- try { parent.fs.close(obj.agentUpdate.fd); } catch (ex) { }
- parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete
- parent.parent.debug('agentupdate', "ERROR: Unable to read first block of agent binary from disk.");
- delete obj.agentUpdate.buf;
- delete obj.agentUpdate;
- } else {
- // Send the first block to the agent
- obj.agentUpdate.ptr += bytesRead;
- parent.parent.debug('agentupdate', "Sent first block of " + bytesRead + " bytes from disk.");
- obj.sendBinary(obj.agentUpdate.buf); // Command 14, mesh agent first data block
- }
- });
- });
- } else {
- // Send the agent from RAM
- obj.agentUpdate = { ptr: 0, buf: Buffer.alloc(parent.parent.agentUpdateBlockSize + 4), taskid: taskid };
-
- // MeshCommand_CoreModule, ask mesh agent to clear the core.
- // The new core will only be sent after the agent updates.
- obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0));
-
- // We got the agent file open on the server side, tell the agent we are sending an update ending with the SHA384 hash of the result
- obj.sendBinary(common.ShortToStr(13) + common.ShortToStr(0)); // Command 13, start mesh agent download
-
- // Send the first mesh agent update data block
- obj.agentUpdate.buf[0] = 0;
- obj.agentUpdate.buf[1] = 14;
- obj.agentUpdate.buf[2] = 0;
- obj.agentUpdate.buf[3] = 1;
-
- // If agent supports compression, send the compressed agent if possible.
- if ((obj.agentInfo.capabilities & 0x100) && (obj.agentExeInfo.zdata != null)) {
- // Send compressed data
- obj.agentUpdate.agentUpdateData = obj.agentExeInfo.zdata;
- obj.agentUpdate.agentUpdateHash = obj.agentExeInfo.zhash;
- } else {
- // Send uncompressed data
- obj.agentUpdate.agentUpdateData = obj.agentExeInfo.data;
- obj.agentUpdate.agentUpdateHash = obj.agentExeInfo.hash;
- }
-
- const len = Math.min(parent.parent.agentUpdateBlockSize, obj.agentUpdate.agentUpdateData.length - obj.agentUpdate.ptr);
- if (len > 0) {
- // Send the first block
- obj.agentUpdate.agentUpdateData.copy(obj.agentUpdate.buf, 4, obj.agentUpdate.ptr, obj.agentUpdate.ptr + len);
- obj.agentUpdate.ptr += len;
- obj.sendBinary(obj.agentUpdate.buf); // Command 14, mesh agent first data block
- parent.parent.debug('agentupdate', "Sent first block of " + len + " bytes from RAM.");
- } else {
- // Error
- parent.parent.debug('agentupdate', "ERROR: Len of " + len + " is invalid.");
- parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete
- delete obj.agentUpdate.buf;
- delete obj.agentUpdate;
- }
- }
- }, null, 1);
-
- } else {
- // Check the mesh core, if the agent is capable of running one
- if (((obj.agentInfo.capabilities & 16) != 0) && (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].core != null)) {
- obj.sendBinary(common.ShortToStr(11) + common.ShortToStr(0)); // Command 11, ask for mesh core hash.
- }
- }
- }
- }
- else if (cmdid == 14) { // MeshCommand_AgentBinaryBlock
- if ((msg.length == 4) && (obj.agentUpdate != null)) {
- const status = common.ReadShort(msg, 2);
- if (status == 1) {
- if (obj.agentExeInfo.data == null) {
- // Read the agent from disk
- parent.fs.read(obj.agentUpdate.fd, obj.agentUpdate.buf, 4, parent.parent.agentUpdateBlockSize, obj.agentUpdate.ptr, function (err, bytesRead, buffer) {
- if ((obj.agentExeInfo == null) || (obj.agentUpdate == null)) return; // Agent disconnected during this async call.
- if ((err != null) || (bytesRead < 0)) {
- // Error reading the agent file, stop here.
- parent.parent.debug('agentupdate', "ERROR: Unable to read agent #" + obj.agentExeInfo.id + " binary from disk.");
- try { parent.fs.close(obj.agentUpdate.fd); } catch (ex) { }
- parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete
- delete obj.agentUpdate.buf;
- delete obj.agentUpdate;
- } else {
- // Send the next block to the agent
- parent.parent.debug('agentupdate', "Sending disk agent #" + obj.agentExeInfo.id + " block, ptr=" + obj.agentUpdate.ptr + ", len=" + bytesRead + ".");
- obj.agentUpdate.ptr += bytesRead;
- if (bytesRead == parent.parent.agentUpdateBlockSize) { obj.sendBinary(obj.agentUpdate.buf); } else { obj.sendBinary(obj.agentUpdate.buf.slice(0, bytesRead + 4)); } // Command 14, mesh agent next data block
- if ((bytesRead < parent.parent.agentUpdateBlockSize) || (obj.agentUpdate.ptr == obj.agentExeInfo.size)) {
- parent.parent.debug('agentupdate', "Completed agent #" + obj.agentExeInfo.id + " update from disk, ptr=" + obj.agentUpdate.ptr + ".");
- obj.sendBinary(common.ShortToStr(13) + common.ShortToStr(0) + obj.agentExeInfo.hash); // Command 13, end mesh agent download, send agent SHA384 hash
- try { parent.fs.close(obj.agentUpdate.fd); } catch (ex) { }
- parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete
- delete obj.agentUpdate.buf;
- delete obj.agentUpdate;
- }
- }
- });
- } else {
- // Send the agent from RAM
- const len = Math.min(parent.parent.agentUpdateBlockSize, obj.agentUpdate.agentUpdateData.length - obj.agentUpdate.ptr);
- if (len > 0) {
- obj.agentUpdate.agentUpdateData.copy(obj.agentUpdate.buf, 4, obj.agentUpdate.ptr, obj.agentUpdate.ptr + len);
- if (len == parent.parent.agentUpdateBlockSize) { obj.sendBinary(obj.agentUpdate.buf); } else { obj.sendBinary(obj.agentUpdate.buf.slice(0, len + 4)); } // Command 14, mesh agent next data block
- parent.parent.debug('agentupdate', "Sending RAM agent #" + obj.agentExeInfo.id + " block, ptr=" + obj.agentUpdate.ptr + ", len=" + len + ".");
- obj.agentUpdate.ptr += len;
- }
-
- if (obj.agentUpdate.ptr == obj.agentUpdate.agentUpdateData.length) {
- parent.parent.debug('agentupdate', "Completed agent #" + obj.agentExeInfo.id + " update from RAM, ptr=" + obj.agentUpdate.ptr + ".");
- obj.sendBinary(common.ShortToStr(13) + common.ShortToStr(0) + obj.agentUpdate.agentUpdateHash); // Command 13, end mesh agent download, send agent SHA384 hash
- parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete
- delete obj.agentUpdate.buf;
- delete obj.agentUpdate;
- }
- }
- }
- }
- }
- else if (cmdid == 15) { // MeshCommand_AgentTag
- var tag = msg.substring(2);
- while (tag.charCodeAt(tag.length - 1) == 0) { tag = tag.substring(0, tag.length - 1); } // Remove end-of-line zeros.
- ChangeAgentTag(tag);
- }
- } else if (obj.authenticated < 2) { // We are not authenticated
- // Check if this is a un-authenticated JSON
- if (msg.charCodeAt(0) == 123) {
- var str = msg.toString('utf8'), command = null;
- if (str[0] == '{') {
- try { command = JSON.parse(str); } catch (ex) { } // If the command can't be parsed, ignore it.
- if ((command != null) && (command.action === 'agentName') && (typeof command.value == 'string') && (command.value.length > 0) && (command.value.length < 256)) { obj.agentName = command.value; }
- }
- return;
- }
- const cmd = common.ReadShort(msg, 0);
- if (cmd == 1) {
- // Agent authentication request
- if ((msg.length != 98) || ((obj.receivedCommands & 1) != 0)) return;
- obj.receivedCommands += 1; // Agent can't send the same command twice on the same connection ever. Block DOS attack path.
-
- if (isIgnoreHashCheck()) {
- // Send the agent web hash back to the agent
- // Send 384 bits SHA384 hash of TLS cert + 384 bits nonce
- obj.sendBinary(common.ShortToStr(1) + msg.substring(2, 50) + obj.nonce); // Command 1, hash + nonce. Use the web hash given by the agent.
- } else {
- // Check that the server hash matches our own web certificate hash (SHA384)
- obj.agentSeenCerthash = msg.substring(2, 50);
- if ((getWebCertHash(domain) != obj.agentSeenCerthash) && (getWebCertFullHash(domain) != obj.agentSeenCerthash) && (parent.defaultWebCertificateHash != obj.agentSeenCerthash) && (parent.defaultWebCertificateFullHash != obj.agentSeenCerthash)) {
- if (parent.parent.supportsProxyCertificatesRequest !== false) {
- obj.badWebCert = Buffer.from(parent.crypto.randomBytes(16), 'binary').toString('base64');
- parent.wsagentsWithBadWebCerts[obj.badWebCert] = obj; // Add this agent to the list of of agents with bad web certificates.
- parent.parent.updateProxyCertificates(false);
- }
- parent.agentStats.agentBadWebCertHashCount++;
- parent.setAgentIssue(obj, "BadWebCertHash: " + Buffer.from(msg.substring(2, 50), 'binary').toString('hex'));
- parent.parent.debug('agent', 'Agent bad web cert hash (Agent:' + (Buffer.from(msg.substring(2, 50), 'binary').toString('hex').substring(0, 10)) + ' != Server:' + (Buffer.from(getWebCertHash(domain), 'binary').toString('hex').substring(0, 10)) + ' or ' + (Buffer.from(getWebCertFullHash(domain), 'binary').toString('hex').substring(0, 10)) + '), holding connection (' + obj.remoteaddrport + ').');
- parent.parent.debug('agent', 'Agent reported web cert hash:' + (Buffer.from(msg.substring(2, 50), 'binary').toString('hex')) + '.');
- console.log('Agent bad web cert hash (Agent:' + (Buffer.from(msg.substring(2, 50), 'binary').toString('hex').substring(0, 10)) + ' != Server:' + (Buffer.from(getWebCertHash(domain), 'binary').toString('hex').substring(0, 10)) + ' or ' + (Buffer.from(getWebCertFullHash(domain), 'binary').toString('hex').substring(0, 10)) + '), holding connection (' + obj.remoteaddrport + ').');
- console.log('Agent reported web cert hash:' + (Buffer.from(msg.substring(2, 50), 'binary').toString('hex')) + '.');
- delete obj.agentSeenCerthash;
- return;
- } else {
- // The hash matched one of the acceptable values, send the agent web hash back to the agent
- // Send 384 bits SHA384 hash of TLS cert + 384 bits nonce
- // Command 1, hash + nonce. Use the web hash given by the agent.
- obj.sendBinary(common.ShortToStr(1) + obj.agentSeenCerthash + obj.nonce);
- }
- }
-
- // Use our server private key to sign the ServerHash + AgentNonce + ServerNonce
- obj.agentnonce = msg.substring(50, 98);
-
- // Check if we got the agent auth confirmation
- if ((obj.receivedCommands & 8) == 0) {
- // If we did not get an indication that the agent already validated this server, send the server signature.
- if (obj.useSwarmCert == true) {
- // Perform the hash signature using older swarm server certificate
- parent.parent.certificateOperations.acceleratorPerformSignature(1, msg.substring(2) + obj.nonce, null, function (tag, signature) {
- // Send back our certificate + signature
- obj.sendBinary(common.ShortToStr(2) + common.ShortToStr(parent.swarmCertificateAsn1.length) + parent.swarmCertificateAsn1 + signature); // Command 2, certificate + signature
- });
- } else {
- // Perform the hash signature using the server agent certificate
- parent.parent.certificateOperations.acceleratorPerformSignature(0, msg.substring(2) + obj.nonce, null, function (tag, signature) {
- // Send back our certificate + signature
- obj.sendBinary(common.ShortToStr(2) + common.ShortToStr(parent.agentCertificateAsn1.length) + parent.agentCertificateAsn1 + signature); // Command 2, certificate + signature
- });
- }
- }
-
- // Check the agent signature if we can
- if (obj.unauthsign != null) {
- if (processAgentSignature(obj.unauthsign) == false) {
- parent.agentStats.agentBadSignature1Count++;
- parent.setAgentIssue(obj, "BadSignature1");
- parent.parent.debug('agent', 'Agent connected with bad signature, holding connection (' + obj.remoteaddrport + ').');
- console.log('Agent connected with bad signature, holding connection (' + obj.remoteaddrport + ').'); return;
- } else { completeAgentConnection(); }
- }
- }
- else if (cmd == 2) {
- // Agent certificate
- if ((msg.length < 4) || ((obj.receivedCommands & 2) != 0)) return;
- obj.receivedCommands += 2; // Agent can't send the same command twice on the same connection ever. Block DOS attack path.
-
- // Decode the certificate
- const certlen = common.ReadShort(msg, 2);
- obj.unauth = {};
- try { obj.unauth.nodeid = Buffer.from(forge.pki.getPublicKeyFingerprint(forge.pki.certificateFromAsn1(forge.asn1.fromDer(msg.substring(4, 4 + certlen))).publicKey, { md: forge.md.sha384.create() }).data, 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); } catch (ex) { console.log(ex); parent.parent.debug('agent', ex); return; }
- obj.unauth.nodeCertPem = '-----BEGIN CERTIFICATE-----\r\n' + Buffer.from(msg.substring(4, 4 + certlen), 'binary').toString('base64') + '\r\n-----END CERTIFICATE-----';
-
- // Check the agent signature if we can
- if (obj.agentnonce == null) { obj.unauthsign = msg.substring(4 + certlen); } else {
- if (processAgentSignature(msg.substring(4 + certlen)) == false) {
- parent.agentStats.agentBadSignature2Count++;
- parent.setAgentIssue(obj, "BadSignature2");
- parent.parent.debug('agent', 'Agent connected with bad signature, holding connection (' + obj.remoteaddrport + ').');
- console.log('Agent connected with bad signature, holding connection (' + obj.remoteaddrport + ').'); return;
- }
- }
- completeAgentConnection();
- }
- else if (cmd == 3) {
- // Agent meshid
- if ((msg.length < 70) || ((obj.receivedCommands & 4) != 0)) return;
- obj.receivedCommands += 4; // Agent can't send the same command twice on the same connection ever. Block DOS attack path.
-
- // Set the meshid
- obj.agentInfo = {};
- obj.agentInfo.infoVersion = common.ReadInt(msg, 2);
- obj.agentInfo.agentId = common.ReadInt(msg, 6);
- obj.agentInfo.agentVersion = common.ReadInt(msg, 10);
- obj.agentInfo.platformType = common.ReadInt(msg, 14);
- if (obj.agentInfo.platformType > 8 || obj.agentInfo.platformType < 1) { obj.agentInfo.platformType = 1; }
- if (msg.substring(50, 66) == '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0') {
- obj.meshid = Buffer.from(msg.substring(18, 50), 'binary').toString('hex'); // Older HEX MeshID
- } else {
- obj.meshid = Buffer.from(msg.substring(18, 66), 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); // New Base64 MeshID
- }
- //console.log('MeshID', obj.meshid);
- obj.agentInfo.capabilities = common.ReadInt(msg, 66);
- if (msg.length > 70) {
- const computerNameLen = common.ReadShort(msg, 70);
- obj.agentInfo.computerName = Buffer.from(msg.substring(72, 72 + computerNameLen), 'binary').toString('utf8');
- //console.log('computerName', msg.length, computerNameLen, obj.agentInfo.computerName);
- } else {
- obj.agentInfo.computerName = '';
- //console.log('computerName-none');
- }
-
- obj.dbMeshKey = 'mesh/' + domain.id + '/' + obj.meshid;
- completeAgentConnection();
- } else if (cmd == 4) {
- if ((msg.length < 2) || ((obj.receivedCommands & 8) != 0)) return;
- obj.receivedCommands += 8; // Agent can't send the same command twice on the same connection ever. Block DOS attack path.
- // Agent already authenticated the server, wants to skip the server signature - which is great for server performance.
- } else if (cmd == 5) {
- // ServerID. Agent is telling us what serverid it expects. Useful if we have many server certificates.
- if ((msg.substring(2, 34) == parent.swarmCertificateHash256) || (msg.substring(2, 50) == parent.swarmCertificateHash384)) { obj.useSwarmCert = true; }
- } else if (cmd == 30) {
- // Agent Commit Date. This is future proofing. Can be used to change server behavior depending on the date range of the agent.
- try { obj.AgentCommitDate = Date.parse(msg.substring(2)) } catch (ex) { }
- //console.log('Connected Agent Commit Date: ' + msg.substring(2) + ", " + Date.parse(msg.substring(2)));
- }
- }
- });
-
- // If error, do nothing
- ws.on('error', function (err) { parent.parent.debug('agent', 'AGENT WSERR: ' + err); console.log('AGENT WSERR: ' + err); obj.close(0); });
-
- // If the mesh agent web socket is closed, clean up.
- ws.on('close', function (req) {
- parent.agentStats.agentClose++;
- if (obj.nodeid != null) {
- const agentId = (obj.agentInfo && obj.agentInfo.agentId) ? obj.agentInfo.agentId : 'Unknown';
- //console.log('Agent disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ') id=' + agentId);
- parent.parent.debug('agent', 'Agent disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ') id=' + agentId);
-
- // Log the agent disconnection if we are not testing agent update
- if (args.agentupdatetest == null) {
- if (parent.wsagentsDisconnections[obj.nodeid] == null) {
- parent.wsagentsDisconnections[obj.nodeid] = 1;
- } else {
- parent.wsagentsDisconnections[obj.nodeid] = ++parent.wsagentsDisconnections[obj.nodeid];
- }
- }
- }
- obj.close(0);
- });
-
- // Return the mesh for this device, in some cases, we may auto-create the mesh.
- function getMeshAutoCreate() {
- var mesh = parent.meshes[obj.dbMeshKey];
-
- // If the mesh was not found and we are in LAN mode, check of the domain can be corrected
- if ((args.lanonly == true) && (mesh == null)) {
- var smesh = obj.dbMeshKey.split('/');
- for (var i in parent.parent.config.domains) {
- mesh = parent.meshes['mesh/' + i + '/' + smesh[2]];
- if (mesh != null) {
- obj.domain = domain = parent.parent.config.domains[i];
- obj.meshid = smesh[2];
- obj.dbMeshKey = 'mesh/' + i + '/' + smesh[2];
- obj.dbNodeKey = 'node/' + domain.id + '/' + obj.nodeid;
- break;
- }
- }
- }
-
- if ((mesh == null) && (typeof domain.orphanagentuser == 'string')) {
- const adminUser = parent.users['user/' + domain.id + '/' + domain.orphanagentuser];
- if ((adminUser != null) && (adminUser.siteadmin == 0xFFFFFFFF)) {
- // Mesh name is hex instead of base64
- const meshname = obj.meshid.substring(0, 18);
-
- // Create a new mesh for this device
- const links = {};
- links[adminUser._id] = { name: adminUser.name, rights: 0xFFFFFFFF };
- mesh = { type: 'mesh', _id: obj.dbMeshKey, name: meshname, mtype: 2, desc: '', domain: domain.id, links: links };
- db.Set(mesh);
- parent.meshes[obj.dbMeshKey] = mesh;
-
- if (adminUser.links == null) adminUser.links = {};
- adminUser.links[obj.dbMeshKey] = { rights: 0xFFFFFFFF };
- db.SetUser(adminUser);
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [adminUser._id, obj.dbNodeKey]), obj, { etype: 'mesh', username: adminUser.name, meshid: obj.dbMeshKey, name: meshname, mtype: 2, desc: '', action: 'createmesh', links: links, msgid: 55, msgArgs: [obj.meshid], msg: "Created device group: " + obj.meshid, domain: domain.id });
- }
- } else {
- if ((mesh != null) && (mesh.deleted != null) && (mesh.links)) {
- // Must un-delete this mesh
- var ids = parent.CreateMeshDispatchTargets(mesh._id, [obj.dbNodeKey]);
-
- // See if users still exists, if so, add links to the mesh
- for (var userid in mesh.links) {
- const user = parent.users[userid];
- if (user) {
- if (user.links == null) { user.links = {}; }
- if (user.links[mesh._id] == null) {
- user.links[mesh._id] = { rights: mesh.links[userid].rights };
- ids.push(user._id);
- db.SetUser(user);
- }
- }
- }
-
- // Send out an event indicating this mesh was "created"
- parent.parent.DispatchEvent(ids, obj, { etype: 'mesh', meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, action: 'createmesh', links: mesh.links, msgid: 56, msgArgs: [mesh._id], msg: "Device group undeleted: " + mesh._id, domain: domain.id });
-
- // Mark the mesh as active
- delete mesh.deleted;
- db.Set(mesh);
- }
- }
- return mesh;
- }
-
- // Send a PING/PONG message
- function sendPing() { obj.send('{"action":"ping"}'); }
- function sendPong() { obj.send('{"action":"pong"}'); }
-
- // Once we get all the information about an agent, run this to hook everything up to the server
- function completeAgentConnection() {
- if ((obj.authenticated != 1) || (obj.meshid == null) || obj.pendingCompleteAgentConnection || (obj.agentInfo == null)) { return; }
- obj.pendingCompleteAgentConnection = true;
-
- // Setup the agent PING/PONG timers
- if ((typeof args.agentping == 'number') && (obj.pingtimer == null)) { obj.pingtimer = setInterval(sendPing, args.agentping * 1000); }
- else if ((typeof args.agentpong == 'number') && (obj.pongtimer == null)) { obj.pongtimer = setInterval(sendPong, args.agentpong * 1000); }
-
- // If this is a recovery agent
- if (obj.agentInfo.capabilities & 0x40) {
- // Inform mesh agent that it's authenticated.
- delete obj.pendingCompleteAgentConnection;
- obj.authenticated = 2;
- obj.sendBinary(common.ShortToStr(4));
-
- // Ask for mesh core hash.
- obj.sendBinary(common.ShortToStr(11) + common.ShortToStr(0));
- return;
- }
-
- // Check if we have too many agent sessions
- if (typeof domain.limits.maxagentsessions == 'number') {
- // Count the number of agent sessions for this domain
- var domainAgentSessionCount = 0;
- for (var i in parent.wsagents) { if (parent.wsagents[i].domain.id == domain.id) { domainAgentSessionCount++; } }
-
- // Check if we have too many user sessions
- if (domainAgentSessionCount >= domain.limits.maxagentsessions) {
- // Too many, hold the connection.
- parent.agentStats.agentMaxSessionHoldCount++;
- return;
- }
- }
-
- /*
- // Check that the mesh exists
- var mesh = parent.meshes[obj.dbMeshKey];
- if (mesh == null) {
- var holdConnection = true;
- if (typeof domain.orphanagentuser == 'string') {
- var adminUser = parent.users['user/' + domain.id + '/' + args.orphanagentuser];
- if ((adminUser != null) && (adminUser.siteadmin == 0xFFFFFFFF)) {
- // Create a new mesh for this device
- holdConnection = false;
- var links = {};
- links[user._id] = { name: adminUser.name, rights: 0xFFFFFFFF };
- mesh = { type: 'mesh', _id: obj.dbMeshKey, name: obj.meshid, mtype: 2, desc: '', domain: domain.id, links: links };
- db.Set(mesh);
- parent.meshes[obj.meshid] = mesh;
- parent.parent.AddEventDispatch(parent.CreateMeshDispatchTargets(obj.meshid, [obj.dbNodeKey]), ws);
-
- if (adminUser.links == null) user.links = {};
- adminUser.links[obj.meshid] = { rights: 0xFFFFFFFF };
- //adminUser.subscriptions = parent.subscribe(adminUser._id, ws);
- db.SetUser(user);
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(meshid, [user._id, obj.dbNodeKey]), obj, { etype: 'mesh', username: user.name, meshid: obj.meshid, name: obj.meshid, mtype: 2, desc: '', action: 'createmesh', links: links, msg: 'Mesh created: ' + obj.meshid, domain: domain.id });
- }
- }
-
- if (holdConnection == true) {
- // If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
- parent.parent.debug('agent', 'Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
- console.log('Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
- return;
- }
- }
- if (mesh.mtype != 2) { // If we disconnect, the agnet will just reconnect. We need to log this or tell agent to connect in a few hours.
- parent.parent.debug('agent', 'Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
- console.log('Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
- return;
- }
- */
-
- // Check that the node exists
- db.Get(obj.dbNodeKey, function (err, nodes) {
- if (obj.agentInfo == null) { return; }
- var device, mesh;
-
- // See if this node exists in the database
- if ((nodes == null) || (nodes.length == 0)) {
- // This device does not exist, use the meshid given by the device
-
- // Check if we already have too many devices for this domain
- if (domain.limits && (typeof domain.limits.maxdevices == 'number')) {
- db.isMaxType(domain.limits.maxdevices, 'node', domain.id, function (ismax, count) {
- if (ismax == true) {
- // Too many devices in this domain.
- parent.agentStats.maxDomainDevicesReached++;
- } else {
- // We are under the limit, create the new device.
- completeAgentConnection2();
- }
- });
- } else {
- completeAgentConnection2();
- }
- return;
- } else {
- device = nodes[0];
- obj.name = device.name;
-
- // This device exists, meshid given by the device must be ignored, use the server side one.
- if ((device.meshid != null) && (device.meshid != obj.dbMeshKey)) {
- obj.dbMeshKey = device.meshid;
- obj.meshid = device.meshid.split('/')[2];
- }
-
- // See if this mesh exists, if it does not we may want to create it.
- mesh = getMeshAutoCreate();
-
- // Check if the mesh exists
- if (mesh == null) {
- // If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
- parent.agentStats.invalidDomainMesh2Count++;
- parent.setAgentIssue(obj, "invalidDomainMesh2");
- parent.parent.debug('agent', 'Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
- console.log('Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
- return;
- }
-
- // Check if the mesh is the right type
- if (mesh.mtype != 2) {
- // If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
- parent.agentStats.invalidMeshType2Count++;
- parent.setAgentIssue(obj, "invalidMeshType2");
- parent.parent.debug('agent', 'Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
- console.log('Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
- return;
- }
-
- // Mark when this device connected
- obj.connectTime = Date.now();
-
- // Device already exists, look if changes have occured
- var changes = [], change = 0, log = 0;
- if (device.agent == null) { device.agent = { ver: obj.agentInfo.agentVersion, id: obj.agentInfo.agentId, caps: obj.agentInfo.capabilities }; change = 1; }
- if (device.rname != obj.agentInfo.computerName) { device.rname = obj.agentInfo.computerName; change = 1; changes.push('computer name'); }
- if (device.agent.ver != obj.agentInfo.agentVersion) { device.agent.ver = obj.agentInfo.agentVersion; change = 1; changes.push('agent version'); }
- if (device.agent.id != obj.agentInfo.agentId) { device.agent.id = obj.agentInfo.agentId; change = 1; changes.push('agent type'); }
- if ((device.agent.caps & 24) != (obj.agentInfo.capabilities & 24)) { device.agent.caps = obj.agentInfo.capabilities; change = 1; changes.push('agent capabilities'); } // If agent console or javascript support changes, update capabilities
- // We want the server name to be sync'ed to the hostname or the --agentName
- // (flag 16 allows to override the name until next connection)
- if (mesh.flags && (mesh.flags & 2)) {
- var preferredName = (mesh.flags & 8) && obj.agentName || obj.agentInfo.computerName;
- if (device.name != preferredName) {device.name = preferredName; change = 1; }
- }
- if (device.ip != obj.remoteaddr) { device.ip = obj.remoteaddr; change = 1; }
-
- if (change == 1) {
- // Do some clean up if needed, these values should not be in the database.
- if (device.conn != null) { delete device.conn; }
- if (device.pwr != null) { delete device.pwr; }
- if (device.agct != null) { delete device.agct; }
- if (device.cict != null) { delete device.cict; }
-
- // Save the updated device in the database
- db.Set(device);
-
- // If this is a temporary device, don't log changes
- if (obj.agentInfo.capabilities & 0x20) { log = 0; }
-
- // Event the node change
- var event = { etype: 'node', action: 'changenode', nodeid: obj.dbNodeKey, domain: domain.id, node: parent.CloneSafeNode(device) };
- if (log == 0) { event.nolog = 1; } else { event.msg = 'Changed device ' + device.name + ' from group ' + mesh.name + ': ' + changes.join(', '); }
- if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(device.meshid, [obj.dbNodeKey]), obj, event);
- }
- }
-
- completeAgentConnection3(device, mesh);
- });
- }
-
- function completeAgentConnection2() {
- // See if this mesh exists, if it does not we may want to create it.
- var mesh = getMeshAutoCreate();
-
- // Check if the mesh exists
- if (mesh == null) {
- // If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
- parent.agentStats.invalidDomainMeshCount++;
- parent.setAgentIssue(obj, "invalidDomainMesh");
- parent.parent.debug('agent', 'Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
- console.log('Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
- return;
- }
-
- // Check if the mesh is the right type
- if (mesh.mtype != 2) {
- // If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
- parent.agentStats.invalidMeshTypeCount++;
- parent.setAgentIssue(obj, "invalidMeshType");
- parent.parent.debug('agent', 'Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
- console.log('Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
- return;
- }
-
- // Mark when this device connected
- obj.connectTime = Date.now();
-
- // This node does not exist, create it.
- var agentName = obj.agentName ? obj.agentName : obj.agentInfo.computerName;
- var device = { type: 'node', mtype: mesh.mtype, _id: obj.dbNodeKey, icon: obj.agentInfo.platformType, meshid: obj.dbMeshKey, name: agentName, rname: obj.agentInfo.computerName, domain: domain.id, agent: { ver: obj.agentInfo.agentVersion, id: obj.agentInfo.agentId, caps: obj.agentInfo.capabilities }, host: null };
- db.Set(device);
-
- // Event the new node
- if (obj.agentInfo.capabilities & 0x20) {
- // This is a temporary agent, don't log.
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, { etype: 'node', action: 'addnode', node: device, domain: domain.id, nolog: 1 });
- } else {
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, { etype: 'node', action: 'addnode', node: device, msgid: 57, msgArgs: [obj.agentInfo.computerName, mesh.name], msg: ('Added device ' + obj.agentInfo.computerName + ' to device group ' + mesh.name), domain: domain.id });
- }
-
- completeAgentConnection3(device, mesh);
- }
-
- function completeAgentConnection3(device, mesh) {
- // Check if this agent is already connected
- const dupAgent = parent.wsagents[obj.dbNodeKey];
- parent.wsagents[obj.dbNodeKey] = obj;
- if (dupAgent) {
- // Record duplicate agents
- if (parent.duplicateAgentsLog[obj.dbNodeKey] == null) {
- if (dupAgent.remoteaddr == obj.remoteaddr) {
- parent.duplicateAgentsLog[obj.dbNodeKey] = { name: device.name, group: mesh.name, ip: [obj.remoteaddr], count: 1 };
- } else {
- parent.duplicateAgentsLog[obj.dbNodeKey] = { name: device.name, group: mesh.name, ip: [obj.remoteaddr, dupAgent.remoteaddr], count: 1 };
- }
- } else {
- parent.duplicateAgentsLog[obj.dbNodeKey].name = device.name;
- parent.duplicateAgentsLog[obj.dbNodeKey].group = mesh.name;
- parent.duplicateAgentsLog[obj.dbNodeKey].count++;
- if (parent.duplicateAgentsLog[obj.dbNodeKey].ip.indexOf(obj.remoteaddr) == -1) { parent.duplicateAgentsLog[obj.dbNodeKey].ip.push(obj.remoteaddr); }
- }
-
- // Close the duplicate agent
- parent.agentStats.duplicateAgentCount++;
- parent.setAgentIssue(obj, 'duplicateAgent');
- if (obj.nodeid != null) { parent.parent.debug('agent', 'Duplicate agent ' + obj.nodeid + ' (' + obj.remoteaddrport + ')'); }
- dupAgent.close(3);
- } else {
- // Indicate the agent is connected
- parent.parent.SetConnectivityState(obj.dbMeshKey, obj.dbNodeKey, obj.connectTime, 1, 1, null, { remoteaddrport: obj.remoteaddrport, name: device.name });
- }
-
- // We are done, ready to communicate with this agent
- delete obj.pendingCompleteAgentConnection;
- obj.authenticated = 2;
-
- // Check how many times this agent disconnected in the last few minutes.
- const disconnectCount = parent.wsagentsDisconnections[obj.nodeid];
- if (disconnectCount > 6) {
- parent.parent.debug('agent', 'Agent in big trouble: NodeId=' + obj.nodeid + ', IP=' + obj.remoteaddrport + ', Agent=' + obj.agentInfo.agentId + '.');
- console.log('Agent in big trouble: NodeId=' + obj.nodeid + ', IP=' + obj.remoteaddrport + ', Agent=' + obj.agentInfo.agentId + '.');
- parent.agentStats.agentInBigTrouble++;
- // TODO: Log or do something to recover?
- return;
- }
-
- // Command 4, inform mesh agent that it's authenticated.
- obj.sendBinary(common.ShortToStr(4));
-
- // Not sure why, but in rare cases, obj.agentInfo is undefined here.
- if ((obj.agentInfo == null) || (typeof obj.agentInfo.capabilities != 'number')) { return; } // This is an odd case.
- obj.agentExeInfo = parent.parent.meshAgentBinaries[obj.agentInfo.agentId];
- if (domain.meshAgentBinaries && domain.meshAgentBinaries[obj.agentInfo.agentId]) { obj.agentExeInfo = domain.meshAgentBinaries[obj.agentInfo.agentId]; }
-
- // Check if this agent is reconnecting too often.
- if (disconnectCount > 4) {
- // Too many disconnections, this agent has issues. Just clear the core.
- obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0));
- parent.parent.debug('agent', 'Agent in trouble: NodeId=' + obj.nodeid + ', IP=' + obj.remoteaddrport + ', Agent=' + obj.agentInfo.agentId + '.');
- parent.agentStats.agentInTrouble++;
- //console.log('Agent in trouble: NodeId=' + obj.nodeid + ', IP=' + obj.remoteaddrport + ', Agent=' + obj.agentInfo.agentId + '.');
- // TODO: Log or do something to recover?
- return;
- }
-
- // Check if we need to make an native update check
- var corename = null;
- if (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId] != null) {
- corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].core;
- } else {
- // MeshCommand_CoreModule, ask mesh agent to clear the core
- obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0));
- }
-
- if ((obj.agentExeInfo != null) && (obj.agentExeInfo.update == true)) {
- // Ask the agent for it's executable binary hash
- obj.sendBinary(common.ShortToStr(12) + common.ShortToStr(0));
- } else {
- // Check the mesh core, if the agent is capable of running one
- if (((obj.agentInfo.capabilities & 16) != 0) && (corename != null)) {
- obj.sendBinary(common.ShortToStr(11) + common.ShortToStr(0)); // Command 11, ask for mesh core hash.
- } else {
- agentCoreIsStable(); // No updates needed, agent is ready to go.
- }
- }
- }
-
- // Indicate to the agent that we want to check Intel AMT configuration
- // This may trigger a CIRA-LMS tunnel from the agent so the server can inspect the device.
- obj.sendUpdatedIntelAmtPolicy = function (policy) {
- if (obj.agentExeInfo && (obj.agentExeInfo.amt == true)) { // Only send Intel AMT policy to agents what could have AMT.
- if (policy == null) { var mesh = parent.meshes[obj.dbMeshKey]; if (mesh == null) return; policy = mesh.amt; }
- if ((policy != null) && (policy.type != 0)) {
- const cookie = parent.parent.encodeCookie({ a: 'apf', n: obj.dbNodeKey, m: obj.dbMeshKey }, parent.parent.loginCookieEncryptionKey);
- try { obj.send(JSON.stringify({ action: 'amtconfig', user: '**MeshAgentApfTunnel**', pass: cookie })); } catch (ex) { }
- }
- }
- }
-
- function recoveryAgentCoreIsStable(mesh) {
- parent.agentStats.recoveryCoreIsStableCount++;
-
- // Recovery agent is doing ok, lets perform main agent checking.
- //console.log('recoveryAgentCoreIsStable()');
-
- // Fetch the the real agent nodeid
- db.Get('da' + obj.dbNodeKey, function (err, nodes, self) {
- if ((nodes != null) && (nodes.length == 1)) {
- self.realNodeKey = nodes[0].raid;
-
- // Get agent connection state
- var agentConnected = false;
- var state = parent.parent.GetConnectivityState(self.realNodeKey);
- if (state) { agentConnected = ((state.connectivity & 1) != 0) }
-
- self.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: self.realNodeKey, agent: agentConnected } }));
- } else {
- self.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: null } }));
- }
- }, obj);
- }
-
- function agentCoreIsStable() {
- parent.agentStats.coreIsStableCount++;
-
- // Check that the mesh exists
- const mesh = parent.meshes[obj.dbMeshKey];
- if (mesh == null) {
- parent.agentStats.meshDoesNotExistCount++;
- parent.setAgentIssue(obj, "meshDoesNotExist");
- // TODO: Mark this agent as part of a mesh that does not exists.
- return; // Probably not worth doing anything else. Hold this agent.
- }
-
- // Check if this is a recovery agent
- if (obj.agentInfo.capabilities & 0x40) {
- recoveryAgentCoreIsStable(mesh);
- return;
- }
-
- // Fetch the the diagnostic agent nodeid
- db.Get('ra' + obj.dbNodeKey, function (err, nodes) {
- if ((nodes != null) && (nodes.length == 1)) {
- obj.diagnosticNodeKey = nodes[0].daid;
- obj.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: obj.diagnosticNodeKey } }));
- }
- });
-
- // Indicate that we want to check the Intel AMT configuration
- // This may trigger a CIRA-LMS tunnel to the server for further processing
- obj.sendUpdatedIntelAmtPolicy();
-
- // Fetch system information
- db.GetHash('si' + obj.dbNodeKey, function (err, results) {
- if ((results != null) && (results.length == 1)) { obj.send(JSON.stringify({ action: 'sysinfo', hash: results[0].hash })); } else { obj.send(JSON.stringify({ action: 'sysinfo' })); }
- });
-
- // Agent error log dump
- if (parent.parent.agentErrorLog != null) {
- db.Get('al' + obj.dbNodeKey, function (err, docs) { // Agent Log
- if ((docs != null) && (docs.length == 1) && (typeof docs[0].lastEvent)) {
- obj.send('{"action":"errorlog","startTime":' + docs[0].lastEvent + '}'); // Ask all events after a given time
- } else {
- obj.send('{"action":"errorlog"}'); // Ask all
- }
- });
- }
-
- // Set agent core dump
- if ((parent.parent.config.settings != null) && ((parent.parent.config.settings.agentcoredump === true) || (parent.parent.config.settings.agentcoredump === false))) {
- obj.send(JSON.stringify({ action: 'coredump', value: parent.parent.config.settings.agentcoredump }));
- if (parent.parent.config.settings.agentcoredump === true) {
- // Check if we requested a core dump file in the last minute, if not, ask if one is present.
- if ((parent.lastCoreDumpRequest == null) || ((Date.now() - parent.lastCoreDumpRequest) >= 60000)) { obj.send(JSON.stringify({ action: 'getcoredump' })); }
- }
- }
-
- // Do this if IP location is enabled on this domain TODO: Set IP location per device group?
- if (domain.iplocation == true) {
- // Check if we already have IP location information for this node
- db.Get('iploc_' + obj.remoteaddr, function (err, iplocs) {
- if ((iplocs != null) && (iplocs.length == 1)) {
- // We have a location in the database for this remote IP
- const iploc = iplocs[0], x = {};
- if ((iploc != null) && (iploc.ip != null) && (iploc.loc != null)) {
- x.publicip = iploc.ip;
- x.iploc = iploc.loc + ',' + (Math.floor((new Date(iploc.date)) / 1000));
- ChangeAgentLocationInfo(x);
- }
- } else {
- // Check if we need to ask for the IP location
- var doIpLocation = 0;
- if (obj.iploc == null) {
- doIpLocation = 1;
- } else {
- const loc = obj.iploc.split(',');
- if (loc.length < 3) {
- doIpLocation = 2;
- } else {
- var t = new Date((parseFloat(loc[2]) * 1000)), now = Date.now();
- t.setDate(t.getDate() + 20);
- if (t < now) { doIpLocation = 3; }
- }
- }
-
- // If we need to ask for IP location, see if we have the quota to do it.
- if (doIpLocation > 0) {
- db.getValueOfTheDay('ipLocationRequestLimitor', 10, function (ipLocationLimitor) {
- if ((ipLocationLimitor != null) && (ipLocationLimitor.value > 0)) {
- ipLocationLimitor.value--;
- db.Set(ipLocationLimitor);
- obj.send(JSON.stringify({ action: 'iplocation' }));
- }
- });
- }
- }
- });
- }
-
- // Indicate server information to the agent.
- var serverInfo = { action: 'serverInfo' };
- if ((typeof domain.terminal == 'object') && (typeof domain.terminal.launchcommand == 'object')) {
- // Send terminal starting command
- serverInfo.termlaunchcommand = {};
- if (typeof domain.terminal.launchcommand.linux == 'string') { serverInfo.termlaunchcommand.linux = domain.terminal.launchcommand.linux; }
- if (typeof domain.terminal.launchcommand.darwin == 'string') { serverInfo.termlaunchcommand.darwin = domain.terminal.launchcommand.darwin; }
- if (typeof domain.terminal.launchcommand.freebsd == 'string') { serverInfo.termlaunchcommand.freebsd = domain.terminal.launchcommand.freebsd; }
- }
- // Enable agent self guest sharing if allowed
- if (domain.agentselfguestsharing) { serverInfo.agentSelfGuestSharing = true; }
- obj.send(JSON.stringify(serverInfo));
-
- // Plug in handler
- if (parent.parent.pluginHandler != null) {
- parent.parent.pluginHandler.callHook('hook_agentCoreIsStable', obj, parent);
- }
- }
-
- // Get the web certificate private key hash for the specified domain
- function getWebCertHash(domain) {
- const hash = parent.webCertificateHashs[domain.id];
- if (hash != null) return hash;
- return parent.webCertificateHash;
- }
-
- // Get the web certificate hash for the specified domain
- function getWebCertFullHash(domain) {
- const hash = parent.webCertificateFullHashs[domain.id];
- if (hash != null) return hash;
- return parent.webCertificateFullHash;
- }
-
- // Verify the agent signature
- function processAgentSignature(msg) {
- if (isIgnoreHashCheck() == false) {
- var verified = false;
-
- // This agent did not report a valid TLS certificate hash, fail now.
- if (obj.agentSeenCerthash == null) return false;
-
- // Raw RSA signatures have an exact length of 256 or 384. PKCS7 is larger.
- if ((msg.length != 384) && (msg.length != 256)) {
- // Verify a PKCS7 signature.
- var msgDer = null;
- try { msgDer = forge.asn1.fromDer(forge.util.createBuffer(msg, 'binary')); } catch (ex) { }
- if (msgDer != null) {
- try {
- const p7 = forge.pkcs7.messageFromAsn1(msgDer);
- const sig = p7.rawCapture.signature;
-
- // Verify with key hash
- var buf = Buffer.from(obj.agentSeenCerthash + obj.nonce + obj.agentnonce, 'binary');
- var verifier = parent.crypto.createVerify('RSA-SHA384');
- verifier.update(buf);
- verified = verifier.verify(obj.unauth.nodeCertPem, sig, 'binary');
- if (verified !== true) {
- // Not a valid signature
- parent.agentStats.invalidPkcsSignatureCount++;
- parent.setAgentIssue(obj, "invalidPkcsSignature");
- return false;
- }
- } catch (ex) { };
- }
- }
-
- if (verified == false) {
- // Verify the RSA signature. This is the fast way, without using forge.
- const verify = parent.crypto.createVerify('SHA384');
- verify.end(Buffer.from(obj.agentSeenCerthash + obj.nonce + obj.agentnonce, 'binary')); // Test using the private key hash
- if (verify.verify(obj.unauth.nodeCertPem, Buffer.from(msg, 'binary')) !== true) {
- parent.agentStats.invalidRsaSignatureCount++;
- parent.setAgentIssue(obj, "invalidRsaSignature");
- return false;
- }
- }
- }
-
- // Connection is a success, clean up
- obj.nodeid = obj.unauth.nodeid;
- obj.dbNodeKey = 'node/' + domain.id + '/' + obj.nodeid;
- delete obj.nonce;
- delete obj.agentnonce;
- delete obj.unauth;
- delete obj.receivedCommands;
- delete obj.agentSeenCerthash;
- if (obj.unauthsign) delete obj.unauthsign;
- parent.agentStats.verifiedAgentConnectionCount++;
- parent.parent.debug('agent', 'Verified agent connection to ' + obj.nodeid + ' (' + obj.remoteaddrport + ').');
- obj.authenticated = 1;
- return true;
- }
-
- // Process incoming agent JSON data
- function processAgentData(msg) {
- if (obj.agentInfo == null) return;
- var i, str = msg.toString('utf8'), command = null;
- if (str[0] == '{') {
- try { command = JSON.parse(str); } catch (ex) {
- // If the command can't be parsed, ignore it.
- parent.agentStats.invalidJsonCount++;
- parent.setAgentIssue(obj, "invalidJson (" + str.length + "): " + str);
- parent.parent.debug('agent', 'Unable to parse agent JSON (' + obj.remoteaddrport + ')');
- console.log('Unable to parse agent JSON (' + obj.remoteaddrport + '): ' + str, ex);
- return;
- }
- if (typeof command != 'object') { return; }
- switch (command.action) {
- case 'msg':
- {
- // If the same console command is processed many times, kick out this agent.
- // This is a safety mesure to guard against the agent DOS'ing the server.
- if (command.type == 'console') {
- if (obj.consoleKickValue == command.value) {
- if (obj.consoleKickCount) { obj.consoleKickCount++; } else { obj.consoleKickCount = 1; }
- if (obj.consoleKickCount > 30) { obj.close(); return; } // 30 identical console messages received, kick out this agent.
- } else {
- obj.consoleKickValue = command.value;
- }
- }
-
- // Route a message
- parent.routeAgentCommand(command, obj.domain.id, obj.dbNodeKey, obj.dbMeshKey);
- break;
- }
- case 'coreinfo':
- {
- // Sent by the agent to update agent information
- ChangeAgentCoreInfo(command);
-
- if ((obj.agentCoreUpdate === true) && (obj.agentExeInfo != null) && (typeof obj.agentExeInfo.url == 'string')) {
- // Agent update. The recovery core was loaded in the agent, send a command to update the agent
- parent.parent.taskLimiter.launch(function (argument, taskid, taskLimiterQueue) { // Medium priority task
- // If agent disconnection, complete and exit now.
- if ((obj.authenticated != 2) || (obj.agentExeInfo == null)) { parent.parent.taskLimiter.completed(taskid); return; }
-
- // Agent update. The recovery core was loaded in the agent, send a command to update the agent
- obj.agentCoreUpdateTaskId = taskid;
- const url = '*' + require('url').parse(obj.agentExeInfo.url).path;
- var cmd = { action: 'agentupdate', url: url, hash: obj.agentExeInfo.hashhex };
- parent.parent.debug('agentupdate', "Sending agent update url: " + cmd.url);
-
- // Add the hash
- if (obj.agentExeInfo.fileHash != null) { cmd.hash = obj.agentExeInfo.fileHashHex; } else { cmd.hash = obj.agentExeInfo.hashhex; }
-
- // Add server TLS cert hash
- if (isIgnoreHashCheck() == false) {
- const tlsCertHash = parent.webCertificateFullHashs[domain.id];
- if (tlsCertHash != null) { cmd.servertlshash = Buffer.from(tlsCertHash, 'binary').toString('hex'); }
- }
-
- // Send the agent update command
- obj.send(JSON.stringify(cmd));
- }, null, 1);
- }
- break;
- }
- case 'smbios':
- {
- // SMBIOS information must never be saved when NeDB is in use. NeDB will currupt that database.
- if (db.SetSMBIOS == null) break;
-
- // See if we need to save SMBIOS information
- if (domain.smbios === true) {
- // Store the RAW SMBios table of this computer
- // Perform sanity checks before storing
- try {
- for (var i in command.value) { var k = parseInt(i); if ((k != i) || (i > 255) || (typeof command.value[i] != 'object') || (command.value[i].length == null) || (command.value[i].length > 1024) || (command.value[i].length < 0)) { delete command.value[i]; } }
- db.SetSMBIOS({ _id: obj.dbNodeKey, domain: domain.id, time: new Date(), value: command.value });
- } catch (ex) { }
- }
-
- // Event the node interface information change (This is a lot of traffic, probably don't need this).
- //parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.meshid, [obj.dbNodeKey]), obj, { action: 'smBiosChange', nodeid: obj.dbNodeKey, domain: domain.id, smbios: command.value, nolog: 1 });
-
- break;
- }
- case 'netinfo':
- {
- // Check if network information is present
- if ((command.netif2 == null) && (command.netif == null)) return;
-
- // Escape any field names that have special characters
- if (command.netif2 != null) {
- for (var i in command.netif2) {
- var esc = common.escapeFieldName(i);
- if (esc !== i) { command.netif2[esc] = command.netif2[i]; delete command.netif2[i]; }
- }
- }
-
- // Sent by the agent to update agent network interface information
- delete command.action;
- command.updateTime = Date.now();
- command._id = 'if' + obj.dbNodeKey;
- command.domain = domain.id;
- command.type = 'ifinfo';
- db.Set(command);
-
- // Event the node interface information change
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.meshid, [obj.dbNodeKey]), obj, { action: 'ifchange', nodeid: obj.dbNodeKey, domain: domain.id, nolog: 1 });
-
- break;
- }
- case 'iplocation':
- {
- // Sent by the agent to update location information
- if ((command.type == 'publicip') && (command.value != null) && (typeof command.value == 'object') && (command.value.ip) && (command.value.loc)) {
- var x = {};
- x.publicip = command.value.ip;
- x.iploc = command.value.loc + ',' + (Math.floor(Date.now() / 1000));
- ChangeAgentLocationInfo(x);
- command.value._id = 'iploc_' + command.value.ip;
- command.value.type = 'iploc';
- command.value.date = Date.now();
- db.Set(command.value); // Store the IP to location data in the database
- // Sample Value: { ip: '192.55.64.246', city: 'Hillsboro', region: 'Oregon', country: 'US', loc: '45.4443,-122.9663', org: 'AS4983 Intel Corporation', postal: '97123' }
- }
- break;
- }
- case 'mc1migration':
- {
- if (command.oldnodeid.length != 64) break;
- const oldNodeKey = 'node//' + command.oldnodeid.toLowerCase();
- db.Get(oldNodeKey, function (err, nodes) {
- if ((nodes == null) || (nodes.length != 1)) return;
- const node = nodes[0];
- if (node.meshid == obj.dbMeshKey) {
- // Update the device name & host
- const newNode = { "name": node.name };
- if (node.intelamt != null) { newNode.intelamt = node.intelamt; }
- ChangeAgentCoreInfo(newNode);
-
- // Delete this node including network interface information and events
- db.Remove(node._id);
- db.Remove('if' + node._id);
-
- // Event node deletion
- const change = 'Migrated device ' + node.name;
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(node.meshid, [obj.dbNodeKey]), obj, { etype: 'node', action: 'removenode', nodeid: node._id, msg: change, domain: node.domain });
- }
- });
- break;
- }
- case 'openUrl':
- {
- // Sent by the agent to return the status of a open URL action.
- // Nothing is done right now.
- break;
- }
- case 'log':
- {
- // Log a value in the event log
- if ((typeof command.msg == 'string') && (command.msg.length < 4096)) {
- var event = { etype: 'node', action: 'agentlog', nodeid: obj.dbNodeKey, domain: domain.id, msg: command.msg };
- if (typeof command.msgid == 'number') { event.msgid = command.msgid; }
- if (typeof command.guestname == 'string') { event.guestname = command.guestname; }
- if (Array.isArray(command.msgArgs)) { event.msgArgs = command.msgArgs; }
- if (typeof command.remoteaddr == 'string') { event.remoteaddr = command.remoteaddr; }
- var targets = parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]);
- if (typeof command.userid == 'string') {
- var loguser = parent.users[command.userid];
- if (loguser) { event.userid = command.userid; event.username = loguser.name; targets.push(command.userid); }
- }
- if (typeof command.xuserid == 'string') {
- var xloguser = parent.users[command.xuserid];
- if (xloguser) { targets.push(command.xuserid); }
- }
- if ((typeof command.sessionid == 'string') && (command.sessionid.length < 500)) { event.sessionid = command.sessionid; }
- parent.parent.DispatchEvent(targets, obj, event);
-
- // If this is a help request, see if we need to email notify anyone
- if (event.msgid == 98) {
- // Get the node and change it if needed
- db.Get(obj.dbNodeKey, function (err, nodes) { // TODO: THIS IS A BIG RACE CONDITION HERE, WE NEED TO FIX THAT. If this call is made twice at the same time on the same device, data will be missed.
- if ((nodes == null) || (nodes.length != 1)) { delete obj.deviceChanging; return; }
- const device = nodes[0];
- if (typeof device.name == 'string') { parent.parent.NotifyUserOfDeviceHelpRequest(domain, device.meshid, device._id, device.name, command.msgArgs[0], command.msgArgs[1]); }
- });
- }
- }
- break;
- }
- case 'ping': { sendPong(); break; }
- case 'pong': { break; }
- case 'getScript':
- {
- // Used by the agent to get configuration scripts.
- if (command.type == 1) {
- parent.getCiraConfigurationScript(obj.dbMeshKey, function (script) {
- obj.send(JSON.stringify({ action: 'getScript', type: 1, script: script.toString() }));
- });
- } else if (command.type == 2) {
- parent.getCiraCleanupScript(function (script) {
- obj.send(JSON.stringify({ action: 'getScript', type: 2, script: script.toString() }));
- });
- }
- break;
- }
- case 'diagnostic':
- {
- if (typeof command.value == 'object') {
- switch (command.value.command) {
- case 'register': {
- // Only main agent can do this
- if (((obj.agentInfo.capabilities & 0x40) == 0) && (typeof command.value.value == 'string') && (command.value.value.length == 64)) {
- // Store links to diagnostic agent id
- var daNodeKey = 'node/' + domain.id + '/' + db.escapeBase64(command.value.value);
- db.Set({ _id: 'da' + daNodeKey, domain: domain.id, time: obj.connectTime, raid: obj.dbNodeKey }); // DiagnosticAgent --> Agent
- db.Set({ _id: 'ra' + obj.dbNodeKey, domain: domain.id, time: obj.connectTime, daid: daNodeKey }); // Agent --> DiagnosticAgent
- }
- break;
- }
- case 'query': {
- // Only the diagnostic agent can do
- if ((obj.agentInfo.capabilities & 0x40) != 0) {
- // Return nodeid of main agent + connection status
- db.Get('da' + obj.dbNodeKey, function (err, nodes) {
- if ((nodes != null) && (nodes.length == 1)) {
- obj.realNodeKey = nodes[0].raid;
-
- // Get agent connection state
- var agentConnected = false;
- var state = parent.parent.GetConnectivityState(obj.realNodeKey);
- if (state) { agentConnected = ((state.connectivity & 1) != 0) }
-
- obj.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: obj.realNodeKey, agent: agentConnected } }));
- } else {
- obj.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: null } }));
- }
- });
- }
- break;
- }
- case 'log': {
- if (((obj.agentInfo.capabilities & 0x40) != 0) && (typeof command.value.value == 'string') && (command.value.value.length < 256)) {
- // If this is a diagnostic agent, log the event in the log of the main agent
- var event = { etype: 'node', action: 'diagnostic', nodeid: obj.realNodeKey, snodeid: obj.dbNodeKey, domain: domain.id, msg: command.value.value };
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, event);
- }
- break;
- }
- }
- }
- break;
- }
- case 'sysinfo': {
- if ((typeof command.data == 'object') && (typeof command.data.hash == 'string')) {
- // Validate command.data.
- if (common.validateObjectForMongo(command.data, 1024) == false) break;
-
- // Save to database
- command.data._id = 'si' + obj.dbNodeKey;
- command.data.type = 'sysinfo';
- command.data.domain = domain.id;
- command.data.time = Date.now();
- db.Set(command.data); // Update system information in the database.
-
- // Event the new sysinfo hash, this will notify everyone that the sysinfo document was changed
- var event = { etype: 'node', action: 'sysinfohash', nodeid: obj.dbNodeKey, domain: domain.id, hash: command.data.hash, nolog: 1 };
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, event);
- }
- break;
- }
- case 'sysinfocheck': {
- // Check system information update
- db.GetHash('si' + obj.dbNodeKey, function (err, results) {
- if ((results != null) && (results.length == 1)) { obj.send(JSON.stringify({ action: 'sysinfo', hash: results[0].hash })); } else { obj.send(JSON.stringify({ action: 'sysinfo' })); }
- });
- break;
- }
- case 'sessions': {
- // This is a list of sessions provided by the agent
- if (obj.sessions == null) { obj.sessions = {}; }
- if (typeof command.value != null) {
- if (command.type == 'kvm') { obj.sessions.kvm = command.value; }
- else if (command.type == 'terminal') { obj.sessions.terminal = command.value; }
- else if (command.type == 'files') { obj.sessions.files = command.value; }
- else if (command.type == 'help') { obj.sessions.help = command.value; }
- else if (command.type == 'tcp') { obj.sessions.tcp = command.value; }
- else if (command.type == 'udp') { obj.sessions.udp = command.value; }
- else if (command.type == 'msg') { obj.sessions.msg = command.value; }
- else if (command.type == 'app') { obj.sessions.app = command.value; }
- }
-
- // Any "help" session must have an associated app, if not, remove it.
- if (obj.sessions.help != null) {
- for (var i in obj.sessions.help) { if (obj.sessions.help[i] == null) { delete obj.sessions.help[i]; } }
- if (Object.keys(obj.sessions.help).length == 0) { delete obj.sessions.help; }
- }
-
- // Inform everyone of updated sessions
- obj.updateSessions();
- break;
- }
- case 'battery': {
- // Device battery and power state
- if (obj.sessions == null) { obj.sessions = {}; }
- if (obj.sessions.battery == null) { obj.sessions.battery = {}; }
- if ((command.state == 'ac') || (command.state == 'dc')) { obj.sessions.battery.state = command.state; } else { delete obj.sessions.battery.state; }
- if ((typeof command.level == 'number') && (command.level >= 0) && (command.level <= 100)) { obj.sessions.battery.level = command.level; } else { delete obj.sessions.battery.level; }
- obj.updateSessions();
- break;
- }
- case 'getcoredump': {
- // Check if we requested a core dump file in the last minute, if so, ignore this.
- if ((parent.lastCoreDumpRequest != null) && ((Date.now() - parent.lastCoreDumpRequest) < 60000)) break;
-
- // Indicates if the agent has a coredump available
- if ((command.exists === true) && (typeof command.agenthashhex == 'string') && (command.agenthashhex.length == 96)) {
- // Check if we already have this exact dump file
- const coreDumpFile = parent.path.join(parent.parent.datapath, '..', 'meshcentral-coredumps', obj.agentInfo.agentId + '-' + command.agenthashhex + '-' + obj.nodeid + '.dmp');
- parent.fs.stat(coreDumpFile, function (err, stats) {
- if (stats != null) return;
- obj.coreDumpPresent = true;
-
- // Check how many files are in the coredumps folder
- const coreDumpPath = parent.path.join(parent.parent.datapath, '..', 'meshcentral-coredumps');
- parent.fs.readdir(coreDumpPath, function (err, files) {
- if ((files != null) && (files.length >= 20)) return; // Don't get more than 20 core dump files.
-
- // Get the core dump uploaded to the server.
- parent.lastCoreDumpRequest = Date.now();
- obj.RequestCoreDump(command.agenthashhex, command.corehashhex);
- });
- });
- }
- break;
- }
- case 'tunnelCloseStats': {
- // TODO: This this extra stats from the tunnel, you can merge this into the tunnel event in the database.
- //console.log(command);
-
- // Validate input
- if ((command.sent == null) || (typeof command.sent != 'string')) return;
- if ((command.sentActual == null) || (typeof command.sentActual != 'string')) return;
- if ((command.sentActual == null) || (typeof command.sentActual != 'number')) return;
-
- // Event the session closed compression data.
- var event = { etype: 'node', action: 'sessioncompression', nodeid: obj.dbNodeKey, domain: domain.id, sent: parseInt(command.sent), sentActual: parseInt(command.sentActual), msgid: 54, msgArgs: [command.sentRatio, parseInt(command.sent), parseInt(command.sentActual)], msg: 'Agent closed session with ' + command.sentRatio + '% agent to server compression. Sent: ' + command.sent + ', Compressed: ' + command.sentActual + '.' };
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, event);
- break;
- }
- case 'lmsinfo': {
- // Agents send the LMS port bindings
- // Example: {"action":"lmsinfo","value":{"ports":["623","16992"]}}
- break;
- }
- case 'plugin': {
- if ((parent.parent.pluginHandler == null) || (typeof command.plugin != 'string')) break;
- try {
- parent.parent.pluginHandler.plugins[command.plugin].serveraction(command, obj, parent);
- } catch (e) {
- parent.parent.debug('agent', 'Error loading plugin handler (' + e + ')');
- console.log('Error loading plugin handler (' + e + ')');
- }
- break;
- }
- case 'meshToolInfo': {
- // Return information about a MeshCentral tool. Current tools are 'MeshCentralRouter' and 'MeshCentralAssistant'
- // Information includes file hash and download location URL
- if (typeof command.name != 'string') break;
- var info = parent.parent.meshToolsBinaries[command.name];
- if ((command.hash != null) && (info.hash == command.hash)) return;
-
- // To build the connection URL, if we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
- var xdomain = (domain.dns == null) ? domain.id : '';
- if (xdomain != '') xdomain += '/';
-
- // Build the response
- const responseCmd = { action: 'meshToolInfo', name: command.name, tag: command.tag, sessionid: command.sessionid, hash: info.hash, size: info.size, url: info.url };
- if ((command.name == 'MeshCentralAssistant') && (command.msh == true)) { responseCmd.url = '*/' + xdomain + 'meshagents?id=10006'; } // If this is Assistant and the MSH needs to be included in the executable, change the URL.
- if (command.cookie === true) { responseCmd.url += ('&auth=' + parent.parent.encodeCookie({ download: info.dlname }, parent.parent.loginCookieEncryptionKey)); }
- if (command.pipe === true) { responseCmd.pipe = true; }
- if (parent.webCertificateHashs[domain.id] != null) { responseCmd.serverhash = Buffer.from(parent.webCertificateHashs[domain.id], 'binary').toString('hex'); }
- try { ws.send(JSON.stringify(responseCmd)); } catch (ex) { }
- break;
- }
- case 'agentupdate': {
- if ((obj.agentExeInfo != null) && (typeof obj.agentExeInfo.url == 'string')) {
- var func = function agentUpdateFunc(argument, taskid, taskLimiterQueue) { // Medium priority task
- // If agent disconnection, complete and exit now.
- if (obj.authenticated != 2) { parent.parent.taskLimiter.completed(taskid); return; }
-
- // Agent is requesting an agent update
- obj.agentCoreUpdateTaskId = taskid;
- const url = '*' + require('url').parse(obj.agentExeInfo.url).path;
- var cmd = { action: 'agentupdate', url: url, hash: obj.agentExeInfo.hashhex, sessionid: agentUpdateFunc.sessionid };
- parent.parent.debug('agentupdate', "Sending user requested agent update url: " + cmd.url);
-
- // Add the hash
- if (obj.agentExeInfo.fileHash != null) { cmd.hash = obj.agentExeInfo.fileHashHex; } else { cmd.hash = obj.agentExeInfo.hashhex; }
-
- // Add server TLS cert hash
- if (isIgnoreHashCheck() == false) {
- const tlsCertHash = parent.webCertificateFullHashs[domain.id];
- if (tlsCertHash != null) { cmd.servertlshash = Buffer.from(tlsCertHash, 'binary').toString('hex'); }
- }
-
- // Send the agent update command
- obj.send(JSON.stringify(cmd));
- }
- func.sessionid = command.sessionid;
-
- // Agent update. The recovery core was loaded in the agent, send a command to update the agent
- parent.parent.taskLimiter.launch(func, null, 1);
- }
- break;
- }
- case 'agentupdatedownloaded': {
- if (obj.agentCoreUpdateTaskId != null) {
- // Indicate this udpate task is complete
- parent.parent.taskLimiter.completed(obj.agentCoreUpdateTaskId);
- delete obj.agentCoreUpdateTaskId;
- }
- break;
- }
- case 'errorlog': { // This is the agent error log
- if ((!Array.isArray(command.log)) || (command.log.length == 0) || (parent.parent.agentErrorLog == null)) break;
- var lastLogEntry = command.log[command.log.length - 1];
- if ((lastLogEntry != null) && (typeof lastLogEntry == 'object') && (typeof lastLogEntry.t == 'number')) {
- parent.fs.write(parent.parent.agentErrorLog, obj.dbNodeKey + ', ' + Date.now() + ', ' + str + '\r\n', function (err) { });
- db.Set({ _id: 'al' + obj.dbNodeKey, lastEvent: lastLogEntry.t });
- }
- break;
- }
- case '2faauth': {
- // Validate input
- if ((typeof command.url != 'string') || (typeof command.approved != 'boolean') || (command.url.startsWith('2fa://') == false)) return;
-
- // parse the URL
- var url = null;
- try { url = require('url').parse(command.url); } catch (ex) { }
- if (url == null) return;
-
- // Decode the cookie
- var urlSplit = url.query.split('&c=');
- if (urlSplit.length != 2) return;
- const authCookie = parent.parent.decodeCookie(urlSplit[1], null, 1);
- if ((authCookie == null) || (typeof authCookie.c != 'string') || (('code=' + authCookie.c) != urlSplit[0])) return;
- if ((typeof authCookie.n != 'string') || (authCookie.n != obj.dbNodeKey) || (typeof authCookie.u != 'string')) return;
-
- // Fetch the user
- const user = parent.users[authCookie.u];
- if (user == null) return;
-
- // Add this device as the authentication push notification device for this user
- if (authCookie.a == 'addAuth') {
- // Do nothing if authentication is not approved.
- // We do not want to indicate that the remote user responded to this.
- if (command.approved !== true) return;
-
- // Change the user
- user.otpdev = obj.dbNodeKey;
- parent.db.SetUser(user);
-
- // Notify change
- var targets = ['*', 'server-users', user._id];
- if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
- var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: 113, msg: "Added push notification authentication device", domain: domain.id };
- if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
- parent.parent.DispatchEvent(targets, obj, event);
- }
-
- // Complete 2FA checking
- if (authCookie.a == 'checkAuth') {
- if (typeof authCookie.s != 'string') return;
- // Notify 2FA response
- parent.parent.DispatchEvent(['2fadev-' + authCookie.s], obj, { etype: '2fadev', action: '2faresponse', domain: domain.id, nodeid: obj.dbNodeKey, code: authCookie.a, userid: user._id, approved: command.approved, sessionid: authCookie.s, nolog: 1 });
- }
-
- break;
- }
- case 'getUserImage': {
- // Validate input
- if (typeof command.userid != 'string') {
- // Send back the default image if required
- if ((command.default) || (command.sentDefault)) {
- try { command.image = 'data:image/png;base64,' + Buffer.from(parent.fs.readFileSync(parent.parent.path.join(__dirname, 'public', 'images', 'user-128.png')), 'binary').toString('base64'); } catch (ex) { }
- obj.send(JSON.stringify(command));
- }
- return;
- }
- var useridsplit = command.userid.split('/');
- if ((useridsplit.length != 3) || (useridsplit[1] != domain.id)) return;
-
- // Add the user's real name if present
- var u = parent.users[command.userid];
- if (u == null) return;
- if (u.name) { command.name = u.name; }
- if (u.realname) { command.realname = u.realname; }
-
- // An agent can only request images of accounts with rights to the device.
- if (parent.GetNodeRights(command.userid, obj.dbMeshKey, obj.dbNodeKey) != 0) {
- parent.db.Get('im' + command.userid, function (err, images) {
- if ((err == null) && (images != null) && (images.length == 1)) {
- // Send back the account image
- command.image = images[0].image;
- } else {
- // Send back the default image if required
- if ((command.default) || (command.sentDefault)) {
- try { command.image = 'data:image/png;base64,' + Buffer.from(parent.fs.readFileSync(parent.parent.path.join(__dirname, 'public', 'images', 'user-128.png')), 'binary').toString('base64'); } catch (ex) { }
- }
- }
- obj.send(JSON.stringify(command));
- });
- }
- break;
- }
- case 'getServerImage': {
- if (command.agent === 'assistant') {
- // Return server title and image for MeshCentral Assistant
- if ((domain.assistantcustomization != null) && (typeof domain.assistantcustomization == 'object')) {
- var ok = false;
- if (typeof domain.assistantcustomization.title == 'string') { ok = true; command.title = domain.assistantcustomization.title; }
- if (typeof domain.assistantcustomization.image == 'string') { try { command.image = 'data:image/jpeg;base64,' + Buffer.from(parent.fs.readFileSync(parent.parent.getConfigFilePath(domain.assistantcustomization.image)), 'binary').toString('base64'); ok = true; } catch (ex) { console.log(ex); } }
- if (ok) { obj.send(JSON.stringify(command)); }
- }
- }
- if (command.agent === 'android') {
- // Return server title and image for MeshCentral Assistant
- if ((domain.androidcustomization != null) && (typeof domain.androidcustomization == 'object')) {
- var ok = false;
- if (typeof domain.androidcustomization.title == 'string') { ok = true; command.title = domain.androidcustomization.title; }
- if (typeof domain.androidcustomization.subtitle == 'string') { ok = true; command.subtitle = domain.androidcustomization.subtitle; }
- if (typeof domain.androidcustomization.image == 'string') { try { command.image = 'data:image/jpeg;base64,' + Buffer.from(parent.fs.readFileSync(parent.parent.getConfigFilePath(domain.androidcustomization.image)), 'binary').toString('base64'); ok = true; } catch (ex) { console.log(ex); } }
- if (ok) { obj.send(JSON.stringify(command)); }
- }
- }
- break;
- }
- case 'guestShare': {
- if ((command.flags == null) || (command.flags == 0)) {
- // Stop any current self-share, this is allowed even if self guest sharing is not allows so to clear any old shares.
- removeGuestSharing(function () {
- delete obj.guestSharing;
- obj.send(JSON.stringify({ action: 'guestShare', flags: command.flags, url: null, viewOnly: false }));
- });
- } else {
- // Add a new self-share, this will replace any share for this device
- if ((domain.agentselfguestsharing == null) || (domain.agentselfguestsharing == false) || (typeof command.flags != 'number')) return; // Check if agent self-sharing is allowed, this is off by default.
- if ((command.flags & 2) == 0) { command.viewOnly = false; } // Only allow "view only" if desktop is shared.
- addGuestSharing(command.flags, command.viewOnly, function (share) {
- obj.guestSharing = true;
- obj.send(JSON.stringify({ action: 'guestShare', url: share.url, flags: share.flags, viewOnly: share.viewOnly }));
- })
- }
- break;
- }
- case 'amtconfig': {
- // Sent by the agent when the agent needs a Intel AMT APF connection to the server
- const cookie = parent.parent.encodeCookie({ a: 'apf', n: obj.dbNodeKey, m: obj.dbMeshKey }, parent.parent.loginCookieEncryptionKey);
- try { obj.send(JSON.stringify({ action: 'amtconfig', user: '**MeshAgentApfTunnel**', pass: cookie })); } catch (ex) { }
- break;
- }
- case 'script-task': {
- // These command are for running regular batch jobs on the remote device
- if (parent.parent.taskManager != null) { parent.parent.taskManager.agentAction(command, obj); }
- break;
- }
- default: {
- parent.agentStats.unknownAgentActionCount++;
- parent.parent.debug('agent', 'Unknown agent action (' + obj.remoteaddrport + '): ' + JSON.stringify(command) + '.');
- console.log('Unknown agent action (' + obj.remoteaddrport + '): ' + JSON.stringify(command) + '.');
- break;
- }
- }
- if (parent.parent.pluginHandler != null) {
- parent.parent.pluginHandler.callHook('hook_processAgentData', command, obj, parent);
- }
- }
- }
-
- function addGuestSharing(flags, viewOnly, func) {
- // Create cookie
- const publicid = 'AS:' + obj.dbNodeKey;
- const extrakey = getRandomAmtPassword();
- const cookie = { a: 6, pid: publicid, k: extrakey }; // New style sharing cookie
- const inviteCookie = parent.parent.encodeCookie(cookie, parent.parent.invitationLinkEncryptionKey);
- if (inviteCookie == null) return;
-
- // Create the server url
- var serverName = parent.getWebServerName(domain, req);
- var httpsPort = ((args.aliasport == null) ? args.port : args.aliasport); // Use HTTPS alias port is specified
- var xdomain = (domain.dns == null) ? domain.id : '';
- if (xdomain != '') xdomain += '/';
- var url = 'https://' + serverName + ':' + httpsPort + '/' + xdomain + 'sharing?c=' + inviteCookie;
- if (serverName.split('.') == 1) { url = '/' + xdomain + page + '?c=' + inviteCookie; }
-
- // Create a device sharing database entry
- var shareEntry = { _id: 'deviceshare-' + publicid, type: 'deviceshare', nodeid: obj.dbNodeKey, p: flags, domain: domain.id, publicid: publicid, guestName: 'Agent', consent: 0x7F, url: url, extrakey: extrakey };
-
- // Add expire time
- if ((typeof domain.agentselfguestsharing == 'object') && (typeof domain.agentselfguestsharing.expire == 'number') && (domain.agentselfguestsharing.expire > 0)) {
- shareEntry.startTime = Date.now();
- shareEntry.expireTime = Date.now() + (60000 * domain.agentselfguestsharing.expire);
- }
-
- if (viewOnly === true) { shareEntry.viewOnly = true; }
- parent.db.Set(shareEntry);
-
- // Send out an event that we added a device share
- var targets = parent.CreateNodeDispatchTargets(obj.dbMeshKey, obj.dbNodeKey);
- var event = { etype: 'node', nodeid: obj.dbNodeKey, action: 'addedDeviceShare', msg: 'Added device share with unlimited time', msgid: 131, msgArgs: ['Agent'], domain: domain.id };
- parent.parent.DispatchEvent(targets, obj, event);
-
- // Send device share update
- parent.db.GetAllTypeNodeFiltered([obj.dbNodeKey], domain.id, 'deviceshare', null, function (err, docs) {
- if (err != null) return;
-
- // Check device sharing
- var now = Date.now();
- for (var i = 0; i < docs.length; i++) {
- const doc = docs[i];
- if (doc.expireTime < now) { parent.db.Remove(doc._id, function () { }); delete docs[i]; } else {
- // This share is ok, remove extra data we don't need to send.
- delete doc._id; delete doc.domain; delete doc.nodeid; delete doc.type;
- }
- }
-
- // Send device share update
- var targets = parent.CreateNodeDispatchTargets(obj.dbMeshKey, obj.dbNodeKey, []);
- parent.parent.DispatchEvent(targets, obj, { etype: 'node', nodeid: obj.dbNodeKey, action: 'deviceShareUpdate', domain: domain.id, deviceShares: docs, nolog: 1 });
-
- // Callback
- if (func) { func({ url: url, flags: flags, viewOnly: viewOnly }); }
- });
- }
-
- function removeGuestSharing(func) {
- var publicid = 'AS:' + obj.dbNodeKey;
- parent.db.GetAllTypeNodeFiltered([obj.dbNodeKey], domain.id, 'deviceshare', null, function (err, docs) {
- if (err != null) return;
-
- // Remove device sharing
- var now = Date.now(), removedExact = null, removed = false, okDocs = [];
- for (var i = 0; i < docs.length; i++) {
- const doc = docs[i];
- if (doc.publicid == publicid) { parent.db.Remove(doc._id, function () { }); removedExact = doc; removed = true; }
- else if (doc.expireTime < now) { parent.db.Remove(doc._id, function () { }); removed = true; } else {
- // This share is ok, remove extra data we don't need to send.
- delete doc._id; delete doc.domain; delete doc.nodeid; delete doc.type;
- okDocs.push(doc);
- }
- }
-
- // Event device share removal
- if (removedExact != null) {
- // Send out an event that we removed a device share
- var targets = parent.CreateNodeDispatchTargets(obj.dbMeshKey, obj.dbNodeKey, ['server-shareremove']);
- var event = { etype: 'node', nodeid: obj.dbNodeKey, action: 'removedDeviceShare', msg: 'Removed Device Share', msgid: 102, msgArgs: ['Agent'], domain: domain.id, publicid: publicid };
- parent.parent.DispatchEvent(targets, obj, event);
- }
-
- // If we removed any shares, send device share update
- if (removed == true) {
- var targets = parent.CreateNodeDispatchTargets(obj.dbMeshKey, obj.dbNodeKey, []);
- parent.parent.DispatchEvent(targets, obj, { etype: 'node', nodeid: obj.dbNodeKey, action: 'deviceShareUpdate', domain: domain.id, deviceShares: okDocs, nolog: 1 });
- }
-
- // Call back when done
- if (func) func(removed);
- });
- }
-
- // Notify update of sessions
- obj.updateSessions = function () {
- // Perform some clean up
- for (var i in obj.sessions) { if (Object.keys(obj.sessions[i]).length == 0) { delete obj.sessions[i]; } }
- if (Object.keys(obj.sessions).length == 0) { delete obj.sessions; }
-
- // Event the new sessions, this will notify everyone that agent sessions have changed
- var event = { etype: 'node', action: 'devicesessions', nodeid: obj.dbNodeKey, domain: domain.id, sessions: obj.sessions, nolog: 1 };
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, event);
- }
-
- // Change the current core information string and event it
- function ChangeAgentCoreInfo(command) {
- if ((obj.agentInfo == null) || (obj.agentInfo.capabilities & 0x40)) return;
- if ((command == null) || (command == null)) return; // Safety, should never happen.
-
- // If the device is pending a change, hold.
- if (obj.deviceChanging === true) { setTimeout(function () { ChangeAgentCoreInfo(command); }, 100); return; }
- obj.deviceChanging = true;
-
- // Check that the mesh exists
- const mesh = parent.meshes[obj.dbMeshKey];
- if (mesh == null) { delete obj.deviceChanging; return; }
-
- // Get the node and change it if needed
- db.Get(obj.dbNodeKey, function (err, nodes) { // TODO: THIS IS A BIG RACE CONDITION HERE, WE NEED TO FIX THAT. If this call is made twice at the same time on the same device, data will be missed.
- if ((nodes == null) || (nodes.length != 1)) { delete obj.deviceChanging; return; }
- const device = nodes[0];
- if (device.agent) {
- var changes = [], change = 0, log = 0;
-
- // Check if anything changes
- if (command.name && (typeof command.name == 'string') && (command.name != device.name)) { change = 1; log = 1; device.name = command.name; changes.push('name'); }
- if ((command.caps != null) && (device.agent.core != command.value)) { if ((command.value == null) && (device.agent.core != null)) { delete device.agent.core; } else { device.agent.core = command.value; } change = 1; } // Don't save this as an event to the db.
- if ((command.caps != null) && ((device.agent.caps & 0xFFFFFFE7) != (command.caps & 0xFFFFFFE7))) { device.agent.caps = ((device.agent.caps & 24) + (command.caps & 0xFFFFFFE7)); change = 1; } // Allow Javascript on the agent to change all capabilities except console and javascript support, Don't save this as an event to the db.
- if ((command.osdesc != null) && (typeof command.osdesc == 'string') && (device.osdesc != command.osdesc)) { device.osdesc = command.osdesc; change = 1; changes.push('os desc'); } // Don't save this as an event to the db.
- if ((typeof command.root == 'boolean') && (command.root !== device.agent.root)) { change = 1; device.agent.root = command.root; }
- if (device.ip != obj.remoteaddr) { device.ip = obj.remoteaddr; change = 1; }
- if (command.intelamt) {
- if (!device.intelamt) { device.intelamt = {}; }
- if ((command.intelamt.Versions != null) && (typeof command.intelamt.Versions == 'object')) {
- if ((command.intelamt.Versions.AMT != null) && (typeof command.intelamt.Versions.AMT == 'string') && (command.intelamt.Versions.AMT.length < 12) && (device.intelamt.ver != command.intelamt.Versions.AMT)) { changes.push('AMT version'); device.intelamt.ver = command.intelamt.Versions.AMT; change = 1; log = 1; }
- if ((command.intelamt.Versions.Sku != null) && (typeof command.intelamt.Versions.Sku == 'string')) {
- const sku = parseInt(command.intelamt.Versions.Sku);
- if (device.intelamt.sku !== sku) { device.intelamt.sku = sku; change = 1; log = 1; }
- }
- }
- if ((command.intelamt.ProvisioningState != null) && (typeof command.intelamt.ProvisioningState == 'number') && (device.intelamt.state != command.intelamt.ProvisioningState)) { changes.push('AMT state'); device.intelamt.state = command.intelamt.ProvisioningState; change = 1; log = 1; }
- if ((command.intelamt.Flags != null) && (typeof command.intelamt.Flags == 'number') && (device.intelamt.flags != command.intelamt.Flags)) {
- if (device.intelamt.flags) { changes.push('AMT flags (' + device.intelamt.flags + ' --> ' + command.intelamt.Flags + ')'); } else { changes.push('AMT flags (' + command.intelamt.Flags + ')'); }
- device.intelamt.flags = command.intelamt.Flags; change = 1; log = 1;
- }
- if ((command.intelamt.UUID != null) && (typeof command.intelamt.UUID == 'string') && (device.intelamt.uuid != command.intelamt.UUID)) { changes.push('AMT uuid'); device.intelamt.uuid = command.intelamt.UUID; change = 1; log = 1; }
- }
- if (command.av != null) { // Antivirus
- if (!device.av) { device.av = []; }
- if (JSON.stringify(device.av) != JSON.stringify(command.av)) { /*changes.push('AV status');*/ device.av = command.av; change = 1; log = 1; }
- }
- if (command.wsc != null) { // Windows Security Center
- if (!device.wsc) { device.wsc = {}; }
- if (JSON.stringify(device.wsc) != JSON.stringify(command.wsc)) { /*changes.push('Windows Security Center status');*/ device.wsc = command.wsc; change = 1; log = 1; }
- }
- if (command.defender != null) { // Defender For Windows Server
- if (!device.defender) { device.defender = {}; }
- if (JSON.stringify(device.defender) != JSON.stringify(command.defender)) { /*changes.push('Defender status');*/ device.defender = command.defender; change = 1; log = 1; }
- }
- if (command.lastbootuptime != null) { // Last Boot Up Time
- if (!device.lastbootuptime) { device.lastbootuptime = ""; }
- if (device.lastbootuptime != command.lastbootuptime) { /*changes.push('Last Boot Up Time');*/ device.lastbootuptime = command.lastbootuptime; change = 1; log = 1; }
- }
-
- // Push Messaging Token
- if ((command.pmt != null) && (typeof command.pmt == 'string') && (device.pmt != command.pmt)) {
- if (typeof device.pmt == 'string') { db.Remove('pmt_' + device.pmt); }
- device.pmt = command.pmt;
- change = 1; // Don't save this change as an event to the db, so no log=1.
- parent.removePmtFromAllOtherNodes(device); // We need to make sure to remove this push messaging token from any other device on this server, all domains included.
- }
-
- if ((command.users != null) && (Array.isArray(command.users)) && (device.users != command.users)) { device.users = command.users; change = 1; } // Don't save this to the db.
- if ((command.lusers != null) && (Array.isArray(command.lusers)) && (device.lusers != command.lusers)) { device.lusers = command.lusers; change = 1; } // Don't save this to the db.
- if ((mesh.mtype == 2) && (!args.wanonly)) {
- // In WAN mode, the hostname of a computer is not important. Don't log hostname changes.
- if (device.host != obj.remoteaddr) { device.host = obj.remoteaddr; change = 1; changes.push('host'); }
- // TODO: Check that the agent has an interface that is the same as the one we got this websocket connection on. Only set if we have a match.
- }
-
- // Remove old volumes and BitLocker data, this is part of sysinfo.
- delete device.volumes;
-
- // If there are changes, event the new device
- if (change == 1) {
- // Do some clean up if needed, these values should not be in the database.
- if (device.conn != null) { delete device.conn; }
- if (device.pwr != null) { delete device.pwr; }
- if (device.agct != null) { delete device.agct; }
- if (device.cict != null) { delete device.cict; }
-
- // Save to the database
- db.Set(device);
-
- // Event the node change
- var event = { etype: 'node', action: 'changenode', nodeid: obj.dbNodeKey, domain: domain.id, node: parent.CloneSafeNode(device) };
- if (changes.length > 0) { event.msg = 'Changed device ' + device.name + ' from group ' + mesh.name + ': ' + changes.join(', '); }
- if ((log == 0) || ((obj.agentInfo) && (obj.agentInfo.capabilities) && (obj.agentInfo.capabilities & 0x20)) || (changes.length == 0)) { event.nolog = 1; } // If this is a temporary device, don't log changes
- if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(device.meshid, [obj.dbNodeKey]), obj, event);
- }
-
- // Device change is done.
- delete obj.deviceChanging;
- }
- });
- }
-
- // Change the current core information string and event it
- function ChangeAgentLocationInfo(command) {
- if (obj.agentInfo.capabilities & 0x40) return;
- if ((command == null) || (command == null)) { return; } // Safety, should never happen.
-
- // Check that the mesh exists
- const mesh = parent.meshes[obj.dbMeshKey];
- if (mesh == null) return;
-
- // If the device is pending a change, hold.
- if (obj.deviceChanging === true) { setTimeout(function () { ChangeAgentLocationInfo(command); }, 100); return; }
- obj.deviceChanging = true;
-
- // Get the node and change it if needed
- db.Get(obj.dbNodeKey, function (err, nodes) {
- if ((nodes == null) || (nodes.length != 1)) { delete obj.deviceChanging; return; }
- const device = nodes[0];
- if (device.agent) {
- var changes = [], change = 0;
-
- // Check if anything changes
- if ((command.publicip) && (device.publicip != command.publicip)) { device.publicip = command.publicip; change = 1; changes.push('public ip'); }
- if ((command.iploc) && (device.iploc != command.iploc)) { device.iploc = command.iploc; change = 1; changes.push('ip location'); }
-
- // If there are changes, save and event
- if (change == 1) {
- // Do some clean up if needed, these values should not be in the database.
- if (device.conn != null) { delete device.conn; }
- if (device.pwr != null) { delete device.pwr; }
- if (device.agct != null) { delete device.agct; }
- if (device.cict != null) { delete device.cict; }
-
- // Save the device
- db.Set(device);
-
- // Event the node change
- var event = { etype: 'node', action: 'changenode', nodeid: obj.dbNodeKey, domain: domain.id, node: parent.CloneSafeNode(device), msgid: 59, msgArgs: [device.name, mesh.name, changes.join(', ')], msg: 'Changed device ' + device.name + ' from group ' + mesh.name + ': ' + changes.join(', ') };
- if (obj.agentInfo.capabilities & 0x20) { event.nolog = 1; } // If this is a temporary device, don't log changes
- if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(device.meshid, [obj.dbNodeKey]), obj, event);
- }
- }
-
- // Done changing the device
- delete obj.deviceChanging;
- });
- }
-
- // Update the mesh agent tab in the database
- function ChangeAgentTag(tag) {
- if ((obj.agentInfo == null) || (obj.agentInfo.capabilities & 0x40)) return;
- if ((tag != null) && (tag.length == 0)) { tag = null; }
-
- // If the device is pending a change, hold.
- if (obj.deviceChanging === true) {
- var func = function ChangeAgentTagFunc() { ChangeAgentCoreInfo(ChangeAgentTagFunc.tag); }
- func.tag = tag;
- setTimeout(func, 100);
- return;
- }
- obj.deviceChanging = true;
-
- // Get the node and change it if needed
- db.Get(obj.dbNodeKey, function (err, nodes) {
- if ((nodes == null) || (nodes.length != 1)) { delete obj.deviceChanging; return; }
- const device = nodes[0];
- if (device.agent) {
- // Parse the agent tag
- var agentTag = null, serverName = null, serverDesc = null, serverTags = null;
- if (tag != null) {
- var taglines = tag.split('\r\n').join('\n').split('\r').join('\n').split('\n');
- for (var i in taglines) {
- var tagline = taglines[i].trim();
- if (tagline.length > 0) {
- if (tagline.startsWith('~')) {
- if (tagline.startsWith('~ServerName:') && (tagline.length > 12) && (serverName == null)) { serverName = tagline.substring(12).trim(); }
- if (tagline.startsWith('~ServerDesc:') && (tagline.length > 12) && (serverDesc == null)) { serverDesc = tagline.substring(12).trim(); }
- if (tagline.startsWith('~ServerTags:') && (tagline.length > 12) && (serverTags == null)) { serverTags = tagline.substring(12).split(','); for (var j in serverTags) { serverTags[j] = serverTags[j].trim(); } }
- } else { if (agentTag == null) { agentTag = tagline; } }
- }
- }
- }
-
- // Set the agent tag
- var changes = false;
- if (device.agent.tag != agentTag) { device.agent.tag = agentTag; if ((device.agent.tag == null) || (device.agent.tag == '')) { delete device.agent.tag; } changes = true; }
- if (domain.agenttag != null) {
- // Set the device's server name
- if ((serverName != null) && (domain.agenttag.servername === 1) && (device.name != serverName)) { device.name = serverName; changes = true; }
-
- // Set the device's server description
- if ((serverDesc != null) && (domain.agenttag.serverdesc === 1) && (device.desc != serverDesc)) { device.desc = serverDesc; changes = true; }
-
- // Set the device's server description if there is no description
- if ((serverDesc != null) && (domain.agenttag.serverdesc === 2) && (device.desc != serverDesc) && ((device.desc == null) || (device.desc == ''))) { device.desc = serverDesc; changes = true; }
-
- if ((serverTags != null) && (domain.agenttag.servertags != null) && (domain.agenttag.servertags != 0)) {
- // Sort the tags
- serverTags.sort();
-
- // Stringify the tags
- var st2 = '', st1 = serverTags.join(',');
- if (device.tags != null) { st2 = device.tags.join(','); }
-
- // Set the device's server tags
- if ((domain.agenttag.servertags === 1) && (st1 != st2)) { device.tags = serverTags; changes = true; }
-
- // Set the device's server tags if there are not tags
- if ((domain.agenttag.servertags === 2) && (st2 == '')) { device.tags = serverTags; changes = true; }
-
- // Append to device's server tags
- if ((domain.agenttag.servertags === 3) && (st1 != st2)) {
- if (device.tags == null) { device.tags = []; }
- for (var i in serverTags) { if (device.tags.indexOf(serverTags[i]) == -1) { device.tags.push(serverTags[i]); } }
- device.tags.sort();
- changes = true;
- }
- }
- }
-
- if (changes == true) {
- // Do some clean up if needed, these values should not be in the database.
- if (device.conn != null) { delete device.conn; }
- if (device.pwr != null) { delete device.pwr; }
- if (device.agct != null) { delete device.agct; }
- if (device.cict != null) { delete device.cict; }
-
- // Update the device
- db.Set(device);
-
- // Event the node change
- var event = { etype: 'node', action: 'changenode', nodeid: obj.dbNodeKey, domain: domain.id, node: parent.CloneSafeNode(device), nolog: 1 };
- if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(device.meshid, [obj.dbNodeKey]), obj, event);
- }
- }
-
- // Done changing the device
- delete obj.deviceChanging;
- });
- }
-
- // Check if we need to update this agent, return true if agent binary update required.
- // Return 0 is no update needed, 1 update using native system, 2 update using meshcore system
- function compareAgentBinaryHash(agentExeInfo, agentHash) {
- // If this is a temporary agent and the server is set to not update temporary agents, don't update the agent.
- if ((obj.agentInfo.capabilities & 0x20) && (args.temporaryagentupdate === false)) return 0;
- // If we are testing the agent update system, always return true
- if ((args.agentupdatetest === true) || (args.agentupdatetest === 1)) return 1;
- if (args.agentupdatetest === 2) return 2;
- // If the hash matches or is null, no update required.
- if ((agentExeInfo.hash == agentHash) || (agentHash == '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0')) return 0;
- // If this is a macOS x86 or ARM agent type and it matched the universal binary, no update required.
- if ((agentExeInfo.id == 16) || (agentExeInfo.id == 29)) {
- if (domain.meshAgentBinaries && domain.meshAgentBinaries[10005]) {
- if (domain.meshAgentBinaries[10005].hash == agentHash) return 0;
- } else {
- if (parent.parent.meshAgentBinaries[10005].hash == agentHash) return 0;
- }
- }
-
- // No match, update the agent.
- if (args.agentupdatesystem === 2) return 2; // If set, force a meshcore update.
- if (agentExeInfo.id == 3) return 2; // Due to a bug in Windows 7 SP1 environement variable exec, we always update 32bit Windows agent using MeshCore for now. Upcoming agent will have a fix for this.
- // NOTE: Windows agents with no commit dates may have bad native update system, so use meshcore system instead.
- // NOTE: Windows agents with commit date prior to 1612740413000 did not kill all "meshagent.exe" processes and update could fail as a result executable being locked, meshcore system will do this.
- if (((obj.AgentCommitDate == null) || (obj.AgentCommitDate < 1612740413000)) && ((agentExeInfo.id == 3) || (agentExeInfo.id == 4))) return 2; // For older Windows agents, use the meshcore update technique.
- return 1; // By default, use the native update technique.
- }
-
- // Request that the core dump file on this agent be uploaded to the server
- obj.RequestCoreDump = function (agenthashhex, corehashhex) {
- if (agenthashhex.length > 16) { agenthashhex = agenthashhex.substring(0, 16); }
- const cookie = parent.parent.encodeCookie({ a: 'aft', b: 'coredump', c: obj.agentInfo.agentId + '-' + agenthashhex + '-' + obj.nodeid + '.dmp' }, parent.parent.loginCookieEncryptionKey);
- obj.send('{"action":"msg","type":"tunnel","value":"*/' + (((domain.dns == null) && (domain.id != '')) ? (domain.id + '/') : '') + 'agenttransfer.ashx?c=' + cookie + '","rights":"4294967295"}');
- }
-
- // Return true if we need to ignore the agent hash check
- function isIgnoreHashCheck() {
- if ((args.ignoreagenthashcheck === true) || (domain.ignoreagenthashcheck === true)) return true;
-
- // Check site wide exceptions
- if (Array.isArray(args.ignoreagenthashcheck)) {
- for (var i = 0; i < args.ignoreagenthashcheck.length; i++) {
- if (require('ipcheck').match(obj.remoteaddr, args.ignoreagenthashcheck[i])) return true;
- }
- }
-
- // Check domain wide exceptions
- if (Array.isArray(domain.ignoreagenthashcheck)) {
- for (var i = 0; i < domain.ignoreagenthashcheck.length; i++) {
- if (require('ipcheck').match(obj.remoteaddr, domain.ignoreagenthashcheck[i])) return true;
- }
- }
-
- return false;
- }
-
- // Generate a random Intel AMT password
- function checkAmtPassword(p) { return (p.length > 7) && (/\d/.test(p)) && (/[a-z]/.test(p)) && (/[A-Z]/.test(p)) && (/\W/.test(p)); }
- function getRandomAmtPassword() { var p; do { p = Buffer.from(parent.crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); } while (checkAmtPassword(p) == false); return p; }
-
- return obj;
-};
+/**
+* @description MeshCentral MeshAgent communication module
+* @author Ylian Saint-Hilaire & Bryan Roe
+* @copyright Intel Corporation 2018-2022
+* @license Apache-2.0
+* @version v0.0.1
+*/
+
+/*xjslint node: true */
+/*xjslint plusplus: true */
+/*xjslint maxlen: 256 */
+/*jshint node: true */
+/*jshint strict: false */
+/*jshint esversion: 6 */
+"use strict";
+
+// Construct a MeshAgent object, called upon connection
+module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
+ const forge = parent.parent.certificateOperations.forge;
+ const common = parent.parent.common;
+ parent.agentStats.createMeshAgentCount++;
+ parent.parent.debug('agent', 'New agent at ' + req.clientIp + ':' + ws._socket.remotePort);
+
+ var obj = {};
+ obj.domain = domain;
+ obj.authenticated = 0;
+ obj.receivedCommands = 0;
+ obj.agentCoreCheck = 0;
+ obj.remoteaddr = req.clientIp;
+ obj.remoteaddrport = obj.remoteaddr + ':' + ws._socket.remotePort;
+ obj.nonce = parent.crypto.randomBytes(48).toString('binary');
+ //ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive, 4 minutes
+ if (args.agentidletimeout != 0) { ws._socket.setTimeout(args.agentidletimeout, function () { obj.close(1); }); } // Inactivity timeout of 2:30 minutes, by default agent will WebSocket ping every 2 minutes and server will pong back.
+ //obj.nodeid = null;
+ //obj.meshid = null;
+ //obj.dbNodeKey = null;
+ //obj.dbMeshKey = null;
+ //obj.connectTime = null;
+ //obj.agentInfo = null;
+
+ ws._socket.bytesReadEx = 0;
+ ws._socket.bytesWrittenEx = 0;
+
+ // Perform data accounting
+ function dataAccounting() {
+ parent.trafficStats.AgentCtrlIn += (ws._socket.bytesRead - ws._socket.bytesReadEx);
+ parent.trafficStats.AgentCtrlOut += (ws._socket.bytesWritten - ws._socket.bytesWrittenEx);
+ ws._socket.bytesReadEx = ws._socket.bytesRead;
+ ws._socket.bytesWrittenEx = ws._socket.bytesWritten;
+ }
+
+ // Send a message to the mesh agent
+ obj.send = function (data, func) { try { if (typeof data == 'string') { ws.send(Buffer.from(data), func); } else { ws.send(data, func); } } catch (e) { } };
+ obj.sendBinary = function (data, func) { try { if (typeof data == 'string') { ws.send(Buffer.from(data, 'binary'), func); } else { ws.send(data, func); } } catch (e) { } };
+
+ // Disconnect this agent
+ obj.close = function (arg) {
+ dataAccounting();
+
+ if ((arg == 1) || (arg == null)) { try { ws.close(); if (obj.nodeid != null) { parent.parent.debug('agent', 'Soft disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ')'); } } catch (e) { console.log(e); } } // Soft close, close the websocket
+ if (arg == 2) {
+ try {
+ if (ws._socket._parent != null)
+ ws._socket._parent.end();
+ else
+ ws._socket.end();
+
+ if (obj.nodeid != null) {
+ parent.parent.debug('agent', 'Hard disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ')');
+ }
+ } catch (e) { console.log(e); }
+ }
+ // If arg == 2, hard close, close the TCP socket
+ // If arg == 3, don't communicate with this agent anymore, but don't disconnect (Duplicate agent).
+
+ // Stop any current self-share
+ if (obj.guestSharing === true) { removeGuestSharing(); }
+
+ // Remove this agent from the webserver list
+ if (parent.wsagents[obj.dbNodeKey] == obj) {
+ delete parent.wsagents[obj.dbNodeKey];
+ parent.parent.ClearConnectivityState(obj.dbMeshKey, obj.dbNodeKey, 1, null, { remoteaddrport: obj.remoteaddrport, name: obj.name });
+ }
+
+ // Remove this agent from the list of agents with bad web certificates
+ if (obj.badWebCert) { delete parent.wsagentsWithBadWebCerts[obj.badWebCert]; }
+
+ // Get the current mesh
+ const mesh = parent.meshes[obj.dbMeshKey];
+
+ // If this is a temporary or recovery agent, or all devices in this group are temporary, remove the agent (0x20 = Temporary, 0x40 = Recovery)
+ if (((obj.agentInfo) && (obj.agentInfo.capabilities) && ((obj.agentInfo.capabilities & 0x20) || (obj.agentInfo.capabilities & 0x40))) || ((mesh) && (mesh.flags) && (mesh.flags & 1))) {
+ // Delete this node including network interface information and events
+ db.Remove(obj.dbNodeKey); // Remove node with that id
+ db.Remove('if' + obj.dbNodeKey); // Remove interface information
+ db.Remove('nt' + obj.dbNodeKey); // Remove notes
+ db.Remove('lc' + obj.dbNodeKey); // Remove last connect time
+ db.Remove('si' + obj.dbNodeKey); // Remove system information
+ db.Remove('al' + obj.dbNodeKey); // Remove error log last time
+ if (db.RemoveSMBIOS) { db.RemoveSMBIOS(obj.dbNodeKey); } // Remove SMBios data
+ db.RemoveAllNodeEvents(obj.dbNodeKey); // Remove all events for this node
+ db.removeAllPowerEventsForNode(obj.dbNodeKey); // Remove all power events for this node
+
+ // Event node deletion
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, { etype: 'node', action: 'removenode', nodeid: obj.dbNodeKey, domain: domain.id, nolog: 1 });
+
+ // Disconnect all connections if needed
+ const state = parent.parent.GetConnectivityState(obj.dbNodeKey);
+ if ((state != null) && (state.connectivity != null)) {
+ if ((state.connectivity & 1) != 0) { parent.wsagents[obj.dbNodeKey].close(); } // Disconnect mesh agent
+ if ((state.connectivity & 2) != 0) { parent.parent.mpsserver.closeAllForNode(obj.dbNodeKey); } // Disconnect CIRA connection
+ }
+ }
+
+ // Set this agent as no longer authenticated
+ obj.authenticated = -1;
+
+ // If we where updating the agent using native method, clean that up.
+ if (obj.agentUpdate != null) {
+ if (obj.agentUpdate.fd) { try { parent.fs.close(obj.agentUpdate.fd); } catch (ex) { } }
+ parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete
+ delete obj.agentUpdate.buf;
+ delete obj.agentUpdate;
+ }
+
+ // If we where updating the agent meshcore method, clean that up.
+ if (obj.agentCoreUpdateTaskId != null) {
+ parent.parent.taskLimiter.completed(obj.agentCoreUpdateTaskId);
+ delete obj.agentCoreUpdateTaskId;
+ }
+
+ // Perform timer cleanup
+ if (obj.pingtimer) { clearInterval(obj.pingtimer); delete obj.pingtimer; }
+ if (obj.pongtimer) { clearInterval(obj.pongtimer); delete obj.pongtimer; }
+
+ // Perform aggressive cleanup
+ delete obj.name;
+ delete obj.nonce;
+ delete obj.nodeid;
+ delete obj.unauth;
+ delete obj.remoteaddr;
+ delete obj.remoteaddrport;
+ delete obj.meshid;
+ delete obj.connectTime;
+ delete obj.agentInfo;
+ delete obj.agentExeInfo;
+ ws.removeAllListeners(['message', 'close', 'error']);
+ };
+
+ // When data is received from the mesh agent web socket
+ ws.on('message', function (msg) {
+ dataAccounting();
+ if (msg.length < 2) return;
+ if (typeof msg == 'object') { msg = msg.toString('binary'); } // TODO: Could change this entire method to use Buffer instead of binary string
+ if (obj.authenticated == 2) { // We are authenticated
+ if ((obj.agentUpdate == null) && (msg.charCodeAt(0) == 123)) { processAgentData(msg); } // Only process JSON messages if meshagent update is not in progress
+ if (msg.length < 2) return;
+ const cmdid = common.ReadShort(msg, 0);
+ if (cmdid == 11) { // MeshCommand_CoreModuleHash
+ if (msg.length == 4) { ChangeAgentCoreInfo({ 'caps': 0 }); } // If the agent indicated that no core is running, clear the core information string.
+ // Mesh core hash, sent by agent with the hash of the current mesh core.
+
+ // If we are performing an agent update, don't update the core.
+ if (obj.agentUpdate != null) { return; }
+
+ // If we are using a custom core, don't try to update it.
+ if (obj.agentCoreCheck == 1000) {
+ obj.sendBinary(common.ShortToStr(16) + common.ShortToStr(0)); // MeshCommand_CoreOk. Indicates to the agent that the core is ok. Start it if it's not already started.
+ agentCoreIsStable();
+ return;
+ }
+
+ // Get the current meshcore hash
+ const agentMeshCoreHash = (msg.length == 52) ? msg.substring(4, 52) : null;
+
+ // If the agent indicates this is a custom core, we are done.
+ if ((agentMeshCoreHash != null) && (agentMeshCoreHash == '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0')) {
+ obj.agentCoreCheck = 0;
+ obj.sendBinary(common.ShortToStr(16) + common.ShortToStr(0)); // MeshCommand_CoreOk. Indicates to the agent that the core is ok. Start it if it's not already started.
+ agentCoreIsStable();
+ return;
+ }
+
+ // We need to check if the core is current. Figure out what core we need.
+ var corename = null;
+ if ((obj.agentInfo != null) && (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId] != null)) {
+ if ((obj.agentCoreCheck == 1001) || (obj.agentCoreUpdate == true)) {
+ // If the user asked, use the recovery core.
+ corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].rcore;
+ } else if (obj.agentCoreCheck == 1011) {
+ // If the user asked, use the tiny core.
+ corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].tcore;
+ } else if (obj.agentInfo.capabilities & 0x40) {
+ // If this is a recovery agent, use the agent recovery core.
+ corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].arcore;
+ } else {
+ // This is the normal core for this agent type.
+ corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].core;
+ }
+ }
+
+ // If we have a core, use it.
+ if (corename != null) {
+ const meshcorehash = parent.parent.defaultMeshCoresHash[corename];
+ if (agentMeshCoreHash != meshcorehash) {
+ if ((obj.agentCoreCheck < 5) || (obj.agentCoreCheck == 1001) || (obj.agentCoreCheck == 1011) || (obj.agentCoreUpdate == true)) {
+ if (meshcorehash == null) {
+ // Clear the core
+ obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0)); // MeshCommand_CoreModule, ask mesh agent to clear the core
+ parent.agentStats.clearingCoreCount++;
+ parent.parent.debug('agent', "Clearing core");
+ } else {
+ // Setup task limiter options, this system limits how many tasks can run at the same time to spread the server load.
+ var taskLimiterOptions = { hash: meshcorehash, core: parent.parent.defaultMeshCores[corename], name: corename };
+
+ // If the agent supports compression, sent the core compressed.
+ if ((obj.agentInfo.capabilities & 0x100) && (parent.parent.defaultMeshCoresDeflate[corename])) {
+ args.core = parent.parent.defaultMeshCoresDeflate[corename];
+ }
+
+ // Update new core with task limiting so not to flood the server. This is a high priority task.
+ obj.agentCoreUpdatePending = true;
+ parent.parent.taskLimiter.launch(function (argument, taskid, taskLimiterQueue) {
+ if (obj.authenticated == 2) {
+ // Send the updated core.
+ delete obj.agentCoreUpdatePending;
+ obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0) + argument.hash + argument.core.toString('binary'), function () { parent.parent.taskLimiter.completed(taskid); }); // MeshCommand_CoreModule, start core update
+ parent.agentStats.updatingCoreCount++;
+ parent.parent.debug('agent', "Updating core " + argument.name);
+ } else {
+ // This agent is probably disconnected, nothing to do.
+ parent.parent.taskLimiter.completed(taskid);
+ }
+ }, taskLimiterOptions, 0);
+ }
+ obj.agentCoreCheck++;
+ }
+ } else {
+ obj.agentCoreCheck = 0;
+ obj.sendBinary(common.ShortToStr(16) + common.ShortToStr(0)); // MeshCommand_CoreOk. Indicates to the agent that the core is ok. Start it if it's not already started.
+ agentCoreIsStable(); // No updates needed, agent is ready to go.
+ }
+ }
+
+ /*
+ // TODO: Check if we have a mesh specific core. If so, use that.
+ var agentMeshCoreHash = null;
+ if (msg.length == 52) { agentMeshCoreHash = msg.substring(4, 52); }
+ if ((agentMeshCoreHash != parent.parent.defaultMeshCoreHash) && (agentMeshCoreHash != parent.parent.defaultMeshCoreNoMeiHash)) {
+ if (obj.agentCoreCheck < 5) { // This check is in place to avoid a looping core update.
+ if (parent.parent.defaultMeshCoreHash == null) {
+ // Update no core
+ obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0)); // Command 10, ask mesh agent to clear the core
+ } else {
+ // Update new core
+ if ((parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId] != null) && (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].amt == true)) {
+ obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0) + parent.parent.defaultMeshCoreHash + parent.parent.defaultMeshCore); // Command 10, ask mesh agent to set the core (with MEI support)
+ } else {
+ obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0) + parent.parent.defaultMeshCoreNoMeiHash + parent.parent.defaultMeshCoreNoMei); // Command 10, ask mesh agent to set the core (No MEI)
+ }
+ }
+ obj.agentCoreCheck++;
+ }
+ } else {
+ obj.agentCoreCheck = 0;
+ }
+ */
+ }
+ else if (cmdid == 12) { // MeshCommand_AgentHash
+ if ((msg.length == 52) && (obj.agentExeInfo != null) && (obj.agentExeInfo.update == true)) {
+ const agenthash = msg.substring(4);
+ const agentUpdateMethod = compareAgentBinaryHash(obj.agentExeInfo, agenthash);
+ if (agentUpdateMethod === 2) { // Use meshcore agent update system
+ // Send the recovery core to the agent, if the agent is capable of running one
+ if (((obj.agentInfo.capabilities & 16) != 0) && (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].core != null)) {
+ parent.agentStats.agentMeshCoreBinaryUpdate++;
+ obj.agentCoreUpdate = true;
+ obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0)); // Ask to clear the core
+ obj.sendBinary(common.ShortToStr(11) + common.ShortToStr(0)); // Ask for meshcore hash
+ }
+ } else if (agentUpdateMethod === 1) { // Use native agent update system
+ // Mesh agent update required, do it using task limiter so not to flood the network. Medium priority task.
+ parent.parent.taskLimiter.launch(function (argument, taskid, taskLimiterQueue) {
+ if (obj.authenticated != 2) { parent.parent.taskLimiter.completed(taskid); return; } // If agent disconnection, complete and exit now.
+ if (obj.nodeid != null) { parent.parent.debug('agent', "Agent update required, NodeID=0x" + obj.nodeid.substring(0, 16) + ', ' + obj.agentExeInfo.desc); }
+ parent.agentStats.agentBinaryUpdate++;
+ if ((obj.agentExeInfo.data == null) && (((obj.agentInfo.capabilities & 0x100) == 0) || (obj.agentExeInfo.zdata == null))) {
+ // Read the agent from disk
+ parent.fs.open(obj.agentExeInfo.path, 'r', function (err, fd) {
+ if (obj.agentExeInfo == null) return; // Agent disconnected during this call.
+ if (err) { parent.parent.debug('agentupdate', "ERROR: " + err); return console.error(err); }
+ obj.agentUpdate = { ptr: 0, buf: Buffer.alloc(parent.parent.agentUpdateBlockSize + 4), fd: fd, taskid: taskid };
+
+ // MeshCommand_CoreModule, ask mesh agent to clear the core.
+ // The new core will only be sent after the agent updates.
+ obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0));
+
+ // We got the agent file open on the server side, tell the agent we are sending an update ending with the SHA384 hash of the result
+ //console.log("Agent update file open.");
+ obj.sendBinary(common.ShortToStr(13) + common.ShortToStr(0)); // Command 13, start mesh agent download
+
+ // Send the first mesh agent update data block
+ obj.agentUpdate.buf[0] = 0;
+ obj.agentUpdate.buf[1] = 14;
+ obj.agentUpdate.buf[2] = 0;
+ obj.agentUpdate.buf[3] = 1;
+ parent.fs.read(obj.agentUpdate.fd, obj.agentUpdate.buf, 4, parent.parent.agentUpdateBlockSize, obj.agentUpdate.ptr, function (err, bytesRead, buffer) {
+ if (obj.agentUpdate == null) return;
+ if ((err != null) || (bytesRead == 0)) {
+ // Error reading the agent file, stop here.
+ try { parent.fs.close(obj.agentUpdate.fd); } catch (ex) { }
+ parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete
+ parent.parent.debug('agentupdate', "ERROR: Unable to read first block of agent binary from disk.");
+ delete obj.agentUpdate.buf;
+ delete obj.agentUpdate;
+ } else {
+ // Send the first block to the agent
+ obj.agentUpdate.ptr += bytesRead;
+ parent.parent.debug('agentupdate', "Sent first block of " + bytesRead + " bytes from disk.");
+ obj.sendBinary(obj.agentUpdate.buf); // Command 14, mesh agent first data block
+ }
+ });
+ });
+ } else {
+ // Send the agent from RAM
+ obj.agentUpdate = { ptr: 0, buf: Buffer.alloc(parent.parent.agentUpdateBlockSize + 4), taskid: taskid };
+
+ // MeshCommand_CoreModule, ask mesh agent to clear the core.
+ // The new core will only be sent after the agent updates.
+ obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0));
+
+ // We got the agent file open on the server side, tell the agent we are sending an update ending with the SHA384 hash of the result
+ obj.sendBinary(common.ShortToStr(13) + common.ShortToStr(0)); // Command 13, start mesh agent download
+
+ // Send the first mesh agent update data block
+ obj.agentUpdate.buf[0] = 0;
+ obj.agentUpdate.buf[1] = 14;
+ obj.agentUpdate.buf[2] = 0;
+ obj.agentUpdate.buf[3] = 1;
+
+ // If agent supports compression, send the compressed agent if possible.
+ if ((obj.agentInfo.capabilities & 0x100) && (obj.agentExeInfo.zdata != null)) {
+ // Send compressed data
+ obj.agentUpdate.agentUpdateData = obj.agentExeInfo.zdata;
+ obj.agentUpdate.agentUpdateHash = obj.agentExeInfo.zhash;
+ } else {
+ // Send uncompressed data
+ obj.agentUpdate.agentUpdateData = obj.agentExeInfo.data;
+ obj.agentUpdate.agentUpdateHash = obj.agentExeInfo.hash;
+ }
+
+ const len = Math.min(parent.parent.agentUpdateBlockSize, obj.agentUpdate.agentUpdateData.length - obj.agentUpdate.ptr);
+ if (len > 0) {
+ // Send the first block
+ obj.agentUpdate.agentUpdateData.copy(obj.agentUpdate.buf, 4, obj.agentUpdate.ptr, obj.agentUpdate.ptr + len);
+ obj.agentUpdate.ptr += len;
+ obj.sendBinary(obj.agentUpdate.buf); // Command 14, mesh agent first data block
+ parent.parent.debug('agentupdate', "Sent first block of " + len + " bytes from RAM.");
+ } else {
+ // Error
+ parent.parent.debug('agentupdate', "ERROR: Len of " + len + " is invalid.");
+ parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete
+ delete obj.agentUpdate.buf;
+ delete obj.agentUpdate;
+ }
+ }
+ }, null, 1);
+
+ } else {
+ // Check the mesh core, if the agent is capable of running one
+ if (((obj.agentInfo.capabilities & 16) != 0) && (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].core != null)) {
+ obj.sendBinary(common.ShortToStr(11) + common.ShortToStr(0)); // Command 11, ask for mesh core hash.
+ }
+ }
+ }
+ }
+ else if (cmdid == 14) { // MeshCommand_AgentBinaryBlock
+ if ((msg.length == 4) && (obj.agentUpdate != null)) {
+ const status = common.ReadShort(msg, 2);
+ if (status == 1) {
+ if (obj.agentExeInfo.data == null) {
+ // Read the agent from disk
+ parent.fs.read(obj.agentUpdate.fd, obj.agentUpdate.buf, 4, parent.parent.agentUpdateBlockSize, obj.agentUpdate.ptr, function (err, bytesRead, buffer) {
+ if ((obj.agentExeInfo == null) || (obj.agentUpdate == null)) return; // Agent disconnected during this async call.
+ if ((err != null) || (bytesRead < 0)) {
+ // Error reading the agent file, stop here.
+ parent.parent.debug('agentupdate', "ERROR: Unable to read agent #" + obj.agentExeInfo.id + " binary from disk.");
+ try { parent.fs.close(obj.agentUpdate.fd); } catch (ex) { }
+ parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete
+ delete obj.agentUpdate.buf;
+ delete obj.agentUpdate;
+ } else {
+ // Send the next block to the agent
+ parent.parent.debug('agentupdate', "Sending disk agent #" + obj.agentExeInfo.id + " block, ptr=" + obj.agentUpdate.ptr + ", len=" + bytesRead + ".");
+ obj.agentUpdate.ptr += bytesRead;
+ if (bytesRead == parent.parent.agentUpdateBlockSize) { obj.sendBinary(obj.agentUpdate.buf); } else { obj.sendBinary(obj.agentUpdate.buf.slice(0, bytesRead + 4)); } // Command 14, mesh agent next data block
+ if ((bytesRead < parent.parent.agentUpdateBlockSize) || (obj.agentUpdate.ptr == obj.agentExeInfo.size)) {
+ parent.parent.debug('agentupdate', "Completed agent #" + obj.agentExeInfo.id + " update from disk, ptr=" + obj.agentUpdate.ptr + ".");
+ obj.sendBinary(common.ShortToStr(13) + common.ShortToStr(0) + obj.agentExeInfo.hash); // Command 13, end mesh agent download, send agent SHA384 hash
+ try { parent.fs.close(obj.agentUpdate.fd); } catch (ex) { }
+ parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete
+ delete obj.agentUpdate.buf;
+ delete obj.agentUpdate;
+ }
+ }
+ });
+ } else {
+ // Send the agent from RAM
+ const len = Math.min(parent.parent.agentUpdateBlockSize, obj.agentUpdate.agentUpdateData.length - obj.agentUpdate.ptr);
+ if (len > 0) {
+ obj.agentUpdate.agentUpdateData.copy(obj.agentUpdate.buf, 4, obj.agentUpdate.ptr, obj.agentUpdate.ptr + len);
+ if (len == parent.parent.agentUpdateBlockSize) { obj.sendBinary(obj.agentUpdate.buf); } else { obj.sendBinary(obj.agentUpdate.buf.slice(0, len + 4)); } // Command 14, mesh agent next data block
+ parent.parent.debug('agentupdate', "Sending RAM agent #" + obj.agentExeInfo.id + " block, ptr=" + obj.agentUpdate.ptr + ", len=" + len + ".");
+ obj.agentUpdate.ptr += len;
+ }
+
+ if (obj.agentUpdate.ptr == obj.agentUpdate.agentUpdateData.length) {
+ parent.parent.debug('agentupdate', "Completed agent #" + obj.agentExeInfo.id + " update from RAM, ptr=" + obj.agentUpdate.ptr + ".");
+ obj.sendBinary(common.ShortToStr(13) + common.ShortToStr(0) + obj.agentUpdate.agentUpdateHash); // Command 13, end mesh agent download, send agent SHA384 hash
+ parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete
+ delete obj.agentUpdate.buf;
+ delete obj.agentUpdate;
+ }
+ }
+ }
+ }
+ }
+ else if (cmdid == 15) { // MeshCommand_AgentTag
+ var tag = msg.substring(2);
+ while (tag.charCodeAt(tag.length - 1) == 0) { tag = tag.substring(0, tag.length - 1); } // Remove end-of-line zeros.
+ ChangeAgentTag(tag);
+ }
+ } else if (obj.authenticated < 2) { // We are not authenticated
+ // Check if this is a un-authenticated JSON
+ if (msg.charCodeAt(0) == 123) {
+ var str = msg.toString('utf8'), command = null;
+ if (str[0] == '{') {
+ try { command = JSON.parse(str); } catch (ex) { } // If the command can't be parsed, ignore it.
+ if ((command != null) && (command.action === 'agentName') && (typeof command.value == 'string') && (command.value.length > 0) && (command.value.length < 256)) { obj.agentName = command.value; }
+ }
+ return;
+ }
+ const cmd = common.ReadShort(msg, 0);
+ if (cmd == 1) {
+ // Agent authentication request
+ if ((msg.length != 98) || ((obj.receivedCommands & 1) != 0)) return;
+ obj.receivedCommands += 1; // Agent can't send the same command twice on the same connection ever. Block DOS attack path.
+
+ if (isIgnoreHashCheck()) {
+ // Send the agent web hash back to the agent
+ // Send 384 bits SHA384 hash of TLS cert + 384 bits nonce
+ obj.sendBinary(common.ShortToStr(1) + msg.substring(2, 50) + obj.nonce); // Command 1, hash + nonce. Use the web hash given by the agent.
+ } else {
+ // Check that the server hash matches our own web certificate hash (SHA384)
+ obj.agentSeenCerthash = msg.substring(2, 50);
+ if ((getWebCertHash(domain) != obj.agentSeenCerthash) && (getWebCertFullHash(domain) != obj.agentSeenCerthash) && (parent.defaultWebCertificateHash != obj.agentSeenCerthash) && (parent.defaultWebCertificateFullHash != obj.agentSeenCerthash)) {
+ if (parent.parent.supportsProxyCertificatesRequest !== false) {
+ obj.badWebCert = Buffer.from(parent.crypto.randomBytes(16), 'binary').toString('base64');
+ parent.wsagentsWithBadWebCerts[obj.badWebCert] = obj; // Add this agent to the list of of agents with bad web certificates.
+ parent.parent.updateProxyCertificates(false);
+ }
+ parent.agentStats.agentBadWebCertHashCount++;
+ parent.setAgentIssue(obj, "BadWebCertHash: " + Buffer.from(msg.substring(2, 50), 'binary').toString('hex'));
+ parent.parent.debug('agent', 'Agent bad web cert hash (Agent:' + (Buffer.from(msg.substring(2, 50), 'binary').toString('hex').substring(0, 10)) + ' != Server:' + (Buffer.from(getWebCertHash(domain), 'binary').toString('hex').substring(0, 10)) + ' or ' + (Buffer.from(getWebCertFullHash(domain), 'binary').toString('hex').substring(0, 10)) + '), holding connection (' + obj.remoteaddrport + ').');
+ parent.parent.debug('agent', 'Agent reported web cert hash:' + (Buffer.from(msg.substring(2, 50), 'binary').toString('hex')) + '.');
+ console.log('Agent bad web cert hash (Agent:' + (Buffer.from(msg.substring(2, 50), 'binary').toString('hex').substring(0, 10)) + ' != Server:' + (Buffer.from(getWebCertHash(domain), 'binary').toString('hex').substring(0, 10)) + ' or ' + (Buffer.from(getWebCertFullHash(domain), 'binary').toString('hex').substring(0, 10)) + '), holding connection (' + obj.remoteaddrport + ').');
+ console.log('Agent reported web cert hash:' + (Buffer.from(msg.substring(2, 50), 'binary').toString('hex')) + '.');
+ delete obj.agentSeenCerthash;
+ return;
+ } else {
+ // The hash matched one of the acceptable values, send the agent web hash back to the agent
+ // Send 384 bits SHA384 hash of TLS cert + 384 bits nonce
+ // Command 1, hash + nonce. Use the web hash given by the agent.
+ obj.sendBinary(common.ShortToStr(1) + obj.agentSeenCerthash + obj.nonce);
+ }
+ }
+
+ // Use our server private key to sign the ServerHash + AgentNonce + ServerNonce
+ obj.agentnonce = msg.substring(50, 98);
+
+ // Check if we got the agent auth confirmation
+ if ((obj.receivedCommands & 8) == 0) {
+ // If we did not get an indication that the agent already validated this server, send the server signature.
+ if (obj.useSwarmCert == true) {
+ // Perform the hash signature using older swarm server certificate
+ parent.parent.certificateOperations.acceleratorPerformSignature(1, msg.substring(2) + obj.nonce, null, function (tag, signature) {
+ // Send back our certificate + signature
+ obj.sendBinary(common.ShortToStr(2) + common.ShortToStr(parent.swarmCertificateAsn1.length) + parent.swarmCertificateAsn1 + signature); // Command 2, certificate + signature
+ });
+ } else {
+ // Perform the hash signature using the server agent certificate
+ parent.parent.certificateOperations.acceleratorPerformSignature(0, msg.substring(2) + obj.nonce, null, function (tag, signature) {
+ // Send back our certificate + signature
+ obj.sendBinary(common.ShortToStr(2) + common.ShortToStr(parent.agentCertificateAsn1.length) + parent.agentCertificateAsn1 + signature); // Command 2, certificate + signature
+ });
+ }
+ }
+
+ // Check the agent signature if we can
+ if (obj.unauthsign != null) {
+ if (processAgentSignature(obj.unauthsign) == false) {
+ parent.agentStats.agentBadSignature1Count++;
+ parent.setAgentIssue(obj, "BadSignature1");
+ parent.parent.debug('agent', 'Agent connected with bad signature, holding connection (' + obj.remoteaddrport + ').');
+ console.log('Agent connected with bad signature, holding connection (' + obj.remoteaddrport + ').'); return;
+ } else { completeAgentConnection(); }
+ }
+ }
+ else if (cmd == 2) {
+ // Agent certificate
+ if ((msg.length < 4) || ((obj.receivedCommands & 2) != 0)) return;
+ obj.receivedCommands += 2; // Agent can't send the same command twice on the same connection ever. Block DOS attack path.
+
+ // Decode the certificate
+ const certlen = common.ReadShort(msg, 2);
+ obj.unauth = {};
+ try { obj.unauth.nodeid = Buffer.from(forge.pki.getPublicKeyFingerprint(forge.pki.certificateFromAsn1(forge.asn1.fromDer(msg.substring(4, 4 + certlen))).publicKey, { md: forge.md.sha384.create() }).data, 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); } catch (ex) { console.log(ex); parent.parent.debug('agent', ex); return; }
+ obj.unauth.nodeCertPem = '-----BEGIN CERTIFICATE-----\r\n' + Buffer.from(msg.substring(4, 4 + certlen), 'binary').toString('base64') + '\r\n-----END CERTIFICATE-----';
+
+ // Check the agent signature if we can
+ if (obj.agentnonce == null) { obj.unauthsign = msg.substring(4 + certlen); } else {
+ if (processAgentSignature(msg.substring(4 + certlen)) == false) {
+ parent.agentStats.agentBadSignature2Count++;
+ parent.setAgentIssue(obj, "BadSignature2");
+ parent.parent.debug('agent', 'Agent connected with bad signature, holding connection (' + obj.remoteaddrport + ').');
+ console.log('Agent connected with bad signature, holding connection (' + obj.remoteaddrport + ').'); return;
+ }
+ }
+ completeAgentConnection();
+ }
+ else if (cmd == 3) {
+ // Agent meshid
+ if ((msg.length < 70) || ((obj.receivedCommands & 4) != 0)) return;
+ obj.receivedCommands += 4; // Agent can't send the same command twice on the same connection ever. Block DOS attack path.
+
+ // Set the meshid
+ obj.agentInfo = {};
+ obj.agentInfo.infoVersion = common.ReadInt(msg, 2);
+ obj.agentInfo.agentId = common.ReadInt(msg, 6);
+ obj.agentInfo.agentVersion = common.ReadInt(msg, 10);
+ obj.agentInfo.platformType = common.ReadInt(msg, 14);
+ if (obj.agentInfo.platformType > 8 || obj.agentInfo.platformType < 1) { obj.agentInfo.platformType = 1; }
+ if (msg.substring(50, 66) == '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0') {
+ obj.meshid = Buffer.from(msg.substring(18, 50), 'binary').toString('hex'); // Older HEX MeshID
+ } else {
+ obj.meshid = Buffer.from(msg.substring(18, 66), 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); // New Base64 MeshID
+ }
+ //console.log('MeshID', obj.meshid);
+ obj.agentInfo.capabilities = common.ReadInt(msg, 66);
+ if (msg.length > 70) {
+ const computerNameLen = common.ReadShort(msg, 70);
+ obj.agentInfo.computerName = Buffer.from(msg.substring(72, 72 + computerNameLen), 'binary').toString('utf8');
+ //console.log('computerName', msg.length, computerNameLen, obj.agentInfo.computerName);
+ } else {
+ obj.agentInfo.computerName = '';
+ //console.log('computerName-none');
+ }
+
+ obj.dbMeshKey = 'mesh/' + domain.id + '/' + obj.meshid;
+ completeAgentConnection();
+ } else if (cmd == 4) {
+ if ((msg.length < 2) || ((obj.receivedCommands & 8) != 0)) return;
+ obj.receivedCommands += 8; // Agent can't send the same command twice on the same connection ever. Block DOS attack path.
+ // Agent already authenticated the server, wants to skip the server signature - which is great for server performance.
+ } else if (cmd == 5) {
+ // ServerID. Agent is telling us what serverid it expects. Useful if we have many server certificates.
+ if ((msg.substring(2, 34) == parent.swarmCertificateHash256) || (msg.substring(2, 50) == parent.swarmCertificateHash384)) { obj.useSwarmCert = true; }
+ } else if (cmd == 30) {
+ // Agent Commit Date. This is future proofing. Can be used to change server behavior depending on the date range of the agent.
+ try { obj.AgentCommitDate = Date.parse(msg.substring(2)) } catch (ex) { }
+ //console.log('Connected Agent Commit Date: ' + msg.substring(2) + ", " + Date.parse(msg.substring(2)));
+ }
+ }
+ });
+
+ // If error, do nothing
+ ws.on('error', function (err) { parent.parent.debug('agent', 'AGENT WSERR: ' + err); console.log('AGENT WSERR: ' + err); obj.close(0); });
+
+ // If the mesh agent web socket is closed, clean up.
+ ws.on('close', function (req) {
+ parent.agentStats.agentClose++;
+ if (obj.nodeid != null) {
+ const agentId = (obj.agentInfo && obj.agentInfo.agentId) ? obj.agentInfo.agentId : 'Unknown';
+ //console.log('Agent disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ') id=' + agentId);
+ parent.parent.debug('agent', 'Agent disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ') id=' + agentId);
+
+ // Log the agent disconnection if we are not testing agent update
+ if (args.agentupdatetest == null) {
+ if (parent.wsagentsDisconnections[obj.nodeid] == null) {
+ parent.wsagentsDisconnections[obj.nodeid] = 1;
+ } else {
+ parent.wsagentsDisconnections[obj.nodeid] = ++parent.wsagentsDisconnections[obj.nodeid];
+ }
+ }
+ }
+ obj.close(0);
+ });
+
+ // Return the mesh for this device, in some cases, we may auto-create the mesh.
+ function getMeshAutoCreate() {
+ var mesh = parent.meshes[obj.dbMeshKey];
+
+ // If the mesh was not found and we are in LAN mode, check of the domain can be corrected
+ if ((args.lanonly == true) && (mesh == null)) {
+ var smesh = obj.dbMeshKey.split('/');
+ for (var i in parent.parent.config.domains) {
+ mesh = parent.meshes['mesh/' + i + '/' + smesh[2]];
+ if (mesh != null) {
+ obj.domain = domain = parent.parent.config.domains[i];
+ obj.meshid = smesh[2];
+ obj.dbMeshKey = 'mesh/' + i + '/' + smesh[2];
+ obj.dbNodeKey = 'node/' + domain.id + '/' + obj.nodeid;
+ break;
+ }
+ }
+ }
+
+ if ((mesh == null) && (typeof domain.orphanagentuser == 'string')) {
+ const adminUser = parent.users['user/' + domain.id + '/' + domain.orphanagentuser];
+ if ((adminUser != null) && (adminUser.siteadmin == 0xFFFFFFFF)) {
+ // Mesh name is hex instead of base64
+ const meshname = obj.meshid.substring(0, 18);
+
+ // Create a new mesh for this device
+ const links = {};
+ links[adminUser._id] = { name: adminUser.name, rights: 0xFFFFFFFF };
+ mesh = { type: 'mesh', _id: obj.dbMeshKey, name: meshname, mtype: 2, desc: '', domain: domain.id, links: links };
+ db.Set(mesh);
+ parent.meshes[obj.dbMeshKey] = mesh;
+
+ if (adminUser.links == null) adminUser.links = {};
+ adminUser.links[obj.dbMeshKey] = { rights: 0xFFFFFFFF };
+ db.SetUser(adminUser);
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [adminUser._id, obj.dbNodeKey]), obj, { etype: 'mesh', username: adminUser.name, meshid: obj.dbMeshKey, name: meshname, mtype: 2, desc: '', action: 'createmesh', links: links, msgid: 55, msgArgs: [obj.meshid], msg: "Created device group: " + obj.meshid, domain: domain.id });
+ }
+ } else {
+ if ((mesh != null) && (mesh.deleted != null) && (mesh.links)) {
+ // Must un-delete this mesh
+ var ids = parent.CreateMeshDispatchTargets(mesh._id, [obj.dbNodeKey]);
+
+ // See if users still exists, if so, add links to the mesh
+ for (var userid in mesh.links) {
+ const user = parent.users[userid];
+ if (user) {
+ if (user.links == null) { user.links = {}; }
+ if (user.links[mesh._id] == null) {
+ user.links[mesh._id] = { rights: mesh.links[userid].rights };
+ ids.push(user._id);
+ db.SetUser(user);
+ }
+ }
+ }
+
+ // Send out an event indicating this mesh was "created"
+ parent.parent.DispatchEvent(ids, obj, { etype: 'mesh', meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, action: 'createmesh', links: mesh.links, msgid: 56, msgArgs: [mesh._id], msg: "Device group undeleted: " + mesh._id, domain: domain.id });
+
+ // Mark the mesh as active
+ delete mesh.deleted;
+ db.Set(mesh);
+ }
+ }
+ return mesh;
+ }
+
+ // Send a PING/PONG message
+ function sendPing() { obj.send('{"action":"ping"}'); }
+ function sendPong() { obj.send('{"action":"pong"}'); }
+
+ // Once we get all the information about an agent, run this to hook everything up to the server
+ function completeAgentConnection() {
+ if ((obj.authenticated != 1) || (obj.meshid == null) || obj.pendingCompleteAgentConnection || (obj.agentInfo == null)) { return; }
+ obj.pendingCompleteAgentConnection = true;
+
+ // Setup the agent PING/PONG timers
+ if ((typeof args.agentping == 'number') && (obj.pingtimer == null)) { obj.pingtimer = setInterval(sendPing, args.agentping * 1000); }
+ else if ((typeof args.agentpong == 'number') && (obj.pongtimer == null)) { obj.pongtimer = setInterval(sendPong, args.agentpong * 1000); }
+
+ // If this is a recovery agent
+ if (obj.agentInfo.capabilities & 0x40) {
+ // Inform mesh agent that it's authenticated.
+ delete obj.pendingCompleteAgentConnection;
+ obj.authenticated = 2;
+ obj.sendBinary(common.ShortToStr(4));
+
+ // Ask for mesh core hash.
+ obj.sendBinary(common.ShortToStr(11) + common.ShortToStr(0));
+ return;
+ }
+
+ // Check if we have too many agent sessions
+ if (typeof domain.limits.maxagentsessions == 'number') {
+ // Count the number of agent sessions for this domain
+ var domainAgentSessionCount = 0;
+ for (var i in parent.wsagents) { if (parent.wsagents[i].domain.id == domain.id) { domainAgentSessionCount++; } }
+
+ // Check if we have too many user sessions
+ if (domainAgentSessionCount >= domain.limits.maxagentsessions) {
+ // Too many, hold the connection.
+ parent.agentStats.agentMaxSessionHoldCount++;
+ return;
+ }
+ }
+
+ /*
+ // Check that the mesh exists
+ var mesh = parent.meshes[obj.dbMeshKey];
+ if (mesh == null) {
+ var holdConnection = true;
+ if (typeof domain.orphanagentuser == 'string') {
+ var adminUser = parent.users['user/' + domain.id + '/' + args.orphanagentuser];
+ if ((adminUser != null) && (adminUser.siteadmin == 0xFFFFFFFF)) {
+ // Create a new mesh for this device
+ holdConnection = false;
+ var links = {};
+ links[user._id] = { name: adminUser.name, rights: 0xFFFFFFFF };
+ mesh = { type: 'mesh', _id: obj.dbMeshKey, name: obj.meshid, mtype: 2, desc: '', domain: domain.id, links: links };
+ db.Set(mesh);
+ parent.meshes[obj.meshid] = mesh;
+ parent.parent.AddEventDispatch(parent.CreateMeshDispatchTargets(obj.meshid, [obj.dbNodeKey]), ws);
+
+ if (adminUser.links == null) user.links = {};
+ adminUser.links[obj.meshid] = { rights: 0xFFFFFFFF };
+ //adminUser.subscriptions = parent.subscribe(adminUser._id, ws);
+ db.SetUser(user);
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(meshid, [user._id, obj.dbNodeKey]), obj, { etype: 'mesh', username: user.name, meshid: obj.meshid, name: obj.meshid, mtype: 2, desc: '', action: 'createmesh', links: links, msg: 'Mesh created: ' + obj.meshid, domain: domain.id });
+ }
+ }
+
+ if (holdConnection == true) {
+ // If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
+ parent.parent.debug('agent', 'Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
+ console.log('Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
+ return;
+ }
+ }
+ if (mesh.mtype != 2) { // If we disconnect, the agnet will just reconnect. We need to log this or tell agent to connect in a few hours.
+ parent.parent.debug('agent', 'Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
+ console.log('Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
+ return;
+ }
+ */
+
+ // Check that the node exists
+ db.Get(obj.dbNodeKey, async function (err, nodes) {
+ if (obj.agentInfo == null) { return; }
+ var device, mesh;
+ var nodeExists = Boolean(false);
+
+ if ((nodes == null) || (nodes.length == 0)) {
+ if(domain.preventduplicatedevices){
+
+ const existingNodes = await new Promise((resolve, reject) => {
+ db.GetNodeByComputerName(domain.id, obj.agentInfo.computerName, (err, result) => {
+ if (err) reject(err);
+ else resolve(result);
+ });
+ });
+
+ if (!existingNodes || existingNodes.length === 0) {
+ // Device does not exist with the name
+ nodeExists = false;
+ } else {
+ console.log("Device already exists");
+ // Remove nodes with the same name
+ existingNodes.forEach((eNode) => {
+
+ parent.parent.debug('agent', 'Removing old dublicated node (' + eNode.rname + ', ' + eNode._id + ').');
+
+ db.Remove(eNode._id); // Remove node with that id
+ db.Remove('if' + eNode._id); // Remove interface information
+ db.Remove('nt' + eNode._id); // Remove notes
+ db.Remove('lc' + eNode._id); // Remove last connect time
+ db.Remove('si' + eNode._id); // Remove system information
+ db.Remove('al' + eNode._id); // Remove error log last time
+ if (db.RemoveSMBIOS) { db.RemoveSMBIOS(eNode._id); } // Remove SMBios data
+ db.RemoveAllNodeEvents(eNode._id); // Remove all events for this node
+ db.removeAllPowerEventsForNode(eNode._id); // Remove all power events for this node
+
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(eNode.meshid, [eNode._id]), obj, { etype: 'node', action: 'removenode', nodeid: eNode._id, domain: eNode.domain, nolog: 1 });
+ });
+
+ // Set mesh from previous node
+ obj.dbMeshKey = existingNodes[0].meshid
+
+ nodeExists = false;
+
+ }
+ } else {
+ nodeExists = false;
+ }
+
+ } else {
+ nodeExists = true;
+ }
+
+ // See if this node exists in the database
+ if (nodeExists == false) {
+ // This device does not exist, use the meshid given by the device
+
+ // Check if we already have too many devices for this domain
+ if (domain.limits && (typeof domain.limits.maxdevices == 'number')) {
+ db.isMaxType(domain.limits.maxdevices, 'node', domain.id, function (ismax, count) {
+ if (ismax == true) {
+ // Too many devices in this domain.
+ parent.agentStats.maxDomainDevicesReached++;
+ } else {
+ // We are under the limit, create the new device.
+ completeAgentConnection2();
+ }
+ });
+ } else {
+ completeAgentConnection2();
+ }
+ return;
+ } else {
+ device = nodes[0];
+ obj.name = device.name;
+
+ // This device exists, meshid given by the device must be ignored, use the server side one.
+ if ((device.meshid != null) && (device.meshid != obj.dbMeshKey)) {
+ obj.dbMeshKey = device.meshid;
+ obj.meshid = device.meshid.split('/')[2];
+ }
+
+ // See if this mesh exists, if it does not we may want to create it.
+ mesh = getMeshAutoCreate();
+
+ // Check if the mesh exists
+ if (mesh == null) {
+ // If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
+ parent.agentStats.invalidDomainMesh2Count++;
+ parent.setAgentIssue(obj, "invalidDomainMesh2");
+ parent.parent.debug('agent', 'Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
+ console.log('Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
+ return;
+ }
+
+ // Check if the mesh is the right type
+ if (mesh.mtype != 2) {
+ // If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
+ parent.agentStats.invalidMeshType2Count++;
+ parent.setAgentIssue(obj, "invalidMeshType2");
+ parent.parent.debug('agent', 'Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
+ console.log('Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
+ return;
+ }
+
+ // Mark when this device connected
+ obj.connectTime = Date.now();
+
+ // Device already exists, look if changes have occured
+ var changes = [], change = 0, log = 0;
+ if (device.agent == null) { device.agent = { ver: obj.agentInfo.agentVersion, id: obj.agentInfo.agentId, caps: obj.agentInfo.capabilities }; change = 1; }
+ if (device.rname != obj.agentInfo.computerName) { device.rname = obj.agentInfo.computerName; change = 1; changes.push('computer name'); }
+ if (device.agent.ver != obj.agentInfo.agentVersion) { device.agent.ver = obj.agentInfo.agentVersion; change = 1; changes.push('agent version'); }
+ if (device.agent.id != obj.agentInfo.agentId) { device.agent.id = obj.agentInfo.agentId; change = 1; changes.push('agent type'); }
+ if ((device.agent.caps & 24) != (obj.agentInfo.capabilities & 24)) { device.agent.caps = obj.agentInfo.capabilities; change = 1; changes.push('agent capabilities'); } // If agent console or javascript support changes, update capabilities
+ // We want the server name to be sync'ed to the hostname or the --agentName
+ // (flag 16 allows to override the name until next connection)
+ if (mesh.flags && (mesh.flags & 2)) {
+ var preferredName = (mesh.flags & 8) && obj.agentName || obj.agentInfo.computerName;
+ if (device.name != preferredName) {device.name = preferredName; change = 1; }
+ }
+ if (device.ip != obj.remoteaddr) { device.ip = obj.remoteaddr; change = 1; }
+
+ if (change == 1) {
+ // Do some clean up if needed, these values should not be in the database.
+ if (device.conn != null) { delete device.conn; }
+ if (device.pwr != null) { delete device.pwr; }
+ if (device.agct != null) { delete device.agct; }
+ if (device.cict != null) { delete device.cict; }
+
+ // Save the updated device in the database
+ db.Set(device);
+
+ // If this is a temporary device, don't log changes
+ if (obj.agentInfo.capabilities & 0x20) { log = 0; }
+
+ // Event the node change
+ var event = { etype: 'node', action: 'changenode', nodeid: obj.dbNodeKey, domain: domain.id, node: parent.CloneSafeNode(device) };
+ if (log == 0) { event.nolog = 1; } else { event.msg = 'Changed device ' + device.name + ' from group ' + mesh.name + ': ' + changes.join(', '); }
+ if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(device.meshid, [obj.dbNodeKey]), obj, event);
+ }
+ }
+
+ completeAgentConnection3(device, mesh);
+ });
+ }
+
+ function completeAgentConnection2() {
+ // See if this mesh exists, if it does not we may want to create it.
+ var mesh = getMeshAutoCreate();
+
+ // Check if the mesh exists
+ if (mesh == null) {
+ // If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
+ parent.agentStats.invalidDomainMeshCount++;
+ parent.setAgentIssue(obj, "invalidDomainMesh");
+ parent.parent.debug('agent', 'Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
+ console.log('Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
+ return;
+ }
+
+ // Check if the mesh is the right type
+ if (mesh.mtype != 2) {
+ // If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
+ parent.agentStats.invalidMeshTypeCount++;
+ parent.setAgentIssue(obj, "invalidMeshType");
+ parent.parent.debug('agent', 'Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
+ console.log('Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
+ return;
+ }
+
+ // Mark when this device connected
+ obj.connectTime = Date.now();
+
+ // This node does not exist, create it.
+ var agentName = obj.agentName ? obj.agentName : obj.agentInfo.computerName;
+ var device = { type: 'node', mtype: mesh.mtype, _id: obj.dbNodeKey, icon: obj.agentInfo.platformType, meshid: obj.dbMeshKey, name: agentName, rname: obj.agentInfo.computerName, domain: domain.id, agent: { ver: obj.agentInfo.agentVersion, id: obj.agentInfo.agentId, caps: obj.agentInfo.capabilities }, host: null };
+ db.Set(device);
+
+ // Event the new node
+ if (obj.agentInfo.capabilities & 0x20) {
+ // This is a temporary agent, don't log.
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, { etype: 'node', action: 'addnode', node: device, domain: domain.id, nolog: 1 });
+ } else {
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, { etype: 'node', action: 'addnode', node: device, msgid: 57, msgArgs: [obj.agentInfo.computerName, mesh.name], msg: ('Added device ' + obj.agentInfo.computerName + ' to device group ' + mesh.name), domain: domain.id });
+ }
+
+ completeAgentConnection3(device, mesh);
+ }
+
+ function completeAgentConnection3(device, mesh) {
+ // Check if this agent is already connected
+ const dupAgent = parent.wsagents[obj.dbNodeKey];
+ parent.wsagents[obj.dbNodeKey] = obj;
+ if (dupAgent) {
+ // Record duplicate agents
+ if (parent.duplicateAgentsLog[obj.dbNodeKey] == null) {
+ if (dupAgent.remoteaddr == obj.remoteaddr) {
+ parent.duplicateAgentsLog[obj.dbNodeKey] = { name: device.name, group: mesh.name, ip: [obj.remoteaddr], count: 1 };
+ } else {
+ parent.duplicateAgentsLog[obj.dbNodeKey] = { name: device.name, group: mesh.name, ip: [obj.remoteaddr, dupAgent.remoteaddr], count: 1 };
+ }
+ } else {
+ parent.duplicateAgentsLog[obj.dbNodeKey].name = device.name;
+ parent.duplicateAgentsLog[obj.dbNodeKey].group = mesh.name;
+ parent.duplicateAgentsLog[obj.dbNodeKey].count++;
+ if (parent.duplicateAgentsLog[obj.dbNodeKey].ip.indexOf(obj.remoteaddr) == -1) { parent.duplicateAgentsLog[obj.dbNodeKey].ip.push(obj.remoteaddr); }
+ }
+
+ // Close the duplicate agent
+ parent.agentStats.duplicateAgentCount++;
+ parent.setAgentIssue(obj, 'duplicateAgent');
+ if (obj.nodeid != null) { parent.parent.debug('agent', 'Duplicate agent ' + obj.nodeid + ' (' + obj.remoteaddrport + ')'); }
+ dupAgent.close(3);
+ } else {
+ // Indicate the agent is connected
+ parent.parent.SetConnectivityState(obj.dbMeshKey, obj.dbNodeKey, obj.connectTime, 1, 1, null, { remoteaddrport: obj.remoteaddrport, name: device.name });
+ }
+
+ // We are done, ready to communicate with this agent
+ delete obj.pendingCompleteAgentConnection;
+ obj.authenticated = 2;
+
+ // Check how many times this agent disconnected in the last few minutes.
+ const disconnectCount = parent.wsagentsDisconnections[obj.nodeid];
+ if (disconnectCount > 6) {
+ parent.parent.debug('agent', 'Agent in big trouble: NodeId=' + obj.nodeid + ', IP=' + obj.remoteaddrport + ', Agent=' + obj.agentInfo.agentId + '.');
+ console.log('Agent in big trouble: NodeId=' + obj.nodeid + ', IP=' + obj.remoteaddrport + ', Agent=' + obj.agentInfo.agentId + '.');
+ parent.agentStats.agentInBigTrouble++;
+ // TODO: Log or do something to recover?
+ return;
+ }
+
+ // Command 4, inform mesh agent that it's authenticated.
+ obj.sendBinary(common.ShortToStr(4));
+
+ // Not sure why, but in rare cases, obj.agentInfo is undefined here.
+ if ((obj.agentInfo == null) || (typeof obj.agentInfo.capabilities != 'number')) { return; } // This is an odd case.
+ obj.agentExeInfo = parent.parent.meshAgentBinaries[obj.agentInfo.agentId];
+ if (domain.meshAgentBinaries && domain.meshAgentBinaries[obj.agentInfo.agentId]) { obj.agentExeInfo = domain.meshAgentBinaries[obj.agentInfo.agentId]; }
+
+ // Check if this agent is reconnecting too often.
+ if (disconnectCount > 4) {
+ // Too many disconnections, this agent has issues. Just clear the core.
+ obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0));
+ parent.parent.debug('agent', 'Agent in trouble: NodeId=' + obj.nodeid + ', IP=' + obj.remoteaddrport + ', Agent=' + obj.agentInfo.agentId + '.');
+ parent.agentStats.agentInTrouble++;
+ //console.log('Agent in trouble: NodeId=' + obj.nodeid + ', IP=' + obj.remoteaddrport + ', Agent=' + obj.agentInfo.agentId + '.');
+ // TODO: Log or do something to recover?
+ return;
+ }
+
+ // Check if we need to make an native update check
+ var corename = null;
+ if (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId] != null) {
+ corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].core;
+ } else {
+ // MeshCommand_CoreModule, ask mesh agent to clear the core
+ obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0));
+ }
+
+ if ((obj.agentExeInfo != null) && (obj.agentExeInfo.update == true)) {
+ // Ask the agent for it's executable binary hash
+ obj.sendBinary(common.ShortToStr(12) + common.ShortToStr(0));
+ } else {
+ // Check the mesh core, if the agent is capable of running one
+ if (((obj.agentInfo.capabilities & 16) != 0) && (corename != null)) {
+ obj.sendBinary(common.ShortToStr(11) + common.ShortToStr(0)); // Command 11, ask for mesh core hash.
+ } else {
+ agentCoreIsStable(); // No updates needed, agent is ready to go.
+ }
+ }
+ }
+
+ // Indicate to the agent that we want to check Intel AMT configuration
+ // This may trigger a CIRA-LMS tunnel from the agent so the server can inspect the device.
+ obj.sendUpdatedIntelAmtPolicy = function (policy) {
+ if (obj.agentExeInfo && (obj.agentExeInfo.amt == true)) { // Only send Intel AMT policy to agents what could have AMT.
+ if (policy == null) { var mesh = parent.meshes[obj.dbMeshKey]; if (mesh == null) return; policy = mesh.amt; }
+ if ((policy != null) && (policy.type != 0)) {
+ const cookie = parent.parent.encodeCookie({ a: 'apf', n: obj.dbNodeKey, m: obj.dbMeshKey }, parent.parent.loginCookieEncryptionKey);
+ try { obj.send(JSON.stringify({ action: 'amtconfig', user: '**MeshAgentApfTunnel**', pass: cookie })); } catch (ex) { }
+ }
+ }
+ }
+
+ function recoveryAgentCoreIsStable(mesh) {
+ parent.agentStats.recoveryCoreIsStableCount++;
+
+ // Recovery agent is doing ok, lets perform main agent checking.
+ //console.log('recoveryAgentCoreIsStable()');
+
+ // Fetch the the real agent nodeid
+ db.Get('da' + obj.dbNodeKey, function (err, nodes, self) {
+ if ((nodes != null) && (nodes.length == 1)) {
+ self.realNodeKey = nodes[0].raid;
+
+ // Get agent connection state
+ var agentConnected = false;
+ var state = parent.parent.GetConnectivityState(self.realNodeKey);
+ if (state) { agentConnected = ((state.connectivity & 1) != 0) }
+
+ self.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: self.realNodeKey, agent: agentConnected } }));
+ } else {
+ self.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: null } }));
+ }
+ }, obj);
+ }
+
+ function agentCoreIsStable() {
+ parent.agentStats.coreIsStableCount++;
+
+ // Check that the mesh exists
+ const mesh = parent.meshes[obj.dbMeshKey];
+ if (mesh == null) {
+ parent.agentStats.meshDoesNotExistCount++;
+ parent.setAgentIssue(obj, "meshDoesNotExist");
+ // TODO: Mark this agent as part of a mesh that does not exists.
+ return; // Probably not worth doing anything else. Hold this agent.
+ }
+
+ // Check if this is a recovery agent
+ if (obj.agentInfo.capabilities & 0x40) {
+ recoveryAgentCoreIsStable(mesh);
+ return;
+ }
+
+ // Fetch the the diagnostic agent nodeid
+ db.Get('ra' + obj.dbNodeKey, function (err, nodes) {
+ if ((nodes != null) && (nodes.length == 1)) {
+ obj.diagnosticNodeKey = nodes[0].daid;
+ obj.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: obj.diagnosticNodeKey } }));
+ }
+ });
+
+ // Indicate that we want to check the Intel AMT configuration
+ // This may trigger a CIRA-LMS tunnel to the server for further processing
+ obj.sendUpdatedIntelAmtPolicy();
+
+ // Fetch system information
+ db.GetHash('si' + obj.dbNodeKey, function (err, results) {
+ if ((results != null) && (results.length == 1)) { obj.send(JSON.stringify({ action: 'sysinfo', hash: results[0].hash })); } else { obj.send(JSON.stringify({ action: 'sysinfo' })); }
+ });
+
+ // Agent error log dump
+ if (parent.parent.agentErrorLog != null) {
+ db.Get('al' + obj.dbNodeKey, function (err, docs) { // Agent Log
+ if ((docs != null) && (docs.length == 1) && (typeof docs[0].lastEvent)) {
+ obj.send('{"action":"errorlog","startTime":' + docs[0].lastEvent + '}'); // Ask all events after a given time
+ } else {
+ obj.send('{"action":"errorlog"}'); // Ask all
+ }
+ });
+ }
+
+ // Set agent core dump
+ if ((parent.parent.config.settings != null) && ((parent.parent.config.settings.agentcoredump === true) || (parent.parent.config.settings.agentcoredump === false))) {
+ obj.send(JSON.stringify({ action: 'coredump', value: parent.parent.config.settings.agentcoredump }));
+ if (parent.parent.config.settings.agentcoredump === true) {
+ // Check if we requested a core dump file in the last minute, if not, ask if one is present.
+ if ((parent.lastCoreDumpRequest == null) || ((Date.now() - parent.lastCoreDumpRequest) >= 60000)) { obj.send(JSON.stringify({ action: 'getcoredump' })); }
+ }
+ }
+
+ // Do this if IP location is enabled on this domain TODO: Set IP location per device group?
+ if (domain.iplocation == true) {
+ // Check if we already have IP location information for this node
+ db.Get('iploc_' + obj.remoteaddr, function (err, iplocs) {
+ if ((iplocs != null) && (iplocs.length == 1)) {
+ // We have a location in the database for this remote IP
+ const iploc = iplocs[0], x = {};
+ if ((iploc != null) && (iploc.ip != null) && (iploc.loc != null)) {
+ x.publicip = iploc.ip;
+ x.iploc = iploc.loc + ',' + (Math.floor((new Date(iploc.date)) / 1000));
+ ChangeAgentLocationInfo(x);
+ }
+ } else {
+ // Check if we need to ask for the IP location
+ var doIpLocation = 0;
+ if (obj.iploc == null) {
+ doIpLocation = 1;
+ } else {
+ const loc = obj.iploc.split(',');
+ if (loc.length < 3) {
+ doIpLocation = 2;
+ } else {
+ var t = new Date((parseFloat(loc[2]) * 1000)), now = Date.now();
+ t.setDate(t.getDate() + 20);
+ if (t < now) { doIpLocation = 3; }
+ }
+ }
+
+ // If we need to ask for IP location, see if we have the quota to do it.
+ if (doIpLocation > 0) {
+ db.getValueOfTheDay('ipLocationRequestLimitor', 10, function (ipLocationLimitor) {
+ if ((ipLocationLimitor != null) && (ipLocationLimitor.value > 0)) {
+ ipLocationLimitor.value--;
+ db.Set(ipLocationLimitor);
+ obj.send(JSON.stringify({ action: 'iplocation' }));
+ }
+ });
+ }
+ }
+ });
+ }
+
+ // Indicate server information to the agent.
+ var serverInfo = { action: 'serverInfo' };
+ if ((typeof domain.terminal == 'object') && (typeof domain.terminal.launchcommand == 'object')) {
+ // Send terminal starting command
+ serverInfo.termlaunchcommand = {};
+ if (typeof domain.terminal.launchcommand.linux == 'string') { serverInfo.termlaunchcommand.linux = domain.terminal.launchcommand.linux; }
+ if (typeof domain.terminal.launchcommand.darwin == 'string') { serverInfo.termlaunchcommand.darwin = domain.terminal.launchcommand.darwin; }
+ if (typeof domain.terminal.launchcommand.freebsd == 'string') { serverInfo.termlaunchcommand.freebsd = domain.terminal.launchcommand.freebsd; }
+ }
+ // Enable agent self guest sharing if allowed
+ if (domain.agentselfguestsharing) { serverInfo.agentSelfGuestSharing = true; }
+ obj.send(JSON.stringify(serverInfo));
+
+ // Plug in handler
+ if (parent.parent.pluginHandler != null) {
+ parent.parent.pluginHandler.callHook('hook_agentCoreIsStable', obj, parent);
+ }
+ }
+
+ // Get the web certificate private key hash for the specified domain
+ function getWebCertHash(domain) {
+ const hash = parent.webCertificateHashs[domain.id];
+ if (hash != null) return hash;
+ return parent.webCertificateHash;
+ }
+
+ // Get the web certificate hash for the specified domain
+ function getWebCertFullHash(domain) {
+ const hash = parent.webCertificateFullHashs[domain.id];
+ if (hash != null) return hash;
+ return parent.webCertificateFullHash;
+ }
+
+ // Verify the agent signature
+ function processAgentSignature(msg) {
+ if (isIgnoreHashCheck() == false) {
+ var verified = false;
+
+ // This agent did not report a valid TLS certificate hash, fail now.
+ if (obj.agentSeenCerthash == null) return false;
+
+ // Raw RSA signatures have an exact length of 256 or 384. PKCS7 is larger.
+ if ((msg.length != 384) && (msg.length != 256)) {
+ // Verify a PKCS7 signature.
+ var msgDer = null;
+ try { msgDer = forge.asn1.fromDer(forge.util.createBuffer(msg, 'binary')); } catch (ex) { }
+ if (msgDer != null) {
+ try {
+ const p7 = forge.pkcs7.messageFromAsn1(msgDer);
+ const sig = p7.rawCapture.signature;
+
+ // Verify with key hash
+ var buf = Buffer.from(obj.agentSeenCerthash + obj.nonce + obj.agentnonce, 'binary');
+ var verifier = parent.crypto.createVerify('RSA-SHA384');
+ verifier.update(buf);
+ verified = verifier.verify(obj.unauth.nodeCertPem, sig, 'binary');
+ if (verified !== true) {
+ // Not a valid signature
+ parent.agentStats.invalidPkcsSignatureCount++;
+ parent.setAgentIssue(obj, "invalidPkcsSignature");
+ return false;
+ }
+ } catch (ex) { };
+ }
+ }
+
+ if (verified == false) {
+ // Verify the RSA signature. This is the fast way, without using forge.
+ const verify = parent.crypto.createVerify('SHA384');
+ verify.end(Buffer.from(obj.agentSeenCerthash + obj.nonce + obj.agentnonce, 'binary')); // Test using the private key hash
+ if (verify.verify(obj.unauth.nodeCertPem, Buffer.from(msg, 'binary')) !== true) {
+ parent.agentStats.invalidRsaSignatureCount++;
+ parent.setAgentIssue(obj, "invalidRsaSignature");
+ return false;
+ }
+ }
+ }
+
+ // Connection is a success, clean up
+ obj.nodeid = obj.unauth.nodeid;
+ obj.dbNodeKey = 'node/' + domain.id + '/' + obj.nodeid;
+ delete obj.nonce;
+ delete obj.agentnonce;
+ delete obj.unauth;
+ delete obj.receivedCommands;
+ delete obj.agentSeenCerthash;
+ if (obj.unauthsign) delete obj.unauthsign;
+ parent.agentStats.verifiedAgentConnectionCount++;
+ parent.parent.debug('agent', 'Verified agent connection to ' + obj.nodeid + ' (' + obj.remoteaddrport + ').');
+ obj.authenticated = 1;
+ return true;
+ }
+
+ // Process incoming agent JSON data
+ function processAgentData(msg) {
+ if (obj.agentInfo == null) return;
+ var i, str = msg.toString('utf8'), command = null;
+ if (str[0] == '{') {
+ try { command = JSON.parse(str); } catch (ex) {
+ // If the command can't be parsed, ignore it.
+ parent.agentStats.invalidJsonCount++;
+ parent.setAgentIssue(obj, "invalidJson (" + str.length + "): " + str);
+ parent.parent.debug('agent', 'Unable to parse agent JSON (' + obj.remoteaddrport + ')');
+ console.log('Unable to parse agent JSON (' + obj.remoteaddrport + '): ' + str, ex);
+ return;
+ }
+ if (typeof command != 'object') { return; }
+ switch (command.action) {
+ case 'msg':
+ {
+ // If the same console command is processed many times, kick out this agent.
+ // This is a safety mesure to guard against the agent DOS'ing the server.
+ if (command.type == 'console') {
+ if (obj.consoleKickValue == command.value) {
+ if (obj.consoleKickCount) { obj.consoleKickCount++; } else { obj.consoleKickCount = 1; }
+ if (obj.consoleKickCount > 30) { obj.close(); return; } // 30 identical console messages received, kick out this agent.
+ } else {
+ obj.consoleKickValue = command.value;
+ }
+ }
+
+ // Route a message
+ parent.routeAgentCommand(command, obj.domain.id, obj.dbNodeKey, obj.dbMeshKey);
+ break;
+ }
+ case 'coreinfo':
+ {
+ // Sent by the agent to update agent information
+ ChangeAgentCoreInfo(command);
+
+ if ((obj.agentCoreUpdate === true) && (obj.agentExeInfo != null) && (typeof obj.agentExeInfo.url == 'string')) {
+ // Agent update. The recovery core was loaded in the agent, send a command to update the agent
+ parent.parent.taskLimiter.launch(function (argument, taskid, taskLimiterQueue) { // Medium priority task
+ // If agent disconnection, complete and exit now.
+ if ((obj.authenticated != 2) || (obj.agentExeInfo == null)) { parent.parent.taskLimiter.completed(taskid); return; }
+
+ // Agent update. The recovery core was loaded in the agent, send a command to update the agent
+ obj.agentCoreUpdateTaskId = taskid;
+ const url = '*' + require('url').parse(obj.agentExeInfo.url).path;
+ var cmd = { action: 'agentupdate', url: url, hash: obj.agentExeInfo.hashhex };
+ parent.parent.debug('agentupdate', "Sending agent update url: " + cmd.url);
+
+ // Add the hash
+ if (obj.agentExeInfo.fileHash != null) { cmd.hash = obj.agentExeInfo.fileHashHex; } else { cmd.hash = obj.agentExeInfo.hashhex; }
+
+ // Add server TLS cert hash
+ if (isIgnoreHashCheck() == false) {
+ const tlsCertHash = parent.webCertificateFullHashs[domain.id];
+ if (tlsCertHash != null) { cmd.servertlshash = Buffer.from(tlsCertHash, 'binary').toString('hex'); }
+ }
+
+ // Send the agent update command
+ obj.send(JSON.stringify(cmd));
+ }, null, 1);
+ }
+ break;
+ }
+ case 'smbios':
+ {
+ // SMBIOS information must never be saved when NeDB is in use. NeDB will currupt that database.
+ if (db.SetSMBIOS == null) break;
+
+ // See if we need to save SMBIOS information
+ if (domain.smbios === true) {
+ // Store the RAW SMBios table of this computer
+ // Perform sanity checks before storing
+ try {
+ for (var i in command.value) { var k = parseInt(i); if ((k != i) || (i > 255) || (typeof command.value[i] != 'object') || (command.value[i].length == null) || (command.value[i].length > 1024) || (command.value[i].length < 0)) { delete command.value[i]; } }
+ db.SetSMBIOS({ _id: obj.dbNodeKey, domain: domain.id, time: new Date(), value: command.value });
+ } catch (ex) { }
+ }
+
+ // Event the node interface information change (This is a lot of traffic, probably don't need this).
+ //parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.meshid, [obj.dbNodeKey]), obj, { action: 'smBiosChange', nodeid: obj.dbNodeKey, domain: domain.id, smbios: command.value, nolog: 1 });
+
+ break;
+ }
+ case 'netinfo':
+ {
+ // Check if network information is present
+ if ((command.netif2 == null) && (command.netif == null)) return;
+
+ // Escape any field names that have special characters
+ if (command.netif2 != null) {
+ for (var i in command.netif2) {
+ var esc = common.escapeFieldName(i);
+ if (esc !== i) { command.netif2[esc] = command.netif2[i]; delete command.netif2[i]; }
+ }
+ }
+
+ // Sent by the agent to update agent network interface information
+ delete command.action;
+ command.updateTime = Date.now();
+ command._id = 'if' + obj.dbNodeKey;
+ command.domain = domain.id;
+ command.type = 'ifinfo';
+ db.Set(command);
+
+ // Event the node interface information change
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.meshid, [obj.dbNodeKey]), obj, { action: 'ifchange', nodeid: obj.dbNodeKey, domain: domain.id, nolog: 1 });
+
+ break;
+ }
+ case 'iplocation':
+ {
+ // Sent by the agent to update location information
+ if ((command.type == 'publicip') && (command.value != null) && (typeof command.value == 'object') && (command.value.ip) && (command.value.loc)) {
+ var x = {};
+ x.publicip = command.value.ip;
+ x.iploc = command.value.loc + ',' + (Math.floor(Date.now() / 1000));
+ ChangeAgentLocationInfo(x);
+ command.value._id = 'iploc_' + command.value.ip;
+ command.value.type = 'iploc';
+ command.value.date = Date.now();
+ db.Set(command.value); // Store the IP to location data in the database
+ // Sample Value: { ip: '192.55.64.246', city: 'Hillsboro', region: 'Oregon', country: 'US', loc: '45.4443,-122.9663', org: 'AS4983 Intel Corporation', postal: '97123' }
+ }
+ break;
+ }
+ case 'mc1migration':
+ {
+ if (command.oldnodeid.length != 64) break;
+ const oldNodeKey = 'node//' + command.oldnodeid.toLowerCase();
+ db.Get(oldNodeKey, function (err, nodes) {
+ if ((nodes == null) || (nodes.length != 1)) return;
+ const node = nodes[0];
+ if (node.meshid == obj.dbMeshKey) {
+ // Update the device name & host
+ const newNode = { "name": node.name };
+ if (node.intelamt != null) { newNode.intelamt = node.intelamt; }
+ ChangeAgentCoreInfo(newNode);
+
+ // Delete this node including network interface information and events
+ db.Remove(node._id);
+ db.Remove('if' + node._id);
+
+ // Event node deletion
+ const change = 'Migrated device ' + node.name;
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(node.meshid, [obj.dbNodeKey]), obj, { etype: 'node', action: 'removenode', nodeid: node._id, msg: change, domain: node.domain });
+ }
+ });
+ break;
+ }
+ case 'openUrl':
+ {
+ // Sent by the agent to return the status of a open URL action.
+ // Nothing is done right now.
+ break;
+ }
+ case 'log':
+ {
+ // Log a value in the event log
+ if ((typeof command.msg == 'string') && (command.msg.length < 4096)) {
+ var event = { etype: 'node', action: 'agentlog', nodeid: obj.dbNodeKey, domain: domain.id, msg: command.msg };
+ if (typeof command.msgid == 'number') { event.msgid = command.msgid; }
+ if (typeof command.guestname == 'string') { event.guestname = command.guestname; }
+ if (Array.isArray(command.msgArgs)) { event.msgArgs = command.msgArgs; }
+ if (typeof command.remoteaddr == 'string') { event.remoteaddr = command.remoteaddr; }
+ var targets = parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]);
+ if (typeof command.userid == 'string') {
+ var loguser = parent.users[command.userid];
+ if (loguser) { event.userid = command.userid; event.username = loguser.name; targets.push(command.userid); }
+ }
+ if (typeof command.xuserid == 'string') {
+ var xloguser = parent.users[command.xuserid];
+ if (xloguser) { targets.push(command.xuserid); }
+ }
+ if ((typeof command.sessionid == 'string') && (command.sessionid.length < 500)) { event.sessionid = command.sessionid; }
+ parent.parent.DispatchEvent(targets, obj, event);
+
+ // If this is a help request, see if we need to email notify anyone
+ if (event.msgid == 98) {
+ // Get the node and change it if needed
+ db.Get(obj.dbNodeKey, function (err, nodes) { // TODO: THIS IS A BIG RACE CONDITION HERE, WE NEED TO FIX THAT. If this call is made twice at the same time on the same device, data will be missed.
+ if ((nodes == null) || (nodes.length != 1)) { delete obj.deviceChanging; return; }
+ const device = nodes[0];
+ if (typeof device.name == 'string') { parent.parent.NotifyUserOfDeviceHelpRequest(domain, device.meshid, device._id, device.name, command.msgArgs[0], command.msgArgs[1]); }
+ });
+ }
+ }
+ break;
+ }
+ case 'ping': { sendPong(); break; }
+ case 'pong': { break; }
+ case 'getScript':
+ {
+ // Used by the agent to get configuration scripts.
+ if (command.type == 1) {
+ parent.getCiraConfigurationScript(obj.dbMeshKey, function (script) {
+ obj.send(JSON.stringify({ action: 'getScript', type: 1, script: script.toString() }));
+ });
+ } else if (command.type == 2) {
+ parent.getCiraCleanupScript(function (script) {
+ obj.send(JSON.stringify({ action: 'getScript', type: 2, script: script.toString() }));
+ });
+ }
+ break;
+ }
+ case 'diagnostic':
+ {
+ if (typeof command.value == 'object') {
+ switch (command.value.command) {
+ case 'register': {
+ // Only main agent can do this
+ if (((obj.agentInfo.capabilities & 0x40) == 0) && (typeof command.value.value == 'string') && (command.value.value.length == 64)) {
+ // Store links to diagnostic agent id
+ var daNodeKey = 'node/' + domain.id + '/' + db.escapeBase64(command.value.value);
+ db.Set({ _id: 'da' + daNodeKey, domain: domain.id, time: obj.connectTime, raid: obj.dbNodeKey }); // DiagnosticAgent --> Agent
+ db.Set({ _id: 'ra' + obj.dbNodeKey, domain: domain.id, time: obj.connectTime, daid: daNodeKey }); // Agent --> DiagnosticAgent
+ }
+ break;
+ }
+ case 'query': {
+ // Only the diagnostic agent can do
+ if ((obj.agentInfo.capabilities & 0x40) != 0) {
+ // Return nodeid of main agent + connection status
+ db.Get('da' + obj.dbNodeKey, function (err, nodes) {
+ if ((nodes != null) && (nodes.length == 1)) {
+ obj.realNodeKey = nodes[0].raid;
+
+ // Get agent connection state
+ var agentConnected = false;
+ var state = parent.parent.GetConnectivityState(obj.realNodeKey);
+ if (state) { agentConnected = ((state.connectivity & 1) != 0) }
+
+ obj.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: obj.realNodeKey, agent: agentConnected } }));
+ } else {
+ obj.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: null } }));
+ }
+ });
+ }
+ break;
+ }
+ case 'log': {
+ if (((obj.agentInfo.capabilities & 0x40) != 0) && (typeof command.value.value == 'string') && (command.value.value.length < 256)) {
+ // If this is a diagnostic agent, log the event in the log of the main agent
+ var event = { etype: 'node', action: 'diagnostic', nodeid: obj.realNodeKey, snodeid: obj.dbNodeKey, domain: domain.id, msg: command.value.value };
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, event);
+ }
+ break;
+ }
+ }
+ }
+ break;
+ }
+ case 'sysinfo': {
+ if ((typeof command.data == 'object') && (typeof command.data.hash == 'string')) {
+ // Validate command.data.
+ if (common.validateObjectForMongo(command.data, 1024) == false) break;
+
+ // Save to database
+ command.data._id = 'si' + obj.dbNodeKey;
+ command.data.type = 'sysinfo';
+ command.data.domain = domain.id;
+ command.data.time = Date.now();
+ db.Set(command.data); // Update system information in the database.
+
+ // Event the new sysinfo hash, this will notify everyone that the sysinfo document was changed
+ var event = { etype: 'node', action: 'sysinfohash', nodeid: obj.dbNodeKey, domain: domain.id, hash: command.data.hash, nolog: 1 };
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, event);
+ }
+ break;
+ }
+ case 'sysinfocheck': {
+ // Check system information update
+ db.GetHash('si' + obj.dbNodeKey, function (err, results) {
+ if ((results != null) && (results.length == 1)) { obj.send(JSON.stringify({ action: 'sysinfo', hash: results[0].hash })); } else { obj.send(JSON.stringify({ action: 'sysinfo' })); }
+ });
+ break;
+ }
+ case 'sessions': {
+ // This is a list of sessions provided by the agent
+ if (obj.sessions == null) { obj.sessions = {}; }
+ if (typeof command.value != null) {
+ if (command.type == 'kvm') { obj.sessions.kvm = command.value; }
+ else if (command.type == 'terminal') { obj.sessions.terminal = command.value; }
+ else if (command.type == 'files') { obj.sessions.files = command.value; }
+ else if (command.type == 'help') { obj.sessions.help = command.value; }
+ else if (command.type == 'tcp') { obj.sessions.tcp = command.value; }
+ else if (command.type == 'udp') { obj.sessions.udp = command.value; }
+ else if (command.type == 'msg') { obj.sessions.msg = command.value; }
+ else if (command.type == 'app') { obj.sessions.app = command.value; }
+ }
+
+ // Any "help" session must have an associated app, if not, remove it.
+ if (obj.sessions.help != null) {
+ for (var i in obj.sessions.help) { if (obj.sessions.help[i] == null) { delete obj.sessions.help[i]; } }
+ if (Object.keys(obj.sessions.help).length == 0) { delete obj.sessions.help; }
+ }
+
+ // Inform everyone of updated sessions
+ obj.updateSessions();
+ break;
+ }
+ case 'battery': {
+ // Device battery and power state
+ if (obj.sessions == null) { obj.sessions = {}; }
+ if (obj.sessions.battery == null) { obj.sessions.battery = {}; }
+ if ((command.state == 'ac') || (command.state == 'dc')) { obj.sessions.battery.state = command.state; } else { delete obj.sessions.battery.state; }
+ if ((typeof command.level == 'number') && (command.level >= 0) && (command.level <= 100)) { obj.sessions.battery.level = command.level; } else { delete obj.sessions.battery.level; }
+ obj.updateSessions();
+ break;
+ }
+ case 'getcoredump': {
+ // Check if we requested a core dump file in the last minute, if so, ignore this.
+ if ((parent.lastCoreDumpRequest != null) && ((Date.now() - parent.lastCoreDumpRequest) < 60000)) break;
+
+ // Indicates if the agent has a coredump available
+ if ((command.exists === true) && (typeof command.agenthashhex == 'string') && (command.agenthashhex.length == 96)) {
+ // Check if we already have this exact dump file
+ const coreDumpFile = parent.path.join(parent.parent.datapath, '..', 'meshcentral-coredumps', obj.agentInfo.agentId + '-' + command.agenthashhex + '-' + obj.nodeid + '.dmp');
+ parent.fs.stat(coreDumpFile, function (err, stats) {
+ if (stats != null) return;
+ obj.coreDumpPresent = true;
+
+ // Check how many files are in the coredumps folder
+ const coreDumpPath = parent.path.join(parent.parent.datapath, '..', 'meshcentral-coredumps');
+ parent.fs.readdir(coreDumpPath, function (err, files) {
+ if ((files != null) && (files.length >= 20)) return; // Don't get more than 20 core dump files.
+
+ // Get the core dump uploaded to the server.
+ parent.lastCoreDumpRequest = Date.now();
+ obj.RequestCoreDump(command.agenthashhex, command.corehashhex);
+ });
+ });
+ }
+ break;
+ }
+ case 'tunnelCloseStats': {
+ // TODO: This this extra stats from the tunnel, you can merge this into the tunnel event in the database.
+ //console.log(command);
+
+ // Validate input
+ if ((command.sent == null) || (typeof command.sent != 'string')) return;
+ if ((command.sentActual == null) || (typeof command.sentActual != 'string')) return;
+ if ((command.sentActual == null) || (typeof command.sentActual != 'number')) return;
+
+ // Event the session closed compression data.
+ var event = { etype: 'node', action: 'sessioncompression', nodeid: obj.dbNodeKey, domain: domain.id, sent: parseInt(command.sent), sentActual: parseInt(command.sentActual), msgid: 54, msgArgs: [command.sentRatio, parseInt(command.sent), parseInt(command.sentActual)], msg: 'Agent closed session with ' + command.sentRatio + '% agent to server compression. Sent: ' + command.sent + ', Compressed: ' + command.sentActual + '.' };
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, event);
+ break;
+ }
+ case 'lmsinfo': {
+ // Agents send the LMS port bindings
+ // Example: {"action":"lmsinfo","value":{"ports":["623","16992"]}}
+ break;
+ }
+ case 'plugin': {
+ if ((parent.parent.pluginHandler == null) || (typeof command.plugin != 'string')) break;
+ try {
+ parent.parent.pluginHandler.plugins[command.plugin].serveraction(command, obj, parent);
+ } catch (e) {
+ parent.parent.debug('agent', 'Error loading plugin handler (' + e + ')');
+ console.log('Error loading plugin handler (' + e + ')');
+ }
+ break;
+ }
+ case 'meshToolInfo': {
+ // Return information about a MeshCentral tool. Current tools are 'MeshCentralRouter' and 'MeshCentralAssistant'
+ // Information includes file hash and download location URL
+ if (typeof command.name != 'string') break;
+ var info = parent.parent.meshToolsBinaries[command.name];
+ if ((command.hash != null) && (info.hash == command.hash)) return;
+
+ // To build the connection URL, if we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
+ var xdomain = (domain.dns == null) ? domain.id : '';
+ if (xdomain != '') xdomain += '/';
+
+ // Build the response
+ const responseCmd = { action: 'meshToolInfo', name: command.name, tag: command.tag, sessionid: command.sessionid, hash: info.hash, size: info.size, url: info.url };
+ if ((command.name == 'MeshCentralAssistant') && (command.msh == true)) { responseCmd.url = '*/' + xdomain + 'meshagents?id=10006'; } // If this is Assistant and the MSH needs to be included in the executable, change the URL.
+ if (command.cookie === true) { responseCmd.url += ('&auth=' + parent.parent.encodeCookie({ download: info.dlname }, parent.parent.loginCookieEncryptionKey)); }
+ if (command.pipe === true) { responseCmd.pipe = true; }
+ if (parent.webCertificateHashs[domain.id] != null) { responseCmd.serverhash = Buffer.from(parent.webCertificateHashs[domain.id], 'binary').toString('hex'); }
+ try { ws.send(JSON.stringify(responseCmd)); } catch (ex) { }
+ break;
+ }
+ case 'agentupdate': {
+ if ((obj.agentExeInfo != null) && (typeof obj.agentExeInfo.url == 'string')) {
+ var func = function agentUpdateFunc(argument, taskid, taskLimiterQueue) { // Medium priority task
+ // If agent disconnection, complete and exit now.
+ if (obj.authenticated != 2) { parent.parent.taskLimiter.completed(taskid); return; }
+
+ // Agent is requesting an agent update
+ obj.agentCoreUpdateTaskId = taskid;
+ const url = '*' + require('url').parse(obj.agentExeInfo.url).path;
+ var cmd = { action: 'agentupdate', url: url, hash: obj.agentExeInfo.hashhex, sessionid: agentUpdateFunc.sessionid };
+ parent.parent.debug('agentupdate', "Sending user requested agent update url: " + cmd.url);
+
+ // Add the hash
+ if (obj.agentExeInfo.fileHash != null) { cmd.hash = obj.agentExeInfo.fileHashHex; } else { cmd.hash = obj.agentExeInfo.hashhex; }
+
+ // Add server TLS cert hash
+ if (isIgnoreHashCheck() == false) {
+ const tlsCertHash = parent.webCertificateFullHashs[domain.id];
+ if (tlsCertHash != null) { cmd.servertlshash = Buffer.from(tlsCertHash, 'binary').toString('hex'); }
+ }
+
+ // Send the agent update command
+ obj.send(JSON.stringify(cmd));
+ }
+ func.sessionid = command.sessionid;
+
+ // Agent update. The recovery core was loaded in the agent, send a command to update the agent
+ parent.parent.taskLimiter.launch(func, null, 1);
+ }
+ break;
+ }
+ case 'agentupdatedownloaded': {
+ if (obj.agentCoreUpdateTaskId != null) {
+ // Indicate this udpate task is complete
+ parent.parent.taskLimiter.completed(obj.agentCoreUpdateTaskId);
+ delete obj.agentCoreUpdateTaskId;
+ }
+ break;
+ }
+ case 'errorlog': { // This is the agent error log
+ if ((!Array.isArray(command.log)) || (command.log.length == 0) || (parent.parent.agentErrorLog == null)) break;
+ var lastLogEntry = command.log[command.log.length - 1];
+ if ((lastLogEntry != null) && (typeof lastLogEntry == 'object') && (typeof lastLogEntry.t == 'number')) {
+ parent.fs.write(parent.parent.agentErrorLog, obj.dbNodeKey + ', ' + Date.now() + ', ' + str + '\r\n', function (err) { });
+ db.Set({ _id: 'al' + obj.dbNodeKey, lastEvent: lastLogEntry.t });
+ }
+ break;
+ }
+ case '2faauth': {
+ // Validate input
+ if ((typeof command.url != 'string') || (typeof command.approved != 'boolean') || (command.url.startsWith('2fa://') == false)) return;
+
+ // parse the URL
+ var url = null;
+ try { url = require('url').parse(command.url); } catch (ex) { }
+ if (url == null) return;
+
+ // Decode the cookie
+ var urlSplit = url.query.split('&c=');
+ if (urlSplit.length != 2) return;
+ const authCookie = parent.parent.decodeCookie(urlSplit[1], null, 1);
+ if ((authCookie == null) || (typeof authCookie.c != 'string') || (('code=' + authCookie.c) != urlSplit[0])) return;
+ if ((typeof authCookie.n != 'string') || (authCookie.n != obj.dbNodeKey) || (typeof authCookie.u != 'string')) return;
+
+ // Fetch the user
+ const user = parent.users[authCookie.u];
+ if (user == null) return;
+
+ // Add this device as the authentication push notification device for this user
+ if (authCookie.a == 'addAuth') {
+ // Do nothing if authentication is not approved.
+ // We do not want to indicate that the remote user responded to this.
+ if (command.approved !== true) return;
+
+ // Change the user
+ user.otpdev = obj.dbNodeKey;
+ parent.db.SetUser(user);
+
+ // Notify change
+ var targets = ['*', 'server-users', user._id];
+ if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
+ var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: 113, msg: "Added push notification authentication device", domain: domain.id };
+ if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
+ parent.parent.DispatchEvent(targets, obj, event);
+ }
+
+ // Complete 2FA checking
+ if (authCookie.a == 'checkAuth') {
+ if (typeof authCookie.s != 'string') return;
+ // Notify 2FA response
+ parent.parent.DispatchEvent(['2fadev-' + authCookie.s], obj, { etype: '2fadev', action: '2faresponse', domain: domain.id, nodeid: obj.dbNodeKey, code: authCookie.a, userid: user._id, approved: command.approved, sessionid: authCookie.s, nolog: 1 });
+ }
+
+ break;
+ }
+ case 'getUserImage': {
+ // Validate input
+ if (typeof command.userid != 'string') {
+ // Send back the default image if required
+ if ((command.default) || (command.sentDefault)) {
+ try { command.image = 'data:image/png;base64,' + Buffer.from(parent.fs.readFileSync(parent.parent.path.join(__dirname, 'public', 'images', 'user-128.png')), 'binary').toString('base64'); } catch (ex) { }
+ obj.send(JSON.stringify(command));
+ }
+ return;
+ }
+ var useridsplit = command.userid.split('/');
+ if ((useridsplit.length != 3) || (useridsplit[1] != domain.id)) return;
+
+ // Add the user's real name if present
+ var u = parent.users[command.userid];
+ if (u == null) return;
+ if (u.name) { command.name = u.name; }
+ if (u.realname) { command.realname = u.realname; }
+
+ // An agent can only request images of accounts with rights to the device.
+ if (parent.GetNodeRights(command.userid, obj.dbMeshKey, obj.dbNodeKey) != 0) {
+ parent.db.Get('im' + command.userid, function (err, images) {
+ if ((err == null) && (images != null) && (images.length == 1)) {
+ // Send back the account image
+ command.image = images[0].image;
+ } else {
+ // Send back the default image if required
+ if ((command.default) || (command.sentDefault)) {
+ try { command.image = 'data:image/png;base64,' + Buffer.from(parent.fs.readFileSync(parent.parent.path.join(__dirname, 'public', 'images', 'user-128.png')), 'binary').toString('base64'); } catch (ex) { }
+ }
+ }
+ obj.send(JSON.stringify(command));
+ });
+ }
+ break;
+ }
+ case 'getServerImage': {
+ if (command.agent === 'assistant') {
+ // Return server title and image for MeshCentral Assistant
+ if ((domain.assistantcustomization != null) && (typeof domain.assistantcustomization == 'object')) {
+ var ok = false;
+ if (typeof domain.assistantcustomization.title == 'string') { ok = true; command.title = domain.assistantcustomization.title; }
+ if (typeof domain.assistantcustomization.image == 'string') { try { command.image = 'data:image/jpeg;base64,' + Buffer.from(parent.fs.readFileSync(parent.parent.getConfigFilePath(domain.assistantcustomization.image)), 'binary').toString('base64'); ok = true; } catch (ex) { console.log(ex); } }
+ if (ok) { obj.send(JSON.stringify(command)); }
+ }
+ }
+ if (command.agent === 'android') {
+ // Return server title and image for MeshCentral Assistant
+ if ((domain.androidcustomization != null) && (typeof domain.androidcustomization == 'object')) {
+ var ok = false;
+ if (typeof domain.androidcustomization.title == 'string') { ok = true; command.title = domain.androidcustomization.title; }
+ if (typeof domain.androidcustomization.subtitle == 'string') { ok = true; command.subtitle = domain.androidcustomization.subtitle; }
+ if (typeof domain.androidcustomization.image == 'string') { try { command.image = 'data:image/jpeg;base64,' + Buffer.from(parent.fs.readFileSync(parent.parent.getConfigFilePath(domain.androidcustomization.image)), 'binary').toString('base64'); ok = true; } catch (ex) { console.log(ex); } }
+ if (ok) { obj.send(JSON.stringify(command)); }
+ }
+ }
+ break;
+ }
+ case 'guestShare': {
+ if ((command.flags == null) || (command.flags == 0)) {
+ // Stop any current self-share, this is allowed even if self guest sharing is not allows so to clear any old shares.
+ removeGuestSharing(function () {
+ delete obj.guestSharing;
+ obj.send(JSON.stringify({ action: 'guestShare', flags: command.flags, url: null, viewOnly: false }));
+ });
+ } else {
+ // Add a new self-share, this will replace any share for this device
+ if ((domain.agentselfguestsharing == null) || (domain.agentselfguestsharing == false) || (typeof command.flags != 'number')) return; // Check if agent self-sharing is allowed, this is off by default.
+ if ((command.flags & 2) == 0) { command.viewOnly = false; } // Only allow "view only" if desktop is shared.
+ addGuestSharing(command.flags, command.viewOnly, function (share) {
+ obj.guestSharing = true;
+ obj.send(JSON.stringify({ action: 'guestShare', url: share.url, flags: share.flags, viewOnly: share.viewOnly }));
+ })
+ }
+ break;
+ }
+ case 'amtconfig': {
+ // Sent by the agent when the agent needs a Intel AMT APF connection to the server
+ const cookie = parent.parent.encodeCookie({ a: 'apf', n: obj.dbNodeKey, m: obj.dbMeshKey }, parent.parent.loginCookieEncryptionKey);
+ try { obj.send(JSON.stringify({ action: 'amtconfig', user: '**MeshAgentApfTunnel**', pass: cookie })); } catch (ex) { }
+ break;
+ }
+ case 'script-task': {
+ // These command are for running regular batch jobs on the remote device
+ if (parent.parent.taskManager != null) { parent.parent.taskManager.agentAction(command, obj); }
+ break;
+ }
+ default: {
+ parent.agentStats.unknownAgentActionCount++;
+ parent.parent.debug('agent', 'Unknown agent action (' + obj.remoteaddrport + '): ' + JSON.stringify(command) + '.');
+ console.log('Unknown agent action (' + obj.remoteaddrport + '): ' + JSON.stringify(command) + '.');
+ break;
+ }
+ }
+ if (parent.parent.pluginHandler != null) {
+ parent.parent.pluginHandler.callHook('hook_processAgentData', command, obj, parent);
+ }
+ }
+ }
+
+ function addGuestSharing(flags, viewOnly, func) {
+ // Create cookie
+ const publicid = 'AS:' + obj.dbNodeKey;
+ const extrakey = getRandomAmtPassword();
+ const cookie = { a: 6, pid: publicid, k: extrakey }; // New style sharing cookie
+ const inviteCookie = parent.parent.encodeCookie(cookie, parent.parent.invitationLinkEncryptionKey);
+ if (inviteCookie == null) return;
+
+ // Create the server url
+ var serverName = parent.getWebServerName(domain, req);
+ var httpsPort = ((args.aliasport == null) ? args.port : args.aliasport); // Use HTTPS alias port is specified
+ var xdomain = (domain.dns == null) ? domain.id : '';
+ if (xdomain != '') xdomain += '/';
+ var url = 'https://' + serverName + ':' + httpsPort + '/' + xdomain + 'sharing?c=' + inviteCookie;
+ if (serverName.split('.') == 1) { url = '/' + xdomain + page + '?c=' + inviteCookie; }
+
+ // Create a device sharing database entry
+ var shareEntry = { _id: 'deviceshare-' + publicid, type: 'deviceshare', nodeid: obj.dbNodeKey, p: flags, domain: domain.id, publicid: publicid, guestName: 'Agent', consent: 0x7F, url: url, extrakey: extrakey };
+
+ // Add expire time
+ if ((typeof domain.agentselfguestsharing == 'object') && (typeof domain.agentselfguestsharing.expire == 'number') && (domain.agentselfguestsharing.expire > 0)) {
+ shareEntry.startTime = Date.now();
+ shareEntry.expireTime = Date.now() + (60000 * domain.agentselfguestsharing.expire);
+ }
+
+ if (viewOnly === true) { shareEntry.viewOnly = true; }
+ parent.db.Set(shareEntry);
+
+ // Send out an event that we added a device share
+ var targets = parent.CreateNodeDispatchTargets(obj.dbMeshKey, obj.dbNodeKey);
+ var event = { etype: 'node', nodeid: obj.dbNodeKey, action: 'addedDeviceShare', msg: 'Added device share with unlimited time', msgid: 131, msgArgs: ['Agent'], domain: domain.id };
+ parent.parent.DispatchEvent(targets, obj, event);
+
+ // Send device share update
+ parent.db.GetAllTypeNodeFiltered([obj.dbNodeKey], domain.id, 'deviceshare', null, function (err, docs) {
+ if (err != null) return;
+
+ // Check device sharing
+ var now = Date.now();
+ for (var i = 0; i < docs.length; i++) {
+ const doc = docs[i];
+ if (doc.expireTime < now) { parent.db.Remove(doc._id, function () { }); delete docs[i]; } else {
+ // This share is ok, remove extra data we don't need to send.
+ delete doc._id; delete doc.domain; delete doc.nodeid; delete doc.type;
+ }
+ }
+
+ // Send device share update
+ var targets = parent.CreateNodeDispatchTargets(obj.dbMeshKey, obj.dbNodeKey, []);
+ parent.parent.DispatchEvent(targets, obj, { etype: 'node', nodeid: obj.dbNodeKey, action: 'deviceShareUpdate', domain: domain.id, deviceShares: docs, nolog: 1 });
+
+ // Callback
+ if (func) { func({ url: url, flags: flags, viewOnly: viewOnly }); }
+ });
+ }
+
+ function removeGuestSharing(func) {
+ var publicid = 'AS:' + obj.dbNodeKey;
+ parent.db.GetAllTypeNodeFiltered([obj.dbNodeKey], domain.id, 'deviceshare', null, function (err, docs) {
+ if (err != null) return;
+
+ // Remove device sharing
+ var now = Date.now(), removedExact = null, removed = false, okDocs = [];
+ for (var i = 0; i < docs.length; i++) {
+ const doc = docs[i];
+ if (doc.publicid == publicid) { parent.db.Remove(doc._id, function () { }); removedExact = doc; removed = true; }
+ else if (doc.expireTime < now) { parent.db.Remove(doc._id, function () { }); removed = true; } else {
+ // This share is ok, remove extra data we don't need to send.
+ delete doc._id; delete doc.domain; delete doc.nodeid; delete doc.type;
+ okDocs.push(doc);
+ }
+ }
+
+ // Event device share removal
+ if (removedExact != null) {
+ // Send out an event that we removed a device share
+ var targets = parent.CreateNodeDispatchTargets(obj.dbMeshKey, obj.dbNodeKey, ['server-shareremove']);
+ var event = { etype: 'node', nodeid: obj.dbNodeKey, action: 'removedDeviceShare', msg: 'Removed Device Share', msgid: 102, msgArgs: ['Agent'], domain: domain.id, publicid: publicid };
+ parent.parent.DispatchEvent(targets, obj, event);
+ }
+
+ // If we removed any shares, send device share update
+ if (removed == true) {
+ var targets = parent.CreateNodeDispatchTargets(obj.dbMeshKey, obj.dbNodeKey, []);
+ parent.parent.DispatchEvent(targets, obj, { etype: 'node', nodeid: obj.dbNodeKey, action: 'deviceShareUpdate', domain: domain.id, deviceShares: okDocs, nolog: 1 });
+ }
+
+ // Call back when done
+ if (func) func(removed);
+ });
+ }
+
+ // Notify update of sessions
+ obj.updateSessions = function () {
+ // Perform some clean up
+ for (var i in obj.sessions) { if (Object.keys(obj.sessions[i]).length == 0) { delete obj.sessions[i]; } }
+ if (Object.keys(obj.sessions).length == 0) { delete obj.sessions; }
+
+ // Event the new sessions, this will notify everyone that agent sessions have changed
+ var event = { etype: 'node', action: 'devicesessions', nodeid: obj.dbNodeKey, domain: domain.id, sessions: obj.sessions, nolog: 1 };
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, event);
+ }
+
+ // Change the current core information string and event it
+ function ChangeAgentCoreInfo(command) {
+ if ((obj.agentInfo == null) || (obj.agentInfo.capabilities & 0x40)) return;
+ if ((command == null) || (command == null)) return; // Safety, should never happen.
+
+ // If the device is pending a change, hold.
+ if (obj.deviceChanging === true) { setTimeout(function () { ChangeAgentCoreInfo(command); }, 100); return; }
+ obj.deviceChanging = true;
+
+ // Check that the mesh exists
+ const mesh = parent.meshes[obj.dbMeshKey];
+ if (mesh == null) { delete obj.deviceChanging; return; }
+
+ // Get the node and change it if needed
+ db.Get(obj.dbNodeKey, function (err, nodes) { // TODO: THIS IS A BIG RACE CONDITION HERE, WE NEED TO FIX THAT. If this call is made twice at the same time on the same device, data will be missed.
+ if ((nodes == null) || (nodes.length != 1)) { delete obj.deviceChanging; return; }
+ const device = nodes[0];
+ if (device.agent) {
+ var changes = [], change = 0, log = 0;
+
+ // Check if anything changes
+ if (command.name && (typeof command.name == 'string') && (command.name != device.name)) { change = 1; log = 1; device.name = command.name; changes.push('name'); }
+ if ((command.caps != null) && (device.agent.core != command.value)) { if ((command.value == null) && (device.agent.core != null)) { delete device.agent.core; } else { device.agent.core = command.value; } change = 1; } // Don't save this as an event to the db.
+ if ((command.caps != null) && ((device.agent.caps & 0xFFFFFFE7) != (command.caps & 0xFFFFFFE7))) { device.agent.caps = ((device.agent.caps & 24) + (command.caps & 0xFFFFFFE7)); change = 1; } // Allow Javascript on the agent to change all capabilities except console and javascript support, Don't save this as an event to the db.
+ if ((command.osdesc != null) && (typeof command.osdesc == 'string') && (device.osdesc != command.osdesc)) { device.osdesc = command.osdesc; change = 1; changes.push('os desc'); } // Don't save this as an event to the db.
+ if ((typeof command.root == 'boolean') && (command.root !== device.agent.root)) { change = 1; device.agent.root = command.root; }
+ if (device.ip != obj.remoteaddr) { device.ip = obj.remoteaddr; change = 1; }
+ if (command.intelamt) {
+ if (!device.intelamt) { device.intelamt = {}; }
+ if ((command.intelamt.Versions != null) && (typeof command.intelamt.Versions == 'object')) {
+ if ((command.intelamt.Versions.AMT != null) && (typeof command.intelamt.Versions.AMT == 'string') && (command.intelamt.Versions.AMT.length < 12) && (device.intelamt.ver != command.intelamt.Versions.AMT)) { changes.push('AMT version'); device.intelamt.ver = command.intelamt.Versions.AMT; change = 1; log = 1; }
+ if ((command.intelamt.Versions.Sku != null) && (typeof command.intelamt.Versions.Sku == 'string')) {
+ const sku = parseInt(command.intelamt.Versions.Sku);
+ if (device.intelamt.sku !== sku) { device.intelamt.sku = sku; change = 1; log = 1; }
+ }
+ }
+ if ((command.intelamt.ProvisioningState != null) && (typeof command.intelamt.ProvisioningState == 'number') && (device.intelamt.state != command.intelamt.ProvisioningState)) { changes.push('AMT state'); device.intelamt.state = command.intelamt.ProvisioningState; change = 1; log = 1; }
+ if ((command.intelamt.Flags != null) && (typeof command.intelamt.Flags == 'number') && (device.intelamt.flags != command.intelamt.Flags)) {
+ if (device.intelamt.flags) { changes.push('AMT flags (' + device.intelamt.flags + ' --> ' + command.intelamt.Flags + ')'); } else { changes.push('AMT flags (' + command.intelamt.Flags + ')'); }
+ device.intelamt.flags = command.intelamt.Flags; change = 1; log = 1;
+ }
+ if ((command.intelamt.UUID != null) && (typeof command.intelamt.UUID == 'string') && (device.intelamt.uuid != command.intelamt.UUID)) { changes.push('AMT uuid'); device.intelamt.uuid = command.intelamt.UUID; change = 1; log = 1; }
+ }
+ if (command.av != null) { // Antivirus
+ if (!device.av) { device.av = []; }
+ if (JSON.stringify(device.av) != JSON.stringify(command.av)) { /*changes.push('AV status');*/ device.av = command.av; change = 1; log = 1; }
+ }
+ if (command.wsc != null) { // Windows Security Center
+ if (!device.wsc) { device.wsc = {}; }
+ if (JSON.stringify(device.wsc) != JSON.stringify(command.wsc)) { /*changes.push('Windows Security Center status');*/ device.wsc = command.wsc; change = 1; log = 1; }
+ }
+ if (command.defender != null) { // Defender For Windows Server
+ if (!device.defender) { device.defender = {}; }
+ if (JSON.stringify(device.defender) != JSON.stringify(command.defender)) { /*changes.push('Defender status');*/ device.defender = command.defender; change = 1; log = 1; }
+ }
+ if (command.lastbootuptime != null) { // Last Boot Up Time
+ if (!device.lastbootuptime) { device.lastbootuptime = ""; }
+ if (device.lastbootuptime != command.lastbootuptime) { /*changes.push('Last Boot Up Time');*/ device.lastbootuptime = command.lastbootuptime; change = 1; log = 1; }
+ }
+
+ // Push Messaging Token
+ if ((command.pmt != null) && (typeof command.pmt == 'string') && (device.pmt != command.pmt)) {
+ if (typeof device.pmt == 'string') { db.Remove('pmt_' + device.pmt); }
+ device.pmt = command.pmt;
+ change = 1; // Don't save this change as an event to the db, so no log=1.
+ parent.removePmtFromAllOtherNodes(device); // We need to make sure to remove this push messaging token from any other device on this server, all domains included.
+ }
+
+ if ((command.users != null) && (Array.isArray(command.users)) && (device.users != command.users)) { device.users = command.users; change = 1; } // Don't save this to the db.
+ if ((command.lusers != null) && (Array.isArray(command.lusers)) && (device.lusers != command.lusers)) { device.lusers = command.lusers; change = 1; } // Don't save this to the db.
+ if ((mesh.mtype == 2) && (!args.wanonly)) {
+ // In WAN mode, the hostname of a computer is not important. Don't log hostname changes.
+ if (device.host != obj.remoteaddr) { device.host = obj.remoteaddr; change = 1; changes.push('host'); }
+ // TODO: Check that the agent has an interface that is the same as the one we got this websocket connection on. Only set if we have a match.
+ }
+
+ // Remove old volumes and BitLocker data, this is part of sysinfo.
+ delete device.volumes;
+
+ // If there are changes, event the new device
+ if (change == 1) {
+ // Do some clean up if needed, these values should not be in the database.
+ if (device.conn != null) { delete device.conn; }
+ if (device.pwr != null) { delete device.pwr; }
+ if (device.agct != null) { delete device.agct; }
+ if (device.cict != null) { delete device.cict; }
+
+ // Save to the database
+ db.Set(device);
+
+ // Event the node change
+ var event = { etype: 'node', action: 'changenode', nodeid: obj.dbNodeKey, domain: domain.id, node: parent.CloneSafeNode(device) };
+ if (changes.length > 0) { event.msg = 'Changed device ' + device.name + ' from group ' + mesh.name + ': ' + changes.join(', '); }
+ if ((log == 0) || ((obj.agentInfo) && (obj.agentInfo.capabilities) && (obj.agentInfo.capabilities & 0x20)) || (changes.length == 0)) { event.nolog = 1; } // If this is a temporary device, don't log changes
+ if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(device.meshid, [obj.dbNodeKey]), obj, event);
+ }
+
+ // Device change is done.
+ delete obj.deviceChanging;
+ }
+ });
+ }
+
+ // Change the current core information string and event it
+ function ChangeAgentLocationInfo(command) {
+ if (obj.agentInfo.capabilities & 0x40) return;
+ if ((command == null) || (command == null)) { return; } // Safety, should never happen.
+
+ // Check that the mesh exists
+ const mesh = parent.meshes[obj.dbMeshKey];
+ if (mesh == null) return;
+
+ // If the device is pending a change, hold.
+ if (obj.deviceChanging === true) { setTimeout(function () { ChangeAgentLocationInfo(command); }, 100); return; }
+ obj.deviceChanging = true;
+
+ // Get the node and change it if needed
+ db.Get(obj.dbNodeKey, function (err, nodes) {
+ if ((nodes == null) || (nodes.length != 1)) { delete obj.deviceChanging; return; }
+ const device = nodes[0];
+ if (device.agent) {
+ var changes = [], change = 0;
+
+ // Check if anything changes
+ if ((command.publicip) && (device.publicip != command.publicip)) { device.publicip = command.publicip; change = 1; changes.push('public ip'); }
+ if ((command.iploc) && (device.iploc != command.iploc)) { device.iploc = command.iploc; change = 1; changes.push('ip location'); }
+
+ // If there are changes, save and event
+ if (change == 1) {
+ // Do some clean up if needed, these values should not be in the database.
+ if (device.conn != null) { delete device.conn; }
+ if (device.pwr != null) { delete device.pwr; }
+ if (device.agct != null) { delete device.agct; }
+ if (device.cict != null) { delete device.cict; }
+
+ // Save the device
+ db.Set(device);
+
+ // Event the node change
+ var event = { etype: 'node', action: 'changenode', nodeid: obj.dbNodeKey, domain: domain.id, node: parent.CloneSafeNode(device), msgid: 59, msgArgs: [device.name, mesh.name, changes.join(', ')], msg: 'Changed device ' + device.name + ' from group ' + mesh.name + ': ' + changes.join(', ') };
+ if (obj.agentInfo.capabilities & 0x20) { event.nolog = 1; } // If this is a temporary device, don't log changes
+ if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(device.meshid, [obj.dbNodeKey]), obj, event);
+ }
+ }
+
+ // Done changing the device
+ delete obj.deviceChanging;
+ });
+ }
+
+ // Update the mesh agent tab in the database
+ function ChangeAgentTag(tag) {
+ if ((obj.agentInfo == null) || (obj.agentInfo.capabilities & 0x40)) return;
+ if ((tag != null) && (tag.length == 0)) { tag = null; }
+
+ // If the device is pending a change, hold.
+ if (obj.deviceChanging === true) {
+ var func = function ChangeAgentTagFunc() { ChangeAgentCoreInfo(ChangeAgentTagFunc.tag); }
+ func.tag = tag;
+ setTimeout(func, 100);
+ return;
+ }
+ obj.deviceChanging = true;
+
+ // Get the node and change it if needed
+ db.Get(obj.dbNodeKey, function (err, nodes) {
+ if ((nodes == null) || (nodes.length != 1)) { delete obj.deviceChanging; return; }
+ const device = nodes[0];
+ if (device.agent) {
+ // Parse the agent tag
+ var agentTag = null, serverName = null, serverDesc = null, serverTags = null;
+ if (tag != null) {
+ var taglines = tag.split('\r\n').join('\n').split('\r').join('\n').split('\n');
+ for (var i in taglines) {
+ var tagline = taglines[i].trim();
+ if (tagline.length > 0) {
+ if (tagline.startsWith('~')) {
+ if (tagline.startsWith('~ServerName:') && (tagline.length > 12) && (serverName == null)) { serverName = tagline.substring(12).trim(); }
+ if (tagline.startsWith('~ServerDesc:') && (tagline.length > 12) && (serverDesc == null)) { serverDesc = tagline.substring(12).trim(); }
+ if (tagline.startsWith('~ServerTags:') && (tagline.length > 12) && (serverTags == null)) { serverTags = tagline.substring(12).split(','); for (var j in serverTags) { serverTags[j] = serverTags[j].trim(); } }
+ } else { if (agentTag == null) { agentTag = tagline; } }
+ }
+ }
+ }
+
+ // Set the agent tag
+ var changes = false;
+ if (device.agent.tag != agentTag) { device.agent.tag = agentTag; if ((device.agent.tag == null) || (device.agent.tag == '')) { delete device.agent.tag; } changes = true; }
+ if (domain.agenttag != null) {
+ // Set the device's server name
+ if ((serverName != null) && (domain.agenttag.servername === 1) && (device.name != serverName)) { device.name = serverName; changes = true; }
+
+ // Set the device's server description
+ if ((serverDesc != null) && (domain.agenttag.serverdesc === 1) && (device.desc != serverDesc)) { device.desc = serverDesc; changes = true; }
+
+ // Set the device's server description if there is no description
+ if ((serverDesc != null) && (domain.agenttag.serverdesc === 2) && (device.desc != serverDesc) && ((device.desc == null) || (device.desc == ''))) { device.desc = serverDesc; changes = true; }
+
+ if ((serverTags != null) && (domain.agenttag.servertags != null) && (domain.agenttag.servertags != 0)) {
+ // Sort the tags
+ serverTags.sort();
+
+ // Stringify the tags
+ var st2 = '', st1 = serverTags.join(',');
+ if (device.tags != null) { st2 = device.tags.join(','); }
+
+ // Set the device's server tags
+ if ((domain.agenttag.servertags === 1) && (st1 != st2)) { device.tags = serverTags; changes = true; }
+
+ // Set the device's server tags if there are not tags
+ if ((domain.agenttag.servertags === 2) && (st2 == '')) { device.tags = serverTags; changes = true; }
+
+ // Append to device's server tags
+ if ((domain.agenttag.servertags === 3) && (st1 != st2)) {
+ if (device.tags == null) { device.tags = []; }
+ for (var i in serverTags) { if (device.tags.indexOf(serverTags[i]) == -1) { device.tags.push(serverTags[i]); } }
+ device.tags.sort();
+ changes = true;
+ }
+ }
+ }
+
+ if (changes == true) {
+ // Do some clean up if needed, these values should not be in the database.
+ if (device.conn != null) { delete device.conn; }
+ if (device.pwr != null) { delete device.pwr; }
+ if (device.agct != null) { delete device.agct; }
+ if (device.cict != null) { delete device.cict; }
+
+ // Update the device
+ db.Set(device);
+
+ // Event the node change
+ var event = { etype: 'node', action: 'changenode', nodeid: obj.dbNodeKey, domain: domain.id, node: parent.CloneSafeNode(device), nolog: 1 };
+ if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(device.meshid, [obj.dbNodeKey]), obj, event);
+ }
+ }
+
+ // Done changing the device
+ delete obj.deviceChanging;
+ });
+ }
+
+ // Check if we need to update this agent, return true if agent binary update required.
+ // Return 0 is no update needed, 1 update using native system, 2 update using meshcore system
+ function compareAgentBinaryHash(agentExeInfo, agentHash) {
+ // If this is a temporary agent and the server is set to not update temporary agents, don't update the agent.
+ if ((obj.agentInfo.capabilities & 0x20) && (args.temporaryagentupdate === false)) return 0;
+ // If we are testing the agent update system, always return true
+ if ((args.agentupdatetest === true) || (args.agentupdatetest === 1)) return 1;
+ if (args.agentupdatetest === 2) return 2;
+ // If the hash matches or is null, no update required.
+ if ((agentExeInfo.hash == agentHash) || (agentHash == '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0')) return 0;
+ // If this is a macOS x86 or ARM agent type and it matched the universal binary, no update required.
+ if ((agentExeInfo.id == 16) || (agentExeInfo.id == 29)) {
+ if (domain.meshAgentBinaries && domain.meshAgentBinaries[10005]) {
+ if (domain.meshAgentBinaries[10005].hash == agentHash) return 0;
+ } else {
+ if (parent.parent.meshAgentBinaries[10005].hash == agentHash) return 0;
+ }
+ }
+
+ // No match, update the agent.
+ if (args.agentupdatesystem === 2) return 2; // If set, force a meshcore update.
+ if (agentExeInfo.id == 3) return 2; // Due to a bug in Windows 7 SP1 environement variable exec, we always update 32bit Windows agent using MeshCore for now. Upcoming agent will have a fix for this.
+ // NOTE: Windows agents with no commit dates may have bad native update system, so use meshcore system instead.
+ // NOTE: Windows agents with commit date prior to 1612740413000 did not kill all "meshagent.exe" processes and update could fail as a result executable being locked, meshcore system will do this.
+ if (((obj.AgentCommitDate == null) || (obj.AgentCommitDate < 1612740413000)) && ((agentExeInfo.id == 3) || (agentExeInfo.id == 4))) return 2; // For older Windows agents, use the meshcore update technique.
+ return 1; // By default, use the native update technique.
+ }
+
+ // Request that the core dump file on this agent be uploaded to the server
+ obj.RequestCoreDump = function (agenthashhex, corehashhex) {
+ if (agenthashhex.length > 16) { agenthashhex = agenthashhex.substring(0, 16); }
+ const cookie = parent.parent.encodeCookie({ a: 'aft', b: 'coredump', c: obj.agentInfo.agentId + '-' + agenthashhex + '-' + obj.nodeid + '.dmp' }, parent.parent.loginCookieEncryptionKey);
+ obj.send('{"action":"msg","type":"tunnel","value":"*/' + (((domain.dns == null) && (domain.id != '')) ? (domain.id + '/') : '') + 'agenttransfer.ashx?c=' + cookie + '","rights":"4294967295"}');
+ }
+
+ // Return true if we need to ignore the agent hash check
+ function isIgnoreHashCheck() {
+ if ((args.ignoreagenthashcheck === true) || (domain.ignoreagenthashcheck === true)) return true;
+
+ // Check site wide exceptions
+ if (Array.isArray(args.ignoreagenthashcheck)) {
+ for (var i = 0; i < args.ignoreagenthashcheck.length; i++) {
+ if (require('ipcheck').match(obj.remoteaddr, args.ignoreagenthashcheck[i])) return true;
+ }
+ }
+
+ // Check domain wide exceptions
+ if (Array.isArray(domain.ignoreagenthashcheck)) {
+ for (var i = 0; i < domain.ignoreagenthashcheck.length; i++) {
+ if (require('ipcheck').match(obj.remoteaddr, domain.ignoreagenthashcheck[i])) return true;
+ }
+ }
+
+ return false;
+ }
+
+ // Generate a random Intel AMT password
+ function checkAmtPassword(p) { return (p.length > 7) && (/\d/.test(p)) && (/[a-z]/.test(p)) && (/[A-Z]/.test(p)) && (/\W/.test(p)); }
+ function getRandomAmtPassword() { var p; do { p = Buffer.from(parent.crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); } while (checkAmtPassword(p) == false); return p; }
+
+ return obj;
+};
diff --git a/meshcentral-config-schema.json b/meshcentral-config-schema.json
index 9d71e2f006..2d6162c6b8 100644
--- a/meshcentral-config-schema.json
+++ b/meshcentral-config-schema.json
@@ -1249,6 +1249,11 @@
"type": "integer",
"default": null,
"description": "The maximum number of devices a user can see on the devices page at the same time. By default all devices will show, but this may need to be limited on servers with large number of devices."
+ },
+ "preventDuplicateDevices": {
+ "type": "boolean",
+ "default": false,
+ "description": "If true, devices with the same name gets removed before a new device with the identical name gets joined. MeshID gets transfered."
},
"unknownUserRootRedirect": {
"type": "string",
diff --git a/meshcentral.js b/meshcentral.js
index 448bd48de7..3c770582df 100644
--- a/meshcentral.js
+++ b/meshcentral.js
@@ -1,4435 +1,4435 @@
-/**
-* @description MeshCentral main module
-* @author Ylian Saint-Hilaire
-* @copyright Intel Corporation 2018-2022
-* @license Apache-2.0
-* @version v0.0.1
-*/
-
-/*xjslint node: true */
-/*xjslint plusplus: true */
-/*xjslint maxlen: 256 */
-/*jshint node: true */
-/*jshint strict: false */
-/*jshint esversion: 6 */
-"use strict";
-
-const common = require('./common.js');
-
-// If app metrics is available
-if (process.argv[2] == '--launch') { try { require('appmetrics-dash').monitor({ url: '/', title: 'MeshCentral', port: 88, host: '127.0.0.1' }); } catch (ex) { } }
-
-function CreateMeshCentralServer(config, args) {
- const obj = {};
- obj.db = null;
- obj.webserver = null; // HTTPS main web server, typically on port 443
- obj.redirserver = null; // HTTP relay web server, typically on port 80
- obj.mpsserver = null; // Intel AMT CIRA server, typically on port 4433
- obj.mqttbroker = null; // MQTT server, not is not often used
- obj.swarmserver = null; // Swarm server, this is used only to update older MeshCentral v1 agents
- obj.smsserver = null; // SMS server, used to send user SMS messages
- obj.msgserver = null; // Messaging server, used to sent used messages
- obj.amtEventHandler = null;
- obj.pluginHandler = null;
- obj.amtScanner = null;
- obj.amtManager = null; // Intel AMT manager, used to oversee all Intel AMT devices, activate them and sync policies
- obj.meshScanner = null;
- obj.taskManager = null;
- obj.letsencrypt = null; // Let's encrypt server, used to get and renew TLS certificates
- obj.eventsDispatch = {};
- obj.fs = require('fs');
- obj.path = require('path');
- obj.crypto = require('crypto');
- obj.exeHandler = require('./exeHandler.js');
- obj.platform = require('os').platform();
- obj.args = args;
- obj.common = common;
- obj.configurationFiles = null;
- obj.certificates = null;
- obj.connectivityByNode = {}; // This object keeps a list of all connected CIRA and agents, by nodeid->value (value: 1 = Agent, 2 = CIRA, 4 = AmtDirect)
- obj.peerConnectivityByNode = {}; // This object keeps a list of all connected CIRA and agents of peers, by serverid->nodeid->value (value: 1 = Agent, 2 = CIRA, 4 = AmtDirect)
- obj.debugSources = [];
- obj.debugRemoteSources = null;
- obj.config = config; // Configuration file
- obj.dbconfig = {}; // Persistance values, loaded from database
- obj.certificateOperations = null;
- obj.defaultMeshCmd = null;
- obj.defaultMeshCores = {};
- obj.defaultMeshCoresDeflate = {};
- obj.defaultMeshCoresHash = {};
- obj.meshToolsBinaries = {}; // Mesh Tools Binaries, ToolName --> { hash:(sha384 hash), size:(binary size), path:(binary path) }
- obj.meshAgentBinaries = {}; // Mesh Agent Binaries, Architecture type --> { hash:(sha384 hash), size:(binary size), path:(binary path) }
- obj.meshAgentInstallScripts = {}; // Mesh Install Scripts, Script ID -- { hash:(sha384 hash), size:(binary size), path:(binary path) }
- obj.multiServer = null;
- obj.ipKvmManager = null;
- obj.maintenanceTimer = null;
- obj.serverId = null;
- obj.serverKey = Buffer.from(obj.crypto.randomBytes(48), 'binary');
- obj.loginCookieEncryptionKey = null;
- obj.invitationLinkEncryptionKey = null;
- obj.serverSelfWriteAllowed = true;
- obj.serverStatsCounter = Math.floor(Math.random() * 1000);
- obj.taskLimiter = obj.common.createTaskLimiterQueue(50, 20, 60); // (maxTasks, maxTaskTime, cleaningInterval) This is a task limiter queue to smooth out server work.
- obj.agentUpdateBlockSize = 65531; // MeshAgent update block size
- obj.serverWarnings = []; // List of warnings that should be shown to administrators
- obj.cookieUseOnceTable = {}; // List of cookies that are already expired
- obj.cookieUseOnceTableCleanCounter = 0; // Clean the cookieUseOnceTable each 20 additions
- obj.firstStats = true; // True until this server saves it's not stats to the database
-
- // Server version
- obj.currentVer = null;
- function getCurrentVersion() { try { obj.currentVer = JSON.parse(obj.fs.readFileSync(obj.path.join(__dirname, 'package.json'), 'utf8')).version; } catch (ex) { } return obj.currentVer; } // Fetch server version
- getCurrentVersion();
-
- // Setup the default configuration and files paths
- if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) {
- obj.parentpath = obj.path.join(__dirname, '../..');
- obj.datapath = obj.path.join(__dirname, '../../meshcentral-data');
- obj.filespath = obj.path.join(__dirname, '../../meshcentral-files');
- obj.backuppath = obj.path.join(__dirname, '../../meshcentral-backups');
- obj.recordpath = obj.path.join(__dirname, '../../meshcentral-recordings');
- obj.webViewsPath = obj.path.join(__dirname, 'views');
- obj.webPublicPath = obj.path.join(__dirname, 'public');
- obj.webEmailsPath = obj.path.join(__dirname, 'emails');
- if (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web/views'))) { obj.webViewsOverridePath = obj.path.join(__dirname, '../../meshcentral-web/views'); }
- if (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web/public'))) { obj.webPublicOverridePath = obj.path.join(__dirname, '../../meshcentral-web/public'); }
- if (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web/emails'))) { obj.webEmailsOverridePath = obj.path.join(__dirname, '../../meshcentral-web/emails'); }
- } else {
- obj.parentpath = __dirname;
- obj.datapath = obj.path.join(__dirname, '../meshcentral-data');
- obj.filespath = obj.path.join(__dirname, '../meshcentral-files');
- obj.backuppath = obj.path.join(__dirname, '../meshcentral-backups');
- obj.recordpath = obj.path.join(__dirname, '../meshcentral-recordings');
- obj.webViewsPath = obj.path.join(__dirname, 'views');
- obj.webPublicPath = obj.path.join(__dirname, 'public');
- obj.webEmailsPath = obj.path.join(__dirname, 'emails');
- if (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web/views'))) { obj.webViewsOverridePath = obj.path.join(__dirname, '../meshcentral-web/views'); }
- if (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web/public'))) { obj.webPublicOverridePath = obj.path.join(__dirname, '../meshcentral-web/public'); }
- if (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web/emails'))) { obj.webEmailsOverridePath = obj.path.join(__dirname, '../meshcentral-web/emails'); }
- }
-
- // Clean up any temporary files
- const removeTime = new Date(Date.now()).getTime() - (30 * 60 * 1000); // 30 minutes
- const dir = obj.fs.readdir(obj.path.join(obj.filespath, 'tmp'), function (err, files) {
- if (err != null) return;
- for (var i in files) { try { const filepath = obj.path.join(obj.filespath, 'tmp', files[i]); if (obj.fs.statSync(filepath).mtime.getTime() < removeTime) { obj.fs.unlink(filepath, function () { }); } } catch (ex) { } }
- });
-
- // Look to see if data and/or file path is specified
- if (obj.config.settings && (typeof obj.config.settings.datapath == 'string')) { obj.datapath = obj.config.settings.datapath; }
- if (obj.config.settings && (typeof obj.config.settings.filespath == 'string')) { obj.filespath = obj.config.settings.filespath; }
-
- // Create data and files folders if needed
- try { obj.fs.mkdirSync(obj.datapath); } catch (ex) { }
- try { obj.fs.mkdirSync(obj.filespath); } catch (ex) { }
-
- // Windows Specific Code, setup service and event log
- obj.service = null;
- obj.servicelog = null;
- if (obj.platform == 'win32') {
- const nodewindows = require('node-windows');
- obj.service = nodewindows.Service;
- const eventlogger = nodewindows.EventLogger;
- obj.servicelog = new eventlogger('MeshCentral');
- }
-
- // Start the Meshcentral server
- obj.Start = function () {
- var i;
- try { require('./pass').hash('test', function () { }, 0); } catch (ex) { console.log('Old version of node, must upgrade.'); return; } // TODO: Not sure if this test works or not.
-
- // Check for invalid arguments
- const validArguments = ['_', 'user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'rediraliasport', 'cert', 'mpscert', 'deletedomain', 'deletedefaultdomain', 'showall', 'showusers', 'showitem', 'listuserids', 'showusergroups', 'shownodes', 'showallmeshes', 'showmeshes', 'showevents', 'showsmbios', 'showpower', 'clearpower', 'showiplocations', 'help', 'exactports', 'xinstall', 'xuninstall', 'install', 'uninstall', 'start', 'stop', 'restart', 'debug', 'filespath', 'datapath', 'noagentupdate', 'launch', 'noserverbackup', 'mongodb', 'mongodbcol', 'wanonly', 'lanonly', 'nousers', 'mpspass', 'ciralocalfqdn', 'dbexport', 'dbexportmin', 'dbimport', 'dbmerge', 'dbfix', 'dbencryptkey', 'selfupdate', 'tlsoffload', 'usenodedefaulttlsciphers', 'tlsciphers', 'userallowedip', 'userblockedip', 'swarmallowedip', 'agentallowedip', 'agentblockedip', 'fastcert', 'swarmport', 'logintoken', 'logintokenkey', 'logintokengen', 'mailtokengen', 'admin', 'unadmin', 'sessionkey', 'sessiontime', 'minify', 'minifycore', 'dblistconfigfiles', 'dbshowconfigfile', 'dbpushconfigfiles', 'oldencrypt', 'dbpullconfigfiles', 'dbdeleteconfigfiles', 'vaultpushconfigfiles', 'vaultpullconfigfiles', 'vaultdeleteconfigfiles', 'configkey', 'loadconfigfromdb', 'npmpath', 'serverid', 'recordencryptionrecode', 'vault', 'token', 'unsealkey', 'name', 'log', 'dbstats', 'translate', 'createaccount', 'setuptelegram', 'resetaccount', 'pass', 'removesubdomain', 'adminaccount', 'domain', 'email', 'configfile', 'maintenancemode', 'nedbtodb', 'removetestagents', 'agentupdatetest', 'hashpassword', 'hashpass', 'indexmcrec', 'mpsdebug', 'dumpcores', 'dev', 'mysql', 'mariadb', 'trustedproxy'];
- for (var arg in obj.args) { obj.args[arg.toLocaleLowerCase()] = obj.args[arg]; if (validArguments.indexOf(arg.toLocaleLowerCase()) == -1) { console.log('Invalid argument "' + arg + '", use --help.'); return; } }
- const ENVVAR_PREFIX = "meshcentral_"
- let envArgs = []
- for (let [envvar, envval] of Object.entries(process.env)) {
- if (envvar.toLocaleLowerCase().startsWith(ENVVAR_PREFIX)) {
- let argname = envvar.slice(ENVVAR_PREFIX.length).toLocaleLowerCase()
- if (!!argname && !(validArguments.indexOf(argname) == -1)) {
- envArgs = envArgs.concat([`--${argname}`, envval])
- }
- }
- }
- envArgs = require('minimist')(envArgs)
- obj.args = Object.assign(envArgs, obj.args)
- if (obj.args.mongodb == true) { console.log('Must specify: --mongodb [connectionstring] \r\nSee https://docs.mongodb.com/manual/reference/connection-string/ for MongoDB connection string.'); return; }
- if (obj.args.mysql == true) { console.log('Must specify: --mysql [connectionstring] \r\nExample mysql://user:password@127.0.0.1:3306/database'); return; }
- if (obj.args.mariadb == true) { console.log('Must specify: --mariadb [connectionstring] \r\nExample mariadb://user:password@127.0.0.1:3306/database'); return; }
- for (i in obj.config.settings) { obj.args[i] = obj.config.settings[i]; } // Place all settings into arguments, arguments have already been placed into settings so arguments take precedence.
-
- if ((obj.args.help == true) || (obj.args['?'] == true)) {
- console.log('MeshCentral v' + getCurrentVersion() + ', remote computer management web portal.');
- console.log('This software is open source under Apache 2.0 license.');
- console.log('Details at: https://www.meshcentral.com\r\n');
- if ((obj.platform == 'win32') || (obj.platform == 'linux')) {
- console.log('Run as a background service');
- console.log(' --install/uninstall Install MeshCentral as a background service.');
- console.log(' --start/stop/restart Control MeshCentral background service.');
- console.log('');
- console.log('Run standalone, console application');
- }
- console.log(' --user [username] Always login as [username] if account exists.');
- console.log(' --port [number] Web server port number.');
- console.log(' --redirport [number] Creates an additional HTTP server to redirect users to the HTTPS server.');
- console.log(' --exactports Server must run with correct ports or exit.');
- console.log(' --noagentupdate Server will not update mesh agent native binaries.');
- console.log(' --nedbtodb Transfer all NeDB records into current database.');
- console.log(' --listuserids Show a list of a user identifiers in the database.');
- console.log(' --cert [name], (country), (org) Create a web server certificate with [name] server name.');
- console.log(' country and organization can optionally be set.');
- console.log('');
- console.log('Server recovery commands, use only when MeshCentral is offline.');
- console.log(' --createaccount [userid] Create a new user account.');
- console.log(' --resetaccount [userid] Unlock an account, disable 2FA and set a new account password.');
- console.log(' --adminaccount [userid] Promote account to site administrator.');
- return;
- }
-
- // Fix a NeDB database
- if (obj.args.dbfix) {
- var lines = null, badJsonCount = 0, fieldNames = [], fixedDb = [];
- try { lines = obj.fs.readFileSync(obj.getConfigFilePath(obj.args.dbfix), { encoding: 'utf8' }).split('\n'); } catch (ex) { console.log('Invalid file: ' + obj.args.dbfix + ': ' + ex); process.exit(); }
- for (var i = 0; i < lines.length; i++) {
- var x = null;
- try { x = JSON.parse(lines[i]); } catch (ex) { badJsonCount++; }
- if (x != null) { fixedDb.push(lines[i]); for (var j in x) { if (fieldNames.indexOf(j) == -1) { fieldNames.push(j); } } }
- }
- console.log('Lines: ' + lines.length + ', badJSON: ' + badJsonCount + ', Feilds: ' + fieldNames);
- obj.fs.writeFileSync(obj.getConfigFilePath(obj.args.dbfix) + '-fixed', fixedDb.join('\n'), { encoding: 'utf8' });
- return;
- }
-
- // Check for invalid cert name
- if ((obj.args.cert != null) && ((typeof obj.args.cert != "string") || (obj.args.cert.indexOf('@') >= 0) || (obj.args.cert.indexOf('/') >= 0) || (obj.args.cert.indexOf(':') >= 0))) { console.log("Invalid certificate name"); process.exit(); return; }
-
- // Perform a password hash
- if (obj.args.hashpassword) { require('./pass').hash(obj.args.hashpassword, function (err, salt, hash, tag) { console.log(salt + ',' + hash); process.exit(); }); return; }
-
- // Dump to mesh cores
- if (obj.args.dumpcores) { obj.updateMeshCore(function () { console.log('Done.'); }, true); return; }
-
- // Setup Telegram
- if (obj.args.setuptelegram) { require('./meshmessaging.js').SetupTelegram(obj); return; }
-
- // Perform web site translations into different languages
- if (obj.args.translate) {
- // Check NodeJS version
- const NodeJSVer = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
- if (NodeJSVer < 8) { console.log("Translation feature requires Node v8 or above, current version is " + process.version + "."); process.exit(); return; }
-
- // Check if translate.json is in the "meshcentral-data" folder, if so use that and translate default pages.
- var translationFile = null, customTranslation = false;
- if (require('fs').existsSync(obj.path.join(obj.datapath, 'translate.json'))) { translationFile = obj.path.join(obj.datapath, 'translate.json'); console.log("Using translate.json in meshcentral-data."); customTranslation = true; }
- if (translationFile == null) { if (require('fs').existsSync(obj.path.join(__dirname, 'translate', 'translate.json'))) { translationFile = obj.path.join(__dirname, 'translate', 'translate.json'); console.log("Using default translate.json."); } }
- if (translationFile == null) { console.log("Unable to find translate.json."); process.exit(); return; }
-
- // Perform translation operations
- var didSomething = false;
- process.chdir(obj.path.join(__dirname, 'translate'));
- const translateEngine = require('./translate/translate.js')
- if (customTranslation == true) {
- // Translate all of the default files using custom translation file
- translateEngine.startEx(['', '', 'minifyall']);
- translateEngine.startEx(['', '', 'translateall', translationFile]);
- translateEngine.startEx(['', '', 'extractall', translationFile]);
- didSomething = true;
- } else {
- // Translate all of the default files
- translateEngine.startEx(['', '', 'minifyall']);
- translateEngine.startEx(['', '', 'translateall']);
- translateEngine.startEx(['', '', 'extractall']);
- didSomething = true;
- }
-
- // Check if "meshcentral-web" exists, if so, translate all pages in that folder.
- if (obj.webViewsOverridePath != null) {
- didSomething = true;
- var files = obj.fs.readdirSync(obj.webViewsOverridePath);
- for (var i in files) {
- var file = obj.path.join(obj.webViewsOverridePath, files[i]);
- if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) {
- translateEngine.startEx(['', '', 'minify', file]);
- }
- }
- files = obj.fs.readdirSync(obj.webViewsOverridePath);
- for (var i in files) {
- var file = obj.path.join(obj.webViewsOverridePath, files[i]);
- if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) {
- translateEngine.startEx(['', '', 'translate', '*', translationFile, file, '--subdir:translations']);
- }
- }
- }
-
- // Check domains and see if "meshcentral-web-DOMAIN" exists, if so, translate all pages in that folder
- for (i in obj.config.domains) {
- if (i == "") continue;
- var path = obj.path.join(obj.datapath, '..', 'meshcentral-web-' + i, 'views');
- if (require('fs').existsSync(path)) {
- didSomething = true;
- var files = obj.fs.readdirSync(path);
- for (var a in files) {
- var file = obj.path.join(path, files[a]);
- if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) {
- translateEngine.startEx(['', '', 'minify', file]);
- }
- }
- files = obj.fs.readdirSync(path);
- for (var a in files) {
- var file = obj.path.join(path, files[a]);
- if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) {
- translateEngine.startEx(['', '', 'translate', '*', translationFile, file, '--subdir:translations']);
- }
- }
- }
- }
- /*
- if (obj.webPublicOverridePath != null) {
- didSomething = true;
- var files = obj.fs.readdirSync(obj.webPublicOverridePath);
- for (var i in files) {
- var file = obj.path.join(obj.webPublicOverridePath, files[i]);
- if (file.endsWith('.htm') && !file.endsWith('-min.htm')) {
- translateEngine.startEx(['', '', 'translate', '*', translationFile, file, '--subdir:translations']);
- }
- }
- }
- */
-
- if (didSomething == false) { console.log("Nothing to do."); }
- console.log('Finished Translating.')
- process.exit();
- return;
- }
-
- // Setup the Node+NPM path if possible, this makes it possible to update the server even if NodeJS and NPM are not in default paths.
- if (obj.args.npmpath == null) {
- try {
- var nodepath = process.argv[0];
- var npmpath = obj.path.join(obj.path.dirname(process.argv[0]), 'npm');
- if (obj.fs.existsSync(nodepath) && obj.fs.existsSync(npmpath)) {
- if (nodepath.indexOf(' ') >= 0) { nodepath = '"' + nodepath + '"'; }
- if (npmpath.indexOf(' ') >= 0) { npmpath = '"' + npmpath + '"'; }
- if (obj.platform == 'win32') { obj.args.npmpath = npmpath; } else { obj.args.npmpath = (nodepath + ' ' + npmpath); }
- }
- } catch (ex) { }
- }
-
- // Linux background service systemd handling
- if (obj.platform == 'linux') {
- if (obj.args.install == true) {
- // Install MeshCentral in Systemd
- console.log('Installing MeshCentral as background Service...');
- var systemdConf = null;
- const userinfo = require('os').userInfo();
- if (require('fs').existsSync('/etc/systemd/system')) { systemdConf = '/etc/systemd/system/meshcentral.service'; }
- else if (require('fs').existsSync('/lib/systemd/system')) { systemdConf = '/lib/systemd/system/meshcentral.service'; }
- else if (require('fs').existsSync('/usr/lib/systemd/system')) { systemdConf = '/usr/lib/systemd/system/meshcentral.service'; }
- else { console.log('Unable to find systemd configuration folder.'); process.exit(); return; }
- console.log('Writing config file...');
- require('child_process').exec('which node', {}, function (error, stdout, stderr) {
- if ((error != null) || (stdout.indexOf('\n') == -1)) { console.log('ERROR: Unable to get node location: ' + error); process.exit(); return; }
- const nodePath = stdout.substring(0, stdout.indexOf('\n'));
- const config = '[Unit]\nDescription=MeshCentral Server\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nType=simple\nLimitNOFILE=1000000\nExecStart=' + nodePath + ' ' + __dirname + '/meshcentral\nWorkingDirectory=' + userinfo.homedir + '\nEnvironment=NODE_ENV=production\nUser=' + userinfo.username + '\nGroup=' + userinfo.username + '\nRestart=always\n# Restart service after 10 seconds if node service crashes\nRestartSec=10\n# Set port permissions capability\nAmbientCapabilities=cap_net_bind_service\n\n[Install]\nWantedBy=multi-user.target\n';
- require('child_process').exec('echo \"' + config + '\" | sudo tee ' + systemdConf, {}, function (error, stdout, stderr) {
- if ((error != null) && (error != '')) { console.log('ERROR: Unable to write config file: ' + error); process.exit(); return; }
- console.log('Enabling service...');
- require('child_process').exec('sudo systemctl enable meshcentral.service', {}, function (error, stdout, stderr) {
- if ((error != null) && (error != '')) { console.log('ERROR: Unable to enable MeshCentral as a service: ' + error); process.exit(); return; }
- if (stdout.length > 0) { console.log(stdout); }
- console.log('Starting service...');
- require('child_process').exec('sudo systemctl start meshcentral.service', {}, function (error, stdout, stderr) {
- if ((error != null) && (error != '')) { console.log('ERROR: Unable to start MeshCentral as a service: ' + error); process.exit(); return; }
- if (stdout.length > 0) { console.log(stdout); }
- console.log('Done.');
- });
- });
- });
- });
- return;
- } else if (obj.args.uninstall == true) {
- // Uninstall MeshCentral in Systemd
- console.log('Uninstalling MeshCentral background service...');
- var systemdConf = null;
- if (require('fs').existsSync('/etc/systemd/system')) { systemdConf = '/etc/systemd/system/meshcentral.service'; }
- else if (require('fs').existsSync('/lib/systemd/system')) { systemdConf = '/lib/systemd/system/meshcentral.service'; }
- else if (require('fs').existsSync('/usr/lib/systemd/system')) { systemdConf = '/usr/lib/systemd/system/meshcentral.service'; }
- else { console.log('Unable to find systemd configuration folder.'); process.exit(); return; }
- console.log('Stopping service...');
- require('child_process').exec('sudo systemctl stop meshcentral.service', {}, function (err, stdout, stderr) {
- if ((err != null) && (err != '')) { console.log('ERROR: Unable to stop MeshCentral as a service: ' + err); }
- if (stdout.length > 0) { console.log(stdout); }
- console.log('Disabling service...');
- require('child_process').exec('sudo systemctl disable meshcentral.service', {}, function (err, stdout, stderr) {
- if ((err != null) && (err != '')) { console.log('ERROR: Unable to disable MeshCentral as a service: ' + err); }
- if (stdout.length > 0) { console.log(stdout); }
- console.log('Removing config file...');
- require('child_process').exec('sudo rm ' + systemdConf, {}, function (err, stdout, stderr) {
- if ((err != null) && (err != '')) { console.log('ERROR: Unable to delete MeshCentral config file: ' + err); }
- console.log('Done.');
- });
- });
- });
- return;
- } else if (obj.args.start == true) {
- // Start MeshCentral in Systemd
- require('child_process').exec('sudo systemctl start meshcentral.service', {}, function (err, stdout, stderr) {
- if ((err != null) && (err != '')) { console.log('ERROR: Unable to start MeshCentral: ' + err); process.exit(); return; }
- console.log('Done.');
- });
- return;
- } else if (obj.args.stop == true) {
- // Stop MeshCentral in Systemd
- require('child_process').exec('sudo systemctl stop meshcentral.service', {}, function (err, stdout, stderr) {
- if ((err != null) && (err != '')) { console.log('ERROR: Unable to stop MeshCentral: ' + err); process.exit(); return; }
- console.log('Done.');
- });
- return;
- } else if (obj.args.restart == true) {
- // Restart MeshCentral in Systemd
- require('child_process').exec('sudo systemctl restart meshcentral.service', {}, function (err, stdout, stderr) {
- if ((err != null) && (err != '')) { console.log('ERROR: Unable to restart MeshCentral: ' + err); process.exit(); return; }
- console.log('Done.');
- });
- return;
- }
- }
-
- // FreeBSD background service handling, MUST USE SPAWN FOR SERVICE COMMANDS!
- if (obj.platform == 'freebsd') {
- if (obj.args.install == true) {
- // Install MeshCentral in rc.d
- console.log('Installing MeshCentral as background Service...');
- var systemdConf = "/usr/local/etc/rc.d/meshcentral";
- const userinfo = require('os').userInfo();
- console.log('Writing config file...');
- require('child_process').exec('which node', {}, function (error, stdout, stderr) {
- if ((error != null) || (stdout.indexOf('\n') == -1)) { console.log('ERROR: Unable to get node location: ' + error); process.exit(); return; }
- const nodePath = stdout.substring(0, stdout.indexOf('\n'));
- const config = '#!/bin/sh\n# MeshCentral FreeBSD Service Script\n# PROVIDE: meshcentral\n# REQUIRE: NETWORKING\n# KEYWORD: shutdown\n. /etc/rc.subr\nname=meshcentral\nuser=' + userinfo.username + '\nrcvar=meshcentral_enable\n: \\${meshcentral_enable:=\\"NO\\"}\n: \\${meshcentral_args:=\\"\\"}\npidfile=/var/run/meshcentral/meshcentral.pid\ncommand=\\"/usr/sbin/daemon\\"\nmeshcentral_chdir=\\"' + obj.parentpath + '\\"\ncommand_args=\\"-r -u \\${user} -P \\${pidfile} -S -T meshcentral -m 3 ' + nodePath + ' ' + __dirname + ' \\${meshcentral_args}\\"\nload_rc_config \\$name\nrun_rc_command \\"\\$1\\"\n';
- require('child_process').exec('echo \"' + config + '\" | tee ' + systemdConf + ' && chmod +x ' + systemdConf, {}, function (error, stdout, stderr) {
- if ((error != null) && (error != '')) { console.log('ERROR: Unable to write config file: ' + error); process.exit(); return; }
- console.log('Enabling service...');
- require('child_process').exec('sysrc meshcentral_enable="YES"', {}, function (error, stdout, stderr) {
- if ((error != null) && (error != '')) { console.log('ERROR: Unable to enable MeshCentral as a service: ' + error); process.exit(); return; }
- if (stdout.length > 0) { console.log(stdout); }
- console.log('Starting service...');
- const service = require('child_process').spawn('service', ['meshcentral', 'start']);
- service.stdout.on('data', function (data) { console.log(data.toString()); });
- service.stderr.on('data', function (data) { console.log(data.toString()); });
- service.on('exit', function (code) {
- console.log((code === 0) ? 'Done.' : 'ERROR: Unable to start MeshCentral as a service');
- process.exit(); // Must exit otherwise we just hang
- });
- });
- });
- });
- return;
- } else if (obj.args.uninstall == true) {
- // Uninstall MeshCentral in rc.d
- console.log('Uninstalling MeshCentral background service...');
- var systemdConf = "/usr/local/etc/rc.d/meshcentral";
- console.log('Stopping service...');
- const service = require('child_process').spawn('service', ['meshcentral', 'stop']);
- service.stdout.on('data', function (data) { console.log(data.toString()); });
- service.stderr.on('data', function (data) { console.log(data.toString()); });
- service.on('exit', function (code) {
- if (code !== 0) { console.log('ERROR: Unable to stop MeshCentral as a service'); }
- console.log('Disabling service...');
- require('child_process').exec('sysrc -x meshcentral_enable', {}, function (err, stdout, stderr) {
- if ((err != null) && (err != '')) { console.log('ERROR: Unable to disable MeshCentral as a service: ' + err); }
- if (stdout.length > 0) { console.log(stdout); }
- console.log('Removing config file...');
- require('child_process').exec('rm ' + systemdConf, {}, function (err, stdout, stderr) {
- if ((err != null) && (err != '')) { console.log('ERROR: Unable to delete MeshCentral config file: ' + err); }
- console.log('Done.');
- process.exit(); // Must exit otherwise we just hang
- });
- });
- });
- return;
- } else if (obj.args.start == true) {
- // Start MeshCentral in rc.d
- const service = require('child_process').spawn('service', ['meshcentral', 'start']);
- service.stdout.on('data', function (data) { console.log(data.toString()); });
- service.stderr.on('data', function (data) { console.log(data.toString()); });
- service.on('exit', function (code) {
- console.log((code === 0) ? 'Done.' : 'ERROR: Unable to start MeshCentral as a service: ' + error);
- process.exit(); // Must exit otherwise we just hang
- });
- return;
- } else if (obj.args.stop == true) {
- // Stop MeshCentral in rc.d
- const service = require('child_process').spawn('service', ['meshcentral', 'stop']);
- service.stdout.on('data', function (data) { console.log(data.toString()); });
- service.stderr.on('data', function (data) { console.log(data.toString()); });
- service.on('exit', function (code) {
- console.log((code === 0) ? 'Done.' : 'ERROR: Unable to stop MeshCentral as a service: ' + error);
- process.exit(); // Must exit otherwise we just hang
- });
- return;
- } else if (obj.args.restart == true) {
- // Restart MeshCentral in rc.d
- const service = require('child_process').spawn('service', ['meshcentral', 'restart']);
- service.stdout.on('data', function (data) { console.log(data.toString()); });
- service.stderr.on('data', function (data) { console.log(data.toString()); });
- service.on('exit', function (code) {
- console.log((code === 0) ? 'Done.' : 'ERROR: Unable to restart MeshCentral as a service: ' + error);
- process.exit(); // Must exit otherwise we just hang
- });
- return;
- }
- }
-
- // Index a recorded file
- if (obj.args.indexmcrec != null) {
- if (typeof obj.args.indexmcrec != 'string') {
- console.log('Usage: --indexmrec [filename.mcrec]');
- } else if (obj.fs.existsSync(obj.args.indexmcrec)) {
- console.log('Indexing file: ' + obj.args.indexmcrec);
- require(require('path').join(__dirname, 'mcrec.js')).indexFile(obj.args.indexmcrec);
- } else {
- console.log('Unable to find file: ' + obj.args.indexmcrec);
- }
- return;
- }
-
- // Windows background service handling
- if ((obj.platform == 'win32') && (obj.service != null)) {
- // Build MeshCentral parent path and Windows Service path
- var mcpath = __dirname;
- if (mcpath.endsWith('\\node_modules\\meshcentral') || mcpath.endsWith('/node_modules/meshcentral')) { mcpath = require('path').join(mcpath, '..', '..'); }
- const servicepath = obj.path.join(mcpath, 'WinService');
-
- // Check if we need to install, start, stop, remove ourself as a background service
- if (((obj.args.xinstall == true) || (obj.args.xuninstall == true) || (obj.args.start == true) || (obj.args.stop == true) || (obj.args.restart == true))) {
- var env = [], xenv = ['user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'exactport', 'rediraliasport', 'debug'];
- for (i in xenv) { if (obj.args[xenv[i]] != null) { env.push({ name: 'mesh' + xenv[i], value: obj.args[xenv[i]] }); } } // Set some args as service environment variables.
-
- var serviceFilePath = null;
- if (obj.fs.existsSync(obj.path.join(servicepath, 'winservice.js'))) { serviceFilePath = obj.path.join(servicepath, 'winservice.js'); }
- else if (obj.fs.existsSync(obj.path.join(__dirname, '../WinService/winservice.js'))) { serviceFilePath = obj.path.join(__dirname, '../WinService/winservice.js'); }
- else if (obj.fs.existsSync(obj.path.join(__dirname, 'winservice.js'))) { serviceFilePath = obj.path.join(__dirname, 'winservice.js'); }
- if (serviceFilePath == null) { console.log('Unable to find winservice.js'); return; }
-
- const svc = new obj.service({ name: 'MeshCentral', description: 'MeshCentral Remote Management Server', script: servicepath, env: env, wait: 2, grow: 0.5 });
- svc.on('install', function () { console.log('MeshCentral service installed.'); svc.start(); });
- svc.on('uninstall', function () { console.log('MeshCentral service uninstalled.'); process.exit(); });
- svc.on('start', function () { console.log('MeshCentral service started.'); process.exit(); });
- svc.on('stop', function () { console.log('MeshCentral service stopped.'); if (obj.args.stop) { process.exit(); } if (obj.args.restart) { console.log('Holding 5 seconds...'); setTimeout(function () { svc.start(); }, 5000); } });
- svc.on('alreadyinstalled', function () { console.log('MeshCentral service already installed.'); process.exit(); });
- svc.on('invalidinstallation', function () { console.log('Invalid MeshCentral service installation.'); process.exit(); });
-
- if (obj.args.xinstall == true) { try { svc.install(); } catch (ex) { logException(ex); } }
- if (obj.args.stop == true || obj.args.restart == true) { try { svc.stop(); } catch (ex) { logException(ex); } }
- if (obj.args.start == true) { try { svc.start(); } catch (ex) { logException(ex); } }
- if (obj.args.xuninstall == true) { try { svc.uninstall(); } catch (ex) { logException(ex); } }
- return;
- }
-
- // Windows service install using the external winservice.js
- if (obj.args.install == true) {
- console.log('Installing MeshCentral as Windows Service...');
- if (obj.fs.existsSync(servicepath) == false) { try { obj.fs.mkdirSync(servicepath); } catch (ex) { console.log('ERROR: Unable to create WinService folder: ' + ex); process.exit(); return; } }
- try { obj.fs.createReadStream(obj.path.join(__dirname, 'winservice.js')).pipe(obj.fs.createWriteStream(obj.path.join(servicepath, 'winservice.js'))); } catch (ex) { console.log('ERROR: Unable to copy winservice.js: ' + ex); process.exit(); return; }
- require('child_process').exec('node winservice.js --install', { maxBuffer: 512000, timeout: 120000, cwd: servicepath }, function (error, stdout, stderr) {
- if ((error != null) && (error != '')) { console.log('ERROR: Unable to install MeshCentral as a service: ' + error); process.exit(); return; }
- console.log(stdout);
- });
- return;
- } else if (obj.args.uninstall == true) {
- console.log('Uninstalling MeshCentral Windows Service...');
- if (obj.fs.existsSync(servicepath) == true) {
- require('child_process').exec('node winservice.js --uninstall', { maxBuffer: 512000, timeout: 120000, cwd: servicepath }, function (error, stdout, stderr) {
- if ((error != null) && (error != '')) { console.log('ERROR: Unable to uninstall MeshCentral service: ' + error); process.exit(); return; }
- console.log(stdout);
- try { obj.fs.unlinkSync(obj.path.join(servicepath, 'winservice.js')); } catch (ex) { }
- try { obj.fs.rmdirSync(servicepath); } catch (ex) { }
- });
- } else if (obj.fs.existsSync(obj.path.join(__dirname, '../WinService')) == true) {
- require('child_process').exec('node winservice.js --uninstall', { maxBuffer: 512000, timeout: 120000, cwd: obj.path.join(__dirname, '../WinService') }, function (error, stdout, stderr) {
- if ((error != null) && (error != '')) { console.log('ERROR: Unable to uninstall MeshCentral service: ' + error); process.exit(); return; }
- console.log(stdout);
- try { obj.fs.unlinkSync(obj.path.join(__dirname, '../WinService/winservice.js')); } catch (ex) { }
- try { obj.fs.rmdirSync(obj.path.join(__dirname, '../WinService')); } catch (ex) { }
- });
- } else {
- require('child_process').exec('node winservice.js --uninstall', { maxBuffer: 512000, timeout: 120000, cwd: __dirname }, function (error, stdout, stderr) {
- if ((error != null) && (error != '')) { console.log('ERROR: Unable to uninstall MeshCentral service: ' + error); process.exit(); return; }
- console.log(stdout);
- });
- }
- return;
- }
- }
-
- // If "--launch" is in the arguments, launch now
- if (obj.args.launch) {
- if (obj.args.vault) { obj.StartVault(); } else { obj.StartEx(); }
- } else {
- // if "--launch" is not specified, launch the server as a child process.
- const startArgs = [];
- for (i in process.argv) {
- if (i > 0) {
- const arg = process.argv[i];
- if ((arg.length > 0) && ((arg.indexOf(' ') >= 0) || (arg.indexOf('&') >= 0))) { startArgs.push(arg); } else { startArgs.push(arg); }
- }
- }
- startArgs.push('--launch', process.pid);
- obj.launchChildServer(startArgs);
- }
- };
-
- // Launch MeshCentral as a child server and monitor it.
- obj.launchChildServer = function (startArgs) {
- const child_process = require('child_process');
- const isInspectorAttached = (()=> { try { return require('node:inspector').url() !== undefined; } catch (_) { return false; } }).call();
- const logFromChildProcess = isInspectorAttached ? () => {} : console.log.bind(console);
- try { if (process.traceDeprecation === true) { startArgs.unshift('--trace-deprecation'); } } catch (ex) { }
- try { if (process.traceProcessWarnings === true) { startArgs.unshift('--trace-warnings'); } } catch (ex) { }
- if (startArgs[0] != "--disable-proto=delete") startArgs.unshift("--disable-proto=delete")
- childProcess = child_process.execFile(process.argv[0], startArgs, { maxBuffer: Infinity, cwd: obj.parentpath }, function (error, stdout, stderr) {
- if (childProcess.xrestart == 1) {
- setTimeout(function () { obj.launchChildServer(startArgs); }, 500); // This is an expected restart.
- } else if (childProcess.xrestart == 2) {
- console.log('Expected exit...');
- process.exit(); // User CTRL-C exit.
- } else if (childProcess.xrestart == 3) {
- // Server self-update exit
- var version = '';
- if (typeof obj.args.selfupdate == 'string') { version = '@' + obj.args.selfupdate; }
- else if (typeof obj.args.specificupdate == 'string') { version = '@' + obj.args.specificupdate; delete obj.args.specificupdate; }
- const child_process = require('child_process');
- const npmpath = ((typeof obj.args.npmpath == 'string') ? obj.args.npmpath : 'npm');
- const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
- const env = Object.assign({}, process.env); // Shallow clone
- if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
- // always use --save-exact - https://stackoverflow.com/a/64507176/1210734
- const xxprocess = child_process.exec(npmpath + ' install --save-exact --no-audit meshcentral' + version + npmproxy, { maxBuffer: Infinity, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) {
- if ((error != null) && (error != '')) { console.log('Update failed: ' + error); }
- });
- xxprocess.data = '';
- xxprocess.stdout.on('data', function (data) { xxprocess.data += data; });
- xxprocess.stderr.on('data', function (data) { xxprocess.data += data; });
- xxprocess.on('close', function (code) {
- if (code == 0) { console.log('Update completed...'); }
-
- // Run the server updated script if present
- if (typeof obj.config.settings.runonserverupdated == 'string') {
- const child_process = require('child_process');
- var parentpath = __dirname;
- if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) { parentpath = require('path').join(__dirname, '../..'); }
- child_process.exec(obj.config.settings.runonserverupdated + ' ' + getCurrentVersion(), { maxBuffer: 512000, timeout: 120000, cwd: parentpath }, function (error, stdout, stderr) { });
- }
-
- if (obj.args.cleannpmcacheonupdate === true) {
- // Perform NPM cache clean
- console.log('Cleaning NPM cache...');
- const xxxprocess = child_process.exec(npmpath + ' cache clean --force', { maxBuffer: Infinity, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { });
- xxxprocess.on('close', function (code) { setTimeout(function () { obj.launchChildServer(startArgs); }, 1000); });
- } else {
- // Run the updated server
- setTimeout(function () { obj.launchChildServer(startArgs); }, 1000);
- }
- });
- } else {
- if (error != null) {
- // This is an un-expected restart
- console.log(error);
- console.log('ERROR: MeshCentral failed with critical error, check mesherrors.txt. Restarting in 5 seconds...');
- setTimeout(function () { obj.launchChildServer(startArgs); }, 5000);
-
- // Run the server error script if present
- if (typeof obj.config.settings.runonservererror == 'string') {
- const child_process = require('child_process');
- var parentpath = __dirname;
- if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) { parentpath = require('path').join(__dirname, '../..'); }
- child_process.exec(obj.config.settings.runonservererror + ' ' + getCurrentVersion(), { maxBuffer: 512000, timeout: 120000, cwd: parentpath }, function (error, stdout, stderr) { });
- }
- }
- }
- });
- childProcess.stdout.on('data', function (data) {
- if (data[data.length - 1] == '\n') { data = data.substring(0, data.length - 1); }
- if (data.indexOf('Updating settings folder...') >= 0) { childProcess.xrestart = 1; }
- else if (data.indexOf('Updating server certificates...') >= 0) { childProcess.xrestart = 1; }
- else if (data.indexOf('Server Ctrl-C exit...') >= 0) { childProcess.xrestart = 2; }
- else if (data.indexOf('Starting self upgrade...') >= 0) { childProcess.xrestart = 3; }
- else if (data.indexOf('Server restart...') >= 0) { childProcess.xrestart = 1; }
- else if (data.indexOf('Starting self upgrade to: ') >= 0) { obj.args.specificupdate = data.substring(26).split('\r')[0].split('\n')[0]; childProcess.xrestart = 3; }
- var datastr = data;
- while (datastr.endsWith('\r') || datastr.endsWith('\n')) { datastr = datastr.substring(0, datastr.length - 1); }
- logFromChildProcess(datastr);
- });
- childProcess.stderr.on('data', function (data) {
- var datastr = data;
- while (datastr.endsWith('\r') || datastr.endsWith('\n')) { datastr = datastr.substring(0, datastr.length - 1); }
- logFromChildProcess('ERR: ' + datastr);
- if (data.startsWith('le.challenges[tls-sni-01].loopback')) { return; } // Ignore this error output from GreenLock
- if (data[data.length - 1] == '\n') { data = data.substring(0, data.length - 1); }
- obj.logError(data);
- });
- childProcess.on('close', function (code) { if ((code != 0) && (code != 123)) { /* console.log("Exited with code " + code); */ } });
- };
-
- obj.logError = function (err) {
- try {
- var errlogpath = null;
- if (typeof obj.args.mesherrorlogpath == 'string') { errlogpath = obj.path.join(obj.args.mesherrorlogpath, 'mesherrors.txt'); } else { errlogpath = obj.getConfigFilePath('mesherrors.txt'); }
- obj.fs.appendFileSync(errlogpath, '-------- ' + new Date().toLocaleString() + ' ---- ' + getCurrentVersion() + ' --------\r\n\r\n' + err + '\r\n\r\n\r\n');
- } catch (ex) { console.log('ERROR: Unable to write to mesherrors.txt.'); }
- };
-
- // Get current and latest MeshCentral server versions using NPM
- obj.getLatestServerVersion = function (callback) {
- if (callback == null) return;
- try {
- if (typeof obj.args.selfupdate == 'string') { callback(getCurrentVersion(), obj.args.selfupdate); return; } // If we are targetting a specific version, return that one as current.
- const child_process = require('child_process');
- const npmpath = ((typeof obj.args.npmpath == 'string') ? obj.args.npmpath : 'npm');
- const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
- const env = Object.assign({}, process.env); // Shallow clone
- if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
- const xxprocess = child_process.exec(npmpath + npmproxy + ' view meshcentral dist-tags.latest', { maxBuffer: 512000, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { });
- xxprocess.data = '';
- xxprocess.stdout.on('data', function (data) { xxprocess.data += data; });
- xxprocess.stderr.on('data', function (data) { });
- xxprocess.on('close', function (code) {
- var latestVer = null;
- if (code == 0) { try { latestVer = xxprocess.data.split(' ').join('').split('\r').join('').split('\n').join(''); } catch (ex) { } }
- callback(getCurrentVersion(), latestVer);
- });
- } catch (ex) { callback(getCurrentVersion(), null, ex); } // If the system is running out of memory, an exception here can easily happen.
- };
-
- // Get current version and all MeshCentral server tags using NPM
- obj.getServerTags = function (callback) {
- if (callback == null) return;
- try {
- if (typeof obj.args.selfupdate == 'string') { callback({ current: getCurrentVersion(), latest: obj.args.selfupdate }); return; } // If we are targetting a specific version, return that one as current.
- const child_process = require('child_process');
- const npmpath = ((typeof obj.args.npmpath == 'string') ? obj.args.npmpath : 'npm');
- const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
- const env = Object.assign({}, process.env); // Shallow clone
- if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
- const xxprocess = child_process.exec(npmpath + npmproxy + ' dist-tag ls meshcentral', { maxBuffer: 512000, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { });
- xxprocess.data = '';
- xxprocess.stdout.on('data', function (data) { xxprocess.data += data; });
- xxprocess.stderr.on('data', function (data) { });
- xxprocess.on('close', function (code) {
- var tags = { current: getCurrentVersion() };
- if (code == 0) {
- try {
- var lines = xxprocess.data.split('\r\n').join('\n').split('\n');
- for (var i in lines) { var s = lines[i].split(': '); if ((s.length == 2) && (obj.args.npmtag == null) || (obj.args.npmtag == s[0])) { tags[s[0]] = s[1]; } }
- } catch (ex) { }
- }
- callback(tags);
- });
- } catch (ex) { callback({ current: getCurrentVersion() }, ex); } // If the system is running out of memory, an exception here can easily happen.
- };
-
- // Use NPM to get list of versions
- obj.getServerVersions = function (callback) {
- try {
- const child_process = require('child_process');
- const npmpath = ((typeof obj.args.npmpath == 'string') ? obj.args.npmpath : 'npm');
- const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
- const env = Object.assign({}, process.env); // Shallow clone
- if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
- const xxprocess = child_process.exec(npmpath + npmproxy + ' view meshcentral versions --json', { maxBuffer: 512000, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { });
- xxprocess.data = '';
- xxprocess.stdout.on('data', function (data) { xxprocess.data += data; });
- xxprocess.stderr.on('data', function (data) { });
- xxprocess.on('close', function (code) {
- (code == 0) ? callback(xxprocess.data) : callback('{}');
- });
- } catch (ex) { callback('{}'); }
- };
-
- // Initiate server self-update
- obj.performServerUpdate = function (version) {
- if (obj.serverSelfWriteAllowed != true) return false;
- if ((version == null) || (version == '') || (typeof version != 'string')) { console.log('Starting self upgrade...'); } else { console.log('Starting self upgrade to: ' + version); }
- process.exit(200);
- return true;
- };
-
- // Initiate server self-update
- obj.performServerCertUpdate = function () { console.log('Updating server certificates...'); process.exit(200); };
-
- // Start by loading configuration from Vault
- obj.StartVault = function () {
- // Check that the configuration can only be loaded from one place
- if ((obj.args.vault != null) && (obj.args.loadconfigfromdb != null)) { console.log("Can't load configuration from both database and Vault."); process.exit(); return; }
-
- // Fix arguments if needed
- if (typeof obj.args.vault == 'string') {
- obj.args.vault = { endpoint: obj.args.vault };
- if (typeof obj.args.token == 'string') { obj.args.vault.token = obj.args.token; }
- if (typeof obj.args.unsealkey == 'string') { obj.args.vault.unsealkey = obj.args.unsealkey; }
- if (typeof obj.args.name == 'string') { obj.args.vault.name = obj.args.name; }
- }
-
- // Load configuration for HashiCorp's Vault if needed
- if (obj.args.vault) {
- if (obj.args.vault.endpoint == null) { console.log('Missing Vault endpoint.'); process.exit(); return; }
- if (obj.args.vault.token == null) { console.log('Missing Vault token.'); process.exit(); return; }
- if (obj.args.vault.unsealkey == null) { console.log('Missing Vault unsealkey.'); process.exit(); return; }
- if (obj.args.vault.name == null) { obj.args.vault.name = 'meshcentral'; }
-
- // Get new instance of the client
- const vault = require("node-vault")({ endpoint: obj.args.vault.endpoint, token: obj.args.vault.token });
- vault.unseal({ key: obj.args.vault.unsealkey })
- .then(function () {
- if (obj.args.vaultdeleteconfigfiles) {
- vault.delete('secret/data/' + obj.args.vault.name)
- .then(function (r) { console.log('Done.'); process.exit(); })
- .catch(function (x) { console.log(x); process.exit(); });
- } else if (obj.args.vaultpushconfigfiles) {
- // Push configuration files into Vault
- if ((obj.args.vaultpushconfigfiles == '*') || (obj.args.vaultpushconfigfiles === true)) { obj.args.vaultpushconfigfiles = obj.datapath; }
- obj.fs.readdir(obj.args.vaultpushconfigfiles, function (err, files) {
- if (err != null) { console.log('ERROR: Unable to read from folder ' + obj.args.vaultpushconfigfiles); process.exit(); return; }
- var configFound = false;
- for (var i in files) { if (files[i] == 'config.json') { configFound = true; } }
- if (configFound == false) { console.log('ERROR: No config.json in folder ' + obj.args.vaultpushconfigfiles); process.exit(); return; }
- var configFiles = {};
- for (var i in files) {
- const file = files[i];
- if ((file == 'config.json') || file.endsWith('.key') || file.endsWith('.crt') || (file == 'terms.txt') || file.endsWith('.jpg') || file.endsWith('.png')) {
- const path = obj.path.join(obj.args.vaultpushconfigfiles, files[i]), binary = Buffer.from(obj.fs.readFileSync(path, { encoding: 'binary' }), 'binary');
- console.log('Pushing ' + file + ', ' + binary.length + ' bytes.');
- if (file.endsWith('.json') || file.endsWith('.key') || file.endsWith('.crt')) { configFiles[file] = binary.toString(); } else { configFiles[file] = binary.toString('base64'); }
- }
- }
- vault.write('secret/data/' + obj.args.vault.name, { "data": configFiles })
- .then(function (r) { console.log('Done.'); process.exit(); })
- .catch(function (x) { console.log(x); process.exit(); });
- });
- } else {
- // Read configuration files from Vault
- vault.read('secret/data/' + obj.args.vault.name)
- .then(function (r) {
- if ((r == null) || (r.data == null) || (r.data.data == null)) { console.log('Unable to read configuration from Vault.'); process.exit(); return; }
- var configFiles = obj.configurationFiles = r.data.data;
-
- // Decode Base64 when needed
- for (var file in configFiles) { if (!file.endsWith('.json') && !file.endsWith('.key') && !file.endsWith('.crt')) { configFiles[file] = Buffer.from(configFiles[file], 'base64'); } }
-
- // Save all of the files
- if (obj.args.vaultpullconfigfiles) {
- for (var i in configFiles) {
- var fullFileName = obj.path.join(obj.args.vaultpullconfigfiles, i);
- try { obj.fs.writeFileSync(fullFileName, configFiles[i]); } catch (ex) { console.log('Unable to write to ' + fullFileName); process.exit(); return; }
- console.log('Pulling ' + i + ', ' + configFiles[i].length + ' bytes.');
- }
- console.log('Done.');
- process.exit();
- }
-
- // Parse the new configuration file
- var config2 = null;
- try { config2 = JSON.parse(configFiles['config.json']); } catch (ex) { console.log('Error, unable to parse config.json from Vault.'); process.exit(); return; }
-
- // Set the command line arguments to the config file if they are not present
- if (!config2.settings) { config2.settings = {}; }
- for (var i in args) { config2.settings[i] = args[i]; }
- obj.args = args = config2.settings;
-
- // Lower case all keys in the config file
- obj.common.objKeysToLower(config2, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'telegram/proxy']);
-
- // Grad some of the values from the original config.json file if present.
- if ((config.settings.vault != null) && (config2.settings != null)) { config2.settings.vault = config.settings.vault; }
-
- // We got a new config.json from the database, let's use it.
- config = obj.config = config2;
- obj.StartEx();
- })
- .catch(function (x) { console.log(x); process.exit(); });
- }
- }).catch(function (x) { console.log(x); process.exit(); });
- return;
- }
- }
-
- // Look for easy command line instructions and do them here.
- obj.StartEx = async function () {
- var i;
- //var wincmd = require('node-windows');
- //wincmd.list(function (svc) { console.log(svc); }, true);
-
- // Setup syslog support. Not supported on Windows.
- if ((require('os').platform() != 'win32') && ((config.settings.syslog != null) || (config.settings.syslogjson != null) || (config.settings.syslogauth != null))) {
- if (config.settings.syslog === true) { config.settings.syslog = 'meshcentral'; }
- if (config.settings.syslogjson === true) { config.settings.syslogjson = 'meshcentral-json'; }
- if (config.settings.syslogauth === true) { config.settings.syslogauth = 'meshcentral-auth'; }
- if (typeof config.settings.syslog == 'string') {
- obj.syslog = require('modern-syslog');
- console.log('Starting ' + config.settings.syslog + ' syslog.');
- obj.syslog.init(config.settings.syslog, obj.syslog.LOG_PID | obj.syslog.LOG_ODELAY, obj.syslog.LOG_LOCAL0);
- obj.syslog.log(obj.syslog.LOG_INFO, "MeshCentral v" + getCurrentVersion() + " Server Start");
- }
- if (typeof config.settings.syslogjson == 'string') {
- obj.syslogjson = require('modern-syslog');
- console.log('Starting ' + config.settings.syslogjson + ' JSON syslog.');
- obj.syslogjson.init(config.settings.syslogjson, obj.syslogjson.LOG_PID | obj.syslogjson.LOG_ODELAY, obj.syslogjson.LOG_LOCAL0);
- obj.syslogjson.log(obj.syslogjson.LOG_INFO, "MeshCentral v" + getCurrentVersion() + " Server Start");
- }
- if (typeof config.settings.syslogauth == 'string') {
- obj.syslogauth = require('modern-syslog');
- console.log('Starting ' + config.settings.syslogauth + ' auth syslog.');
- obj.syslogauth.init(config.settings.syslogauth, obj.syslogauth.LOG_PID | obj.syslogauth.LOG_ODELAY, obj.syslogauth.LOG_LOCAL0);
- obj.syslogauth.log(obj.syslogauth.LOG_INFO, "MeshCentral v" + getCurrentVersion() + " Server Start");
- }
- }
- // Setup TCP syslog support, this works on all OS's.
- if (config.settings.syslogtcp != null) {
- const syslog = require('syslog');
- if (config.settings.syslogtcp === true) {
- obj.syslogtcp = syslog.createClient(514, 'localhost');
- } else {
- const sp = config.settings.syslogtcp.split(':');
- obj.syslogtcp = syslog.createClient(parseInt(sp[1]), sp[0]);
- }
- obj.syslogtcp.log("MeshCentral v" + getCurrentVersion() + " Server Start", obj.syslogtcp.LOG_INFO);
- }
-
- // Check top level configuration for any unrecognized values
- if (config) { for (var i in config) { if ((typeof i == 'string') && (i.length > 0) && (i[0] != '_') && (['settings', 'domaindefaults', 'domains', 'configfiles', 'smtp', 'letsencrypt', 'peers', 'sms', 'messaging', 'sendgrid', 'sendmail', 'firebase', 'firebaserelay', '$schema'].indexOf(i) == -1)) { addServerWarning('Unrecognized configuration option \"' + i + '\".', 3, [i]); } } }
-
- // Read IP lists from files if applicable
- config.settings.userallowedip = obj.args.userallowedip = readIpListFromFile(obj.args.userallowedip);
- config.settings.userblockedip = obj.args.userblockedip = readIpListFromFile(obj.args.userblockedip);
- config.settings.agentallowedip = obj.args.agentallowedip = readIpListFromFile(obj.args.agentallowedip);
- config.settings.agentblockedip = obj.args.agentblockedip = readIpListFromFile(obj.args.agentblockedip);
- config.settings.swarmallowedip = obj.args.swarmallowedip = readIpListFromFile(obj.args.swarmallowedip);
-
- // Check IP lists and ranges
- if (typeof obj.args.userallowedip == 'string') { if (obj.args.userallowedip == '') { config.settings.userallowedip = obj.args.userallowedip = null; } else { config.settings.userallowedip = obj.args.userallowedip = obj.args.userallowedip.split(' ').join('').split(','); } }
- if (typeof obj.args.userblockedip == 'string') { if (obj.args.userblockedip == '') { config.settings.userblockedip = obj.args.userblockedip = null; } else { config.settings.userblockedip = obj.args.userblockedip = obj.args.userblockedip.split(' ').join('').split(','); } }
- if (typeof obj.args.agentallowedip == 'string') { if (obj.args.agentallowedip == '') { config.settings.agentallowedip = obj.args.agentallowedip = null; } else { config.settings.agentallowedip = obj.args.agentallowedip = obj.args.agentallowedip.split(' ').join('').split(','); } }
- if (typeof obj.args.agentblockedip == 'string') { if (obj.args.agentblockedip == '') { config.settings.agentblockedip = obj.args.agentblockedip = null; } else { config.settings.agentblockedip = obj.args.agentblockedip = obj.args.agentblockedip.split(' ').join('').split(','); } }
- if (typeof obj.args.swarmallowedip == 'string') { if (obj.args.swarmallowedip == '') { obj.args.swarmallowedip = null; } else { obj.args.swarmallowedip = obj.args.swarmallowedip.split(' ').join('').split(','); } }
- if ((typeof obj.args.agentupdateblocksize == 'number') && (obj.args.agentupdateblocksize >= 1024) && (obj.args.agentupdateblocksize <= 65531)) { obj.agentUpdateBlockSize = obj.args.agentupdateblocksize; }
- if (typeof obj.args.trustedproxy == 'string') { obj.args.trustedproxy = obj.args.trustedproxy.split(' ').join('').split(','); }
- if (typeof obj.args.tlsoffload == 'string') { obj.args.tlsoffload = obj.args.tlsoffload.split(' ').join('').split(','); }
-
- // Check IP lists and ranges and if DNS return IP addresses
- config.settings.userallowedip = await resolveDomainsToIps(config.settings.userallowedip);
- config.settings.userblockedip = await resolveDomainsToIps(config.settings.userblockedip);
- config.settings.agentallowedip = await resolveDomainsToIps(config.settings.agentallowedip);
- config.settings.agentblockedip = await resolveDomainsToIps(config.settings.agentblockedip);
- config.settings.swarmallowedip = await resolveDomainsToIps(config.settings.swarmallowedip);
-
- // Check the "cookieIpCheck" value
- if ((obj.args.cookieipcheck === false) || (obj.args.cookieipcheck == 'none')) { obj.args.cookieipcheck = 'none'; }
- else if ((typeof obj.args.cookieipcheck != 'string') || (obj.args.cookieipcheck.toLowerCase() != 'strict')) { obj.args.cookieipcheck = 'lax'; }
- else { obj.args.cookieipcheck = 'strict'; }
-
- // Check the "cookieSameSite" value
- if (typeof obj.args.cookiesamesite != 'string') { delete obj.args.cookiesamesite; }
- else if (['none', 'lax', 'strict'].indexOf(obj.args.cookiesamesite.toLowerCase()) == -1) { delete obj.args.cookiesamesite; } else { obj.args.cookiesamesite = obj.args.cookiesamesite.toLowerCase(); }
-
- // Check if WebSocket compression is supported. It's known to be broken in NodeJS v11.11 to v12.15, and v13.2
- const verSplit = process.version.substring(1).split('.');
- const ver = parseInt(verSplit[0]) + (parseInt(verSplit[1]) / 100);
- if (((ver >= 11.11) && (ver <= 12.15)) || (ver == 13.2)) {
- if ((obj.args.wscompression === true) || (obj.args.agentwscompression === true)) { addServerWarning('WebSocket compression is disabled, this feature is broken in NodeJS v11.11 to v12.15 and v13.2', 4); }
- obj.args.wscompression = obj.args.agentwscompression = false;
- obj.config.settings.wscompression = obj.config.settings.agentwscompression = false;
- }
-
- // Local console tracing
- if (typeof obj.args.debug == 'string') { obj.debugSources = obj.args.debug.toLowerCase().split(','); }
- else if (typeof obj.args.debug == 'object') { obj.debugSources = obj.args.debug; }
- else if (obj.args.debug === true) { obj.debugSources = '*'; }
-
- require('./db.js').CreateDB(obj,
- function (db) {
- obj.db = db;
- obj.db.SetupDatabase(function (dbversion) {
- // See if any database operations needs to be completed
- if (obj.args.deletedomain) { obj.db.DeleteDomain(obj.args.deletedomain, function () { console.log('Deleted domain ' + obj.args.deletedomain + '.'); process.exit(); }); return; }
- if (obj.args.deletedefaultdomain) { obj.db.DeleteDomain('', function () { console.log('Deleted default domain.'); process.exit(); }); return; }
- if (obj.args.showall) { obj.db.GetAll(function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
- if (obj.args.showusers) { obj.db.GetAllType('user', function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
- if (obj.args.showitem) { obj.db.Get(obj.args.showitem, function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
- if (obj.args.listuserids) { obj.db.GetAllType('user', function (err, docs) { for (var i in docs) { console.log(docs[i]._id); } process.exit(); }); return; }
- if (obj.args.showusergroups) { obj.db.GetAllType('ugrp', function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
- if (obj.args.shownodes) { obj.db.GetAllType('node', function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
- if (obj.args.showallmeshes) { obj.db.GetAllType('mesh', function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
- if (obj.args.showmeshes) { obj.db.GetAllType('mesh', function (err, docs) { var x = []; for (var i in docs) { if (docs[i].deleted == null) { x.push(docs[i]); } } console.log(JSON.stringify(x, null, 2)); process.exit(); }); return; }
- if (obj.args.showevents) { obj.db.GetAllEvents(function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
- if (obj.args.showsmbios) { obj.db.GetAllSMBIOS(function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
- if (obj.args.showpower) { obj.db.getAllPower(function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
- if (obj.args.clearpower) { obj.db.removeAllPowerEvents(function () { process.exit(); }); return; }
- if (obj.args.showiplocations) { obj.db.GetAllType('iploc', function (err, docs) { console.log(docs); process.exit(); }); return; }
- if (obj.args.logintoken) { obj.getLoginToken(obj.args.logintoken, function (r) { console.log(r); process.exit(); }); return; }
- if (obj.args.logintokenkey) { obj.showLoginTokenKey(function (r) { console.log(r); process.exit(); }); return; }
- if (obj.args.recordencryptionrecode) { obj.db.performRecordEncryptionRecode(function (count) { console.log('Re-encoded ' + count + ' record(s).'); process.exit(); }); return; }
- if (obj.args.dbstats) { obj.db.getDbStats(function (stats) { console.log(stats); process.exit(); }); return; }
- if (obj.args.createaccount) { // Create a new user account
- if ((typeof obj.args.createaccount != 'string') || ((obj.args.pass == null) && (obj.args.hashpass == null)) || (obj.args.pass == '') || (obj.args.hashpass == '') || (obj.args.createaccount.indexOf(' ') >= 0)) { console.log("Usage: --createaccount [userid] --pass [password] --domain (domain) --email (email) --name (name)."); process.exit(); return; }
- var userid = 'user/' + (obj.args.domain ? obj.args.domain : '') + '/' + obj.args.createaccount.toLowerCase(), domainid = obj.args.domain ? obj.args.domain : '';
- if (obj.args.createaccount.startsWith('user/')) { userid = obj.args.createaccount; domainid = obj.args.createaccount.split('/')[1]; }
- if (userid.split('/').length != 3) { console.log("Invalid userid."); process.exit(); return; }
- obj.db.Get(userid, function (err, docs) {
- if (err != null) { console.log("Database error: " + err); process.exit(); return; }
- if ((docs != null) && (docs.length != 0)) { console.log('User already exists.'); process.exit(); return; }
- if ((domainid != '') && ((config.domains == null) || (config.domains[domainid] == null))) { console.log("Invalid domain."); process.exit(); return; }
- const user = { _id: userid, type: 'user', name: (typeof obj.args.name == 'string') ? obj.args.name : (userid.split('/')[2]), domain: domainid, creation: Math.floor(Date.now() / 1000), links: {} };
- if (typeof obj.args.email == 'string') { user.email = obj.args.email; user.emailVerified = true; }
- if (obj.args.hashpass) {
- // Create an account using a pre-hashed password. Use --hashpassword to pre-hash a password.
- var hashpasssplit = obj.args.hashpass.split(',');
- if (hashpasssplit.length != 2) { console.log("Invalid hashed password."); process.exit(); return; }
- user.salt = hashpasssplit[0];
- user.hash = hashpasssplit[1];
- obj.db.Set(user, function () { console.log("Done. This command will only work if MeshCentral is stopped."); process.exit(); return; });
- } else {
- // Hash the password and create the account.
- require('./pass').hash(obj.args.pass, function (err, salt, hash, tag) { if (err) { console.log("Unable create account password: " + err); process.exit(); return; } user.salt = salt; user.hash = hash; obj.db.Set(user, function () { console.log("Done."); process.exit(); return; }); }, 0);
- }
- });
- return;
- }
- if (obj.args.resetaccount) { // Unlock a user account, set a new password and remove 2FA
- if ((typeof obj.args.resetaccount != 'string') || (obj.args.resetaccount.indexOf(' ') >= 0)) { console.log("Usage: --resetaccount [userid] --domain (domain) --pass [password]."); process.exit(); return; }
- var userid = 'user/' + (obj.args.domain ? obj.args.domain : '') + '/' + obj.args.resetaccount.toLowerCase();
- if (obj.args.resetaccount.startsWith('user/')) { userid = obj.args.resetaccount; }
- if (userid.split('/').length != 3) { console.log("Invalid userid."); process.exit(); return; }
- obj.db.Get(userid, function (err, docs) {
- if (err != null) { console.log("Database error: " + err); process.exit(); return; }
- if ((docs == null) || (docs.length == 0)) { console.log("Unknown userid, usage: --resetaccount [userid] --domain (domain) --pass [password]."); process.exit(); return; }
- const user = docs[0]; if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { user.siteadmin -= 32; } // Unlock the account.
- delete user.phone; delete user.otpekey; delete user.otpsecret; delete user.otpkeys; delete user.otphkeys; delete user.otpdev; delete user.otpsms; delete user.otpmsg; user.otpduo; // Disable 2FA
- delete user.msghandle; // Disable users 2fa messaging too
- var config = getConfig(false);
- if (config.domains[user.domain].auth || config.domains[user.domain].authstrategies) {
- console.log('This users domain has external authentication methods enabled so the password will not be changed if you set one')
- obj.db.Set(user, function () { console.log("Done."); process.exit(); return; });
- } else {
- if (obj.args.hashpass && (typeof obj.args.hashpass == 'string')) {
- // Reset an account using a pre-hashed password. Use --hashpassword to pre-hash a password.
- var hashpasssplit = obj.args.hashpass.split(',');
- if (hashpasssplit.length != 2) { console.log("Invalid hashed password."); process.exit(); return; }
- user.salt = hashpasssplit[0];
- user.hash = hashpasssplit[1];
- obj.db.Set(user, function () { console.log("Done. This command will only work if MeshCentral is stopped."); process.exit(); return; });
- } else if (obj.args.pass && (typeof obj.args.pass == 'string')) {
- // Hash the password and reset the account.
- require('./pass').hash(String(obj.args.pass), user.salt, function (err, hash, tag) {
- if (err) { console.log("Unable to reset password: " + err); process.exit(); return; }
- user.hash = hash;
- obj.db.Set(user, function () { console.log("Done."); process.exit(); return; });
- }, 0);
- } else {
- console.log('Not setting a users password');
- obj.db.Set(user, function () { console.log("Done."); process.exit(); return; });
- }
- }
- });
- return;
- }
- if (obj.args.adminaccount) { // Set a user account to server administrator
- if ((typeof obj.args.adminaccount != 'string') || (obj.args.adminaccount.indexOf(' ') >= 0)) { console.log("Invalid userid, usage: --adminaccount [username] --domain (domain)"); process.exit(); return; }
- var userid = 'user/' + (obj.args.domain ? obj.args.domain : '') + '/' + obj.args.adminaccount.toLowerCase();
- if (obj.args.adminaccount.startsWith('user/')) { userid = obj.args.adminaccount; }
- if (userid.split('/').length != 3) { console.log("Invalid userid."); process.exit(); return; }
- obj.db.Get(userid, function (err, docs) {
- if (err != null) { console.log("Database error: " + err); process.exit(); return; }
- if ((docs == null) || (docs.length == 0)) { console.log("Unknown userid, usage: --adminaccount [userid] --domain (domain)."); process.exit(); return; }
- docs[0].siteadmin = 0xFFFFFFFF; // Set user as site administrator
- obj.db.Set(docs[0], function () { console.log("Done. This command will only work if MeshCentral is stopped."); process.exit(); return; });
- });
- return;
- }
- if (obj.args.removesubdomain) { // Remove all references to a sub domain from the database
- if ((typeof obj.args.removesubdomain != 'string') || (obj.args.removesubdomain.indexOf(' ') >= 0)) { console.log("Invalid sub domain, usage: --removesubdomain [domain]"); process.exit(); return; }
- obj.db.removeDomain(obj.args.removesubdomain, function () { console.log("Done."); process.exit(); return; });
- return;
- }
- if (obj.args.removetestagents) { // Remove all test agents from the database
- db.GetAllType('node', function (err, docs) {
- if ((err != null) || (docs.length == 0)) {
- console.log('Unable to get any nodes from the database');
- process.exit(0);
- } else {
- // Load all users
- const allusers = {}, removeCount = 0;
- obj.db.GetAllType('user', function (err, docs) {
- obj.common.unEscapeAllLinksFieldName(docs);
- for (i in docs) { allusers[docs[i]._id] = docs[i]; }
- });
-
- // Look at all devices
- for (var i in docs) {
- if ((docs[i] != null) && (docs[i].agent != null) && (docs[i].agent.id == 23)) {
- // Remove this test node
- const node = docs[i];
-
- // Delete this node including network interface information, events and timeline
- removeCount++;
- db.Remove(node._id); // Remove node with that id
- db.Remove('if' + node._id); // Remove interface information
- db.Remove('nt' + node._id); // Remove notes
- db.Remove('lc' + node._id); // Remove last connect time
- db.Remove('si' + node._id); // Remove system information
- if (db.RemoveSMBIOS) { db.RemoveSMBIOS(node._id); } // Remove SMBios data
- db.RemoveAllNodeEvents(node._id); // Remove all events for this node
- db.removeAllPowerEventsForNode(node._id); // Remove all power events for this node
- if (typeof node.pmt == 'string') { db.Remove('pmt_' + node.pmt); } // Remove Push Messaging Token
- db.Get('ra' + node._id, function (err, nodes) {
- if ((nodes != null) && (nodes.length == 1)) { db.Remove('da' + nodes[0].daid); } // Remove diagnostic agent to real agent link
- db.Remove('ra' + node._id); // Remove real agent to diagnostic agent link
- });
-
- // Remove any user node links
- if (node.links != null) {
- for (var i in node.links) {
- if (i.startsWith('user/')) {
- var cuser = allusers[i];
- if ((cuser != null) && (cuser.links != null) && (cuser.links[node._id] != null)) {
- // Remove the user link & save the user
- delete cuser.links[node._id];
- if (Object.keys(cuser.links).length == 0) { delete cuser.links; }
- db.SetUser(cuser);
- }
- }
- }
- }
-
- }
- }
- if (removeCount == 0) {
- console.log("Done, no devices removed.");
- process.exit(0);
- } else {
- console.log("Removed " + removeCount + " device(s), holding 10 seconds...");
- setTimeout(function () { console.log("Done."); process.exit(0); }, 10000)
- }
- }
- });
- return;
- }
-
- // Import NeDB data into database
- if (obj.args.nedbtodb) {
- if (db.databaseType == 1) { console.log("NeDB is current database, can't perform transfer."); process.exit(); return; }
- console.log("Transfering NeDB data into database...");
- db.nedbtodb(function (msg) { console.log(msg); process.exit(); })
- return;
- }
-
- // Show a list of all configuration files in the database
- if (obj.args.dblistconfigfiles) {
- obj.db.GetAllType('cfile', function (err, docs) {
- if (err == null) {
- if (docs.length == 0) {
- console.log("No files found.");
- } else {
- for (var i in docs) {
- if (typeof obj.args.dblistconfigfiles == 'string') {
- const data = obj.db.decryptData(obj.args.dblistconfigfiles, docs[i].data);
- if (data == null) {
- console.log(docs[i]._id.split('/')[1] + ', ' + Buffer.from(docs[i].data, 'base64').length + ' encrypted bytes - Unable to decrypt.');
- } else {
- console.log(docs[i]._id.split('/')[1] + ', ' + data.length + ' bytes, decoded correctly.');
- }
- } else {
- console.log(docs[i]._id.split('/')[1] + ', ' + Buffer.from(docs[i].data, 'base64').length + ' encrypted bytes.');
- }
- }
- }
- } else { console.log('Unable to read from database.'); } process.exit();
- });
- return;
- }
-
- // Display the content of a configuration file in the database
- if (obj.args.dbshowconfigfile) {
- if (typeof obj.args.configkey != 'string') { console.log("Error, --configkey is required."); process.exit(); return; }
- obj.db.getConfigFile(obj.args.dbshowconfigfile, function (err, docs) {
- if (err == null) {
- if (docs.length == 0) { console.log("File not found."); } else {
- const data = obj.db.decryptData(obj.args.configkey, docs[0].data);
- if (data == null) { console.log("Invalid config key."); } else { console.log(data); }
- }
- } else { console.log("Unable to read from database."); }
- process.exit();
- }); return;
- }
-
- // Delete all configuration files from database
- if (obj.args.dbdeleteconfigfiles) {
- console.log("Deleting all configuration files from the database..."); obj.db.RemoveAllOfType('cfile', function () { console.log('Done.'); process.exit(); });
- }
-
- // Push all relevent files from meshcentral-data into the database
- if (obj.args.dbpushconfigfiles) {
- if (typeof obj.args.configkey != 'string') { console.log("Error, --configkey is required."); process.exit(); return; }
- if ((obj.args.dbpushconfigfiles !== true) && (typeof obj.args.dbpushconfigfiles != 'string')) {
- console.log("Usage: --dbpulldatafiles (path) This will import files from folder into the database");
- console.log(" --dbpulldatafiles This will import files from meshcentral-data into the db.");
- process.exit();
- } else {
- if ((obj.args.dbpushconfigfiles == '*') || (obj.args.dbpushconfigfiles === true)) { obj.args.dbpushconfigfiles = obj.datapath; }
- obj.fs.readdir(obj.args.dbpushconfigfiles, function (err, files) {
- if (err != null) { console.log('ERROR: Unable to read from folder ' + obj.args.dbpushconfigfiles); process.exit(); return; }
- var configFound = false;
- for (var i in files) { if (files[i] == 'config.json') { configFound = true; } }
- if (configFound == false) { console.log('ERROR: No config.json in folder ' + obj.args.dbpushconfigfiles); process.exit(); return; }
- obj.db.RemoveAllOfType('cfile', function () {
- obj.fs.readdir(obj.args.dbpushconfigfiles, function (err, files) {
- var lockCount = 1
- for (var i in files) {
- const file = files[i];
- if ((file == 'config.json') || file.endsWith('.key') || file.endsWith('.crt') || (file == 'terms.txt') || file.endsWith('.jpg') || file.endsWith('.png')) {
- const path = obj.path.join(obj.args.dbpushconfigfiles, files[i]), binary = Buffer.from(obj.fs.readFileSync(path, { encoding: 'binary' }), 'binary');
- console.log('Pushing ' + file + ', ' + binary.length + ' bytes.');
- lockCount++;
- if (obj.args.oldencrypt) {
- obj.db.setConfigFile(file, obj.db.oldEncryptData(obj.args.configkey, binary), function () { if ((--lockCount) == 0) { console.log('Done.'); process.exit(); } });
- } else {
- obj.db.setConfigFile(file, obj.db.encryptData(obj.args.configkey, binary), function () { if ((--lockCount) == 0) { console.log('Done.'); process.exit(); } });
- }
- }
- }
- if (--lockCount == 0) { process.exit(); }
- });
- });
- });
- }
- return;
- }
-
- // Pull all database files into meshcentral-data
- if (obj.args.dbpullconfigfiles) {
- if (typeof obj.args.configkey != 'string') { console.log("Error, --configkey is required."); process.exit(); return; }
- if (typeof obj.args.dbpullconfigfiles != 'string') {
- console.log("Usage: --dbpulldatafiles (path)");
- process.exit();
- } else {
- obj.db.GetAllType('cfile', function (err, docs) {
- if (err == null) {
- if (docs.length == 0) {
- console.log("File not found.");
- } else {
- for (var i in docs) {
- const file = docs[i]._id.split('/')[1], binary = obj.db.decryptData(obj.args.configkey, docs[i].data);
- if (binary == null) {
- console.log("Invalid config key.");
- } else {
- const fullFileName = obj.path.join(obj.args.dbpullconfigfiles, file);
- try { obj.fs.writeFileSync(fullFileName, binary); } catch (ex) { console.log('Unable to write to ' + fullFileName); process.exit(); return; }
- console.log('Pulling ' + file + ', ' + binary.length + ' bytes.');
- }
- }
- }
- } else {
- console.log("Unable to read from database.");
- }
- process.exit();
- });
- }
- return;
- }
-
- if (obj.args.dbexport) {
- // Export the entire database to a JSON file
- if (obj.args.dbexport == true) { obj.args.dbexport = obj.getConfigFilePath('meshcentral.db.json'); }
- obj.db.GetAll(function (err, docs) {
- obj.fs.writeFileSync(obj.args.dbexport, JSON.stringify(docs));
- console.log('Exported ' + docs.length + ' objects(s) to ' + obj.args.dbexport + '.'); process.exit();
- });
- return;
- }
- if (obj.args.dbexportmin) {
- // Export a minimal database to a JSON file. Export only users, meshes and nodes.
- // This is a useful command to look at the database.
- if (obj.args.dbexportmin == true) { obj.args.dbexportmin = obj.getConfigFilePath('meshcentral.db.json'); }
- obj.db.GetAllType({ $in: ['user', 'node', 'mesh'] }, function (err, docs) {
- obj.fs.writeFileSync(obj.args.dbexportmin, JSON.stringify(docs));
- console.log('Exported ' + docs.length + ' objects(s) to ' + obj.args.dbexportmin + '.'); process.exit();
- });
- return;
- }
- if (obj.args.dbimport) {
- // Import the entire database from a JSON file
- if (obj.args.dbimport == true) { obj.args.dbimport = obj.getConfigFilePath('meshcentral.db.json'); }
- var json = null, json2 = '', badCharCount = 0;
- try { json = obj.fs.readFileSync(obj.args.dbimport, { encoding: 'utf8' }); } catch (ex) { console.log('Invalid JSON file: ' + obj.args.dbimport + ': ' + ex); process.exit(); }
- for (i = 0; i < json.length; i++) { if (json.charCodeAt(i) >= 32) { json2 += json[i]; } else { var tt = json.charCodeAt(i); if (tt != 10 && tt != 13) { badCharCount++; } } } // Remove all bad chars
- if (badCharCount > 0) { console.log(badCharCount + ' invalid character(s) where removed.'); }
- try { json = JSON.parse(json2); } catch (ex) { console.log('Invalid JSON format: ' + obj.args.dbimport + ': ' + e); process.exit(); }
- if ((json == null) || (typeof json.length != 'number') || (json.length < 1)) { console.log('Invalid JSON format: ' + obj.args.dbimport + '.'); }
- // Escape MongoDB invalid field chars
- for (i in json) {
- const doc = json[i];
- for (var j in doc) { if (j.indexOf('.') >= 0) { console.log("Invalid field name (" + j + ") in document: " + json[i]); return; } }
- //if ((json[i].type == 'ifinfo') && (json[i].netif2 != null)) { for (var j in json[i].netif2) { var esc = obj.common.escapeFieldName(j); if (esc !== j) { json[i].netif2[esc] = json[i].netif2[j]; delete json[i].netif2[j]; } } }
- //if ((json[i].type == 'mesh') && (json[i].links != null)) { for (var j in json[i].links) { var esc = obj.common.escapeFieldName(j); if (esc !== j) { json[i].links[esc] = json[i].links[j]; delete json[i].links[j]; } } }
- }
- //for (i in json) { if ((json[i].type == "node") && (json[i].host != null)) { json[i].rname = json[i].host; delete json[i].host; } } // DEBUG: Change host to rname
- setTimeout(function () { // If the Mongo database is being created for the first time, there is a race condition here. This will get around it.
- obj.db.RemoveAll(function () {
- obj.db.InsertMany(json, function (err) {
- if (err != null) { console.log(err); } else { console.log('Imported ' + json.length + ' objects(s) from ' + obj.args.dbimport + '.'); } process.exit();
- });
- });
- }, 100);
- return;
- }
- /*
- if (obj.args.dbimport) {
- // Import the entire database from a very large JSON file
- obj.db.RemoveAll(function () {
- if (obj.args.dbimport == true) { obj.args.dbimport = obj.getConfigFilePath('meshcentral.db.json'); }
- var json = null, json2 = "", badCharCount = 0;
- const StreamArray = require('stream-json/streamers/StreamArray');
- const jsonStream = StreamArray.withParser();
- jsonStream.on('data', function (data) { obj.db.Set(data.value); });
- jsonStream.on('end', () => { console.log('Done.'); process.exit(); });
- obj.fs.createReadStream(obj.args.dbimport).pipe(jsonStream.input);
- });
- return;
- }
- */
- if (obj.args.dbmerge) {
- // Import the entire database from a JSON file
- if (obj.args.dbmerge == true) { obj.args.dbmerge = obj.getConfigFilePath('meshcentral.db.json'); }
- var json = null, json2 = "", badCharCount = 0;
- try { json = obj.fs.readFileSync(obj.args.dbmerge, { encoding: 'utf8' }); } catch (ex) { console.log('Invalid JSON file: ' + obj.args.dbmerge + ': ' + ex); process.exit(); }
- for (i = 0; i < json.length; i++) { if (json.charCodeAt(i) >= 32) { json2 += json[i]; } else { var tt = json.charCodeAt(i); if (tt != 10 && tt != 13) { badCharCount++; } } } // Remove all bad chars
- if (badCharCount > 0) { console.log(badCharCount + ' invalid character(s) where removed.'); }
- try { json = JSON.parse(json2); } catch (ex) { console.log('Invalid JSON format: ' + obj.args.dbmerge + ': ' + ex); process.exit(); }
- if ((json == null) || (typeof json.length != 'number') || (json.length < 1)) { console.log('Invalid JSON format: ' + obj.args.dbimport + '.'); }
-
- // Get all users from current database
- obj.db.GetAllType('user', function (err, docs) {
- const users = {}, usersCount = 0;
- for (var i in docs) { users[docs[i]._id] = docs[i]; usersCount++; }
-
- // Fetch all meshes from the database
- obj.db.GetAllType('mesh', function (err, docs) {
- obj.common.unEscapeAllLinksFieldName(docs);
- const meshes = {}, meshesCount = 0;
- for (var i in docs) { meshes[docs[i]._id] = docs[i]; meshesCount++; }
- console.log('Loaded ' + usersCount + ' users and ' + meshesCount + ' meshes.');
- // Look at each object in the import file
- const objectToAdd = [];
- for (var i in json) {
- const newobj = json[i];
- if (newobj.type == 'user') {
- // Check if the user already exists
- var existingUser = users[newobj._id];
- if (existingUser) {
- // Merge the links
- if (typeof newobj.links == 'object') {
- for (var j in newobj.links) {
- if ((existingUser.links == null) || (existingUser.links[j] == null)) {
- if (existingUser.links == null) { existingUser.links = {}; }
- existingUser.links[j] = newobj.links[j];
- }
- }
- }
- if (existingUser.name == 'admin') { existingUser.links = {}; }
- objectToAdd.push(existingUser); // Add this user
- } else {
- objectToAdd.push(newobj); // Add this user
- }
- } else if (newobj.type == 'mesh') {
- // Add this object
- objectToAdd.push(newobj);
- } // Don't add nodes.
- }
- console.log('Importing ' + objectToAdd.length + ' object(s)...');
- var pendingCalls = 1;
- for (var i in objectToAdd) {
- pendingCalls++;
- obj.db.Set(objectToAdd[i], function (err) { if (err != null) { console.log(err); } else { if (--pendingCalls == 0) { process.exit(); } } });
- }
- if (--pendingCalls == 0) { process.exit(); }
- });
- });
- return;
- }
-
- // Check if the database is capable of performing a backup
- // Moved behind autobackup config init in startex4: obj.db.checkBackupCapability(function (err, msg) { if (msg != null) { obj.addServerWarning(msg, true) } });
-
- // Load configuration for database if needed
- if (obj.args.loadconfigfromdb) {
- var key = null;
- if (typeof obj.args.configkey == 'string') { key = obj.args.configkey; }
- else if (typeof obj.args.loadconfigfromdb == 'string') { key = obj.args.loadconfigfromdb; }
- if (key == null) { console.log("Error, --configkey is required."); process.exit(); return; }
- obj.db.getAllConfigFiles(key, function (configFiles) {
- if (configFiles == null) { console.log("Error, no configuration files found or invalid configkey."); process.exit(); return; }
- if (!configFiles['config.json']) { console.log("Error, could not file config.json from database."); process.exit(); return; }
- if (typeof configFiles['config.json'] == 'object') { configFiles['config.json'] = configFiles['config.json'].toString(); }
- if (configFiles['config.json'].charCodeAt(0) == 65279) { configFiles['config.json'] = configFiles['config.json'].substring(1); }
- obj.configurationFiles = configFiles;
-
- // Parse the new configuration file
- var config2 = null;
- try { config2 = JSON.parse(configFiles['config.json']); } catch (ex) { console.log('Error, unable to parse config.json from database.', ex); process.exit(); return; }
-
- // Set the command line arguments to the config file if they are not present
- if (!config2.settings) { config2.settings = {}; }
- for (i in args) { config2.settings[i] = args[i]; }
-
- // Lower case all keys in the config file
- common.objKeysToLower(config2, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'telegram/proxy']);
-
- // Grab some of the values from the original config.json file if present.
- config2['mysql'] = config['mysql'];
- config2['mariadb'] = config['mariadb'];
- config2['mongodb'] = config['mongodb'];
- config2['mongodbcol'] = config['mongodbcol'];
- config2['dbencryptkey'] = config['dbencryptkey'];
- config2['acebase'] = config['acebase'];
- config2['sqlite3'] = config['sqlite3'];
-
- // We got a new config.json from the database, let's use it.
- config = obj.config = config2;
- obj.StartEx1b();
- });
- } else {
- config = obj.config = getConfig(obj.args.vault == null);
- obj.StartEx1b();
- }
- });
- }
- );
- };
-
- // Time to start the server of real.
- obj.StartEx1b = async function () {
- var i;
-
- // Add NodeJS version warning if needed
- if (Number(process.version.match(/^v(\d+\.\d+)/)[1]) < 16) { addServerWarning("MeshCentral will require Node v16 or above in the future, your current version is " + process.version + "."); }
-
- // Setup certificate operations
- obj.certificateOperations = require('./certoperations.js').CertificateOperations(obj);
-
- // Linux format /var/log/auth.log
- if (obj.config.settings.authlog != null) {
- obj.fs.open(obj.config.settings.authlog, 'a', function (err, fd) {
- if (err == null) { obj.authlogfile = fd; } else { console.log('ERROR: Unable to open: ' + obj.config.settings.authlog); }
- })
- }
-
- // Start CrowdSec bouncer if needed: https://www.crowdsec.net/
- if (typeof obj.args.crowdsec == 'object') { obj.crowdSecBounser = require('./crowdsec.js').CreateCrowdSecBouncer(obj, obj.args.crowdsec); }
-
- // Check if self update is allowed. If running as a Windows service, self-update is not possible.
- if (obj.fs.existsSync(obj.path.join(__dirname, 'daemon'))) { obj.serverSelfWriteAllowed = false; }
-
- // If we are targetting a specific version, update now.
- if ((obj.serverSelfWriteAllowed == true) && (typeof obj.args.selfupdate == 'string')) {
- obj.args.selfupdate = obj.args.selfupdate.toLowerCase();
- if (getCurrentVersion() !== obj.args.selfupdate) { obj.performServerUpdate(); return; } // We are targetting a specific version, run self update now.
- }
-
- // Write the server state
- obj.updateServerState('state', 'starting');
- if (process.pid) { obj.updateServerState('server-pid', process.pid); }
- if (process.ppid) { obj.updateServerState('server-parent-pid', process.ppid); }
-
- // Read environment variables. For a subset of arguments, we allow them to be read from environment variables.
- const xenv = ['user', 'port', 'mpsport', 'mpsaliasport', 'redirport', 'rediraliasport', 'exactport', 'debug'];
- for (i in xenv) { if ((obj.args[xenv[i]] == null) && (process.env['mesh' + xenv[i]])) { obj.args[xenv[i]] = obj.common.toNumber(process.env['mesh' + xenv[i]]); } }
-
- // Validate the domains, this is used for multi-hosting
- if (obj.config.domains == null) { obj.config.domains = {}; }
- if (obj.config.domains[''] == null) { obj.config.domains[''] = {}; }
- if (obj.config.domains[''].dns != null) { console.log("ERROR: Default domain can't have a DNS name."); return; }
- var xdomains = {}; for (i in obj.config.domains) { xdomains[i.toLowerCase()] = obj.config.domains[i]; } obj.config.domains = xdomains;
- var bannedDomains = ['public', 'private', 'images', 'scripts', 'styles', 'views']; // List of banned domains
- for (i in obj.config.domains) { for (var j in bannedDomains) { if (i == bannedDomains[j]) { console.log("ERROR: Domain '" + i + "' is not allowed domain name in config.json."); delete obj.config.domains[i]; } } }
- for (i in obj.config.domains) { if ((i.length > 64) || (Buffer.from(i).length > 64)) { console.log("ERROR: Domain '" + i + "' is longer that 64 bytes, this is not allowed."); delete obj.config.domains[i]; } }
- for (i in obj.config.domains) {
- // Remove any domains that start with underscore
- if (i.startsWith('_')) { delete obj.config.domains[i]; continue; }
-
- // Apply default domain settings if present
- if (typeof obj.config.domaindefaults == 'object') { for (var j in obj.config.domaindefaults) { if (obj.config.domains[i][j] == null) { obj.config.domains[i][j] = obj.config.domaindefaults[j]; } } }
-
- // Perform domain setup
- if (typeof obj.config.domains[i] != 'object') { console.log("ERROR: Invalid domain configuration in config.json."); process.exit(); return; }
- if ((i.length > 0) && (i[0] == '_')) { delete obj.config.domains[i]; continue; } // Remove any domains with names that start with _
- if (typeof config.domains[i].auth == 'string') { config.domains[i].auth = config.domains[i].auth.toLowerCase(); }
- if (obj.config.domains[i].limits == null) { obj.config.domains[i].limits = {}; }
- if (obj.config.domains[i].dns == null) { obj.config.domains[i].url = (i == '') ? '/' : ('/' + i + '/'); } else { obj.config.domains[i].url = '/'; }
- obj.config.domains[i].id = i;
- if ((typeof obj.config.domains[i].maxdeviceview != 'number') || (obj.config.domains[i].maxdeviceview < 1)) { delete obj.config.domains[i].maxdeviceview; }
- if (typeof obj.config.domains[i].loginkey == 'string') { obj.config.domains[i].loginkey = [obj.config.domains[i].loginkey]; }
- if ((obj.config.domains[i].loginkey != null) && (obj.common.validateAlphaNumericArray(obj.config.domains[i].loginkey, 1, 128) == false)) { console.log("ERROR: Invalid login key, must be alpha-numeric string with no spaces."); process.exit(); return; }
- if (typeof obj.config.domains[i].agentkey == 'string') { obj.config.domains[i].agentkey = [obj.config.domains[i].agentkey]; }
- if ((obj.config.domains[i].agentkey != null) && (obj.common.validateAlphaNumericArray(obj.config.domains[i].agentkey, 1, 128) == false)) { console.log("ERROR: Invalid agent key, must be alpha-numeric string with no spaces."); process.exit(); return; }
- obj.config.domains[i].userallowedip = obj.config.domains[i].userallowedip = readIpListFromFile(obj.config.domains[i].userallowedip);
- obj.config.domains[i].userblockedip = obj.config.domains[i].userblockedip = readIpListFromFile(obj.config.domains[i].userblockedip);
- obj.config.domains[i].agentallowedip = obj.config.domains[i].agentallowedip = readIpListFromFile(obj.config.domains[i].agentallowedip);
- obj.config.domains[i].agentblockedip = obj.config.domains[i].agentblockedip = readIpListFromFile(obj.config.domains[i].agentblockedip);
- if (typeof obj.config.domains[i].userallowedip == 'string') { if (obj.config.domains[i].userallowedip == '') { delete obj.config.domains[i].userallowedip; } else { obj.config.domains[i].userallowedip = obj.config.domains[i].userallowedip.split(' ').join('').split(','); } }
- if (typeof obj.config.domains[i].userblockedip == 'string') { if (obj.config.domains[i].userblockedip == '') { delete obj.config.domains[i].userblockedip; } else { obj.config.domains[i].userblockedip = obj.config.domains[i].userblockedip.split(' ').join('').split(','); } }
- if (typeof obj.config.domains[i].agentallowedip == 'string') { if (obj.config.domains[i].agentallowedip == '') { delete obj.config.domains[i].agentallowedip; } else { obj.config.domains[i].agentallowedip = obj.config.domains[i].agentallowedip.split(' ').join('').split(','); } }
- if (typeof obj.config.domains[i].agentblockedip == 'string') { if (obj.config.domains[i].agentblockedip == '') { delete obj.config.domains[i].agentblockedip; } else { obj.config.domains[i].agentblockedip = obj.config.domains[i].agentblockedip.split(' ').join('').split(','); } }
- // Check IP lists and ranges and if DNS return IP addresses
- obj.config.domains[i].userallowedip = await resolveDomainsToIps(obj.config.domains[i].userallowedip);
- obj.config.domains[i].userblockedip = await resolveDomainsToIps(obj.config.domains[i].userblockedip);
- obj.config.domains[i].agentallowedip = await resolveDomainsToIps(obj.config.domains[i].agentallowedip);
- obj.config.domains[i].agentblockedip = await resolveDomainsToIps(obj.config.domains[i].agentblockedip);
- if (typeof obj.config.domains[i].ignoreagenthashcheck == 'string') { if (obj.config.domains[i].ignoreagenthashcheck == '') { delete obj.config.domains[i].ignoreagenthashcheck; } else { obj.config.domains[i].ignoreagenthashcheck = obj.config.domains[i].ignoreagenthashcheck.split(','); } }
- if (typeof obj.config.domains[i].allowedorigin == 'string') { if (obj.config.domains[i].allowedorigin == '') { delete obj.config.domains[i].allowedorigin; } else { obj.config.domains[i].allowedorigin = obj.config.domains[i].allowedorigin.split(','); } }
- if ((obj.config.domains[i].passwordrequirements != null) && (typeof obj.config.domains[i].passwordrequirements == 'object')) {
- if (typeof obj.config.domains[i].passwordrequirements.skip2factor == 'string') {
- obj.config.domains[i].passwordrequirements.skip2factor = obj.config.domains[i].passwordrequirements.skip2factor.split(',');
- } else {
- delete obj.config.domains[i].passwordrequirements.skip2factor;
- }
- // Fix the list of users to add "user/domain/" if needed
- if (Array.isArray(obj.config.domains[i].passwordrequirements.logintokens)) {
- var newValues = [];
- for (var j in obj.config.domains[i].passwordrequirements.logintokens) {
- var splitVal = obj.config.domains[i].passwordrequirements.logintokens[j].split('/');;
- if (splitVal.length == 1) { newValues.push('user/' + i + '/' + splitVal[0]); }
- if (splitVal.length == 2) { newValues.push('user/' + splitVal[0] + '/' + splitVal[1]); }
- if (splitVal.length == 3) { newValues.push(splitVal[0] + '/' + splitVal[1] + '/' + splitVal[2]); }
- }
- obj.config.domains[i].passwordrequirements.logintokens = newValues;
- }
- }
- if ((obj.config.domains[i].auth == 'ldap') && (typeof obj.config.domains[i].ldapoptions != 'object')) {
- if (i == '') { console.log("ERROR: Default domain is LDAP, but is missing LDAPOptions."); } else { console.log("ERROR: Domain '" + i + "' is LDAP, but is missing LDAPOptions."); }
- process.exit();
- return;
- }
- if ((obj.config.domains[i].auth == 'ldap') || (obj.config.domains[i].auth == 'sspi')) { obj.config.domains[i].newaccounts = 0; } // No new accounts allowed in SSPI/LDAP authentication modes.
- if (obj.config.domains[i].sitestyle == null) { obj.config.domains[i].sitestyle = 2; } // Default to site style #2
-
- // Convert newAccountsRights from a array of strings to flags number.
- obj.config.domains[i].newaccountsrights = obj.common.meshServerRightsArrayToNumber(obj.config.domains[i].newaccountsrights);
- if (typeof (obj.config.domains[i].newaccountsrights) != 'number') { delete obj.config.domains[i].newaccountsrights; }
-
- // Check if there is a web views path and/or web public path for this domain
- if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) {
- if ((obj.config.domains[i].webviewspath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web-' + i + '/views')))) { obj.config.domains[i].webviewspath = obj.path.join(__dirname, '../../meshcentral-web-' + i + '/views'); }
- if ((obj.config.domains[i].webpublicpath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web-' + i + '/public')))) { obj.config.domains[i].webpublicpath = obj.path.join(__dirname, '../../meshcentral-web-' + i + '/public'); }
- if ((obj.config.domains[i].webemailspath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web-' + i + '/emails')))) { obj.config.domains[i].webemailspath = obj.path.join(__dirname, '../../meshcentral-web-' + i + '/emails'); }
- } else {
- if ((obj.config.domains[i].webviewspath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web-' + i + '/views')))) { obj.config.domains[i].webviewspath = obj.path.join(__dirname, '../meshcentral-web-' + i + '/views'); }
- if ((obj.config.domains[i].webpublicpath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web-' + i + '/public')))) { obj.config.domains[i].webpublicpath = obj.path.join(__dirname, '../meshcentral-web-' + i + '/public'); }
- if ((obj.config.domains[i].webemailspath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web-' + i + '/emails')))) { obj.config.domains[i].webemailspath = obj.path.join(__dirname, '../meshcentral-web-' + i + '/emails'); }
- }
-
- // Check agent customization if any
- if (typeof obj.config.domains[i].agentcustomization == 'object') {
- if (typeof obj.config.domains[i].agentcustomization.displayname != 'string') { delete obj.config.domains[i].agentcustomization.displayname; } else { obj.config.domains[i].agentcustomization.displayname = obj.config.domains[i].agentcustomization.displayname.split('\r').join('').split('\n').join(''); }
- if (typeof obj.config.domains[i].agentcustomization.description != 'string') { delete obj.config.domains[i].agentcustomization.description; } else { obj.config.domains[i].agentcustomization.description = obj.config.domains[i].agentcustomization.description.split('\r').join('').split('\n').join(''); }
- if (typeof obj.config.domains[i].agentcustomization.companyname != 'string') { delete obj.config.domains[i].agentcustomization.companyname; } else { obj.config.domains[i].agentcustomization.companyname = obj.config.domains[i].agentcustomization.companyname.split('\r').join('').split('\n').join(''); }
- if (typeof obj.config.domains[i].agentcustomization.servicename != 'string') { delete obj.config.domains[i].agentcustomization.servicename; } else { obj.config.domains[i].agentcustomization.servicename = obj.config.domains[i].agentcustomization.servicename.split('\r').join('').split('\n').join('').split(' ').join('').split('"').join('').split('\'').join('').split('>').join('').split('<').join('').split('/').join('').split('\\').join(''); }
- if (typeof obj.config.domains[i].agentcustomization.image != 'string') { delete obj.config.domains[i].agentcustomization.image; } else { try { obj.config.domains[i].agentcustomization.image = 'data:image/png;base64,' + Buffer.from(obj.fs.readFileSync(obj.getConfigFilePath(obj.config.domains[i].agentcustomization.image)), 'binary').toString('base64'); } catch (ex) { console.log(ex); delete obj.config.domains[i].agentcustomization.image; } }
- } else {
- delete obj.config.domains[i].agentcustomization;
- }
-
- // Convert user consent flags
- if (typeof obj.config.domains[i].userconsentflags == 'object') {
- var flags = 0;
- if (obj.config.domains[i].userconsentflags.desktopnotify == true) { flags |= 1; }
- if (obj.config.domains[i].userconsentflags.terminalnotify == true) { flags |= 2; }
- if (obj.config.domains[i].userconsentflags.filenotify == true) { flags |= 4; }
- if (obj.config.domains[i].userconsentflags.desktopprompt == true) { flags |= 8; }
- if (obj.config.domains[i].userconsentflags.terminalprompt == true) { flags |= 16; }
- if (obj.config.domains[i].userconsentflags.fileprompt == true) { flags |= 32; }
- if (obj.config.domains[i].userconsentflags.desktopprivacybar == true) { flags |= 64; }
- obj.config.domains[i].userconsentflags = flags;
- }
-
- // If we have Intel AMT manager settings, take a look at them here.
- if (typeof obj.config.domains[i].amtmanager == 'object') {
- if (typeof obj.config.domains[i].amtmanager.tlsrootcert == 'object') {
- obj.config.domains[i].amtmanager.tlsrootcert2 = obj.certificateOperations.loadGenericCertAndKey(obj.config.domains[i].amtmanager.tlsrootcert);
- if (obj.config.domains[i].amtmanager.tlsrootcert2 == null) { // Show an error message if needed
- if (i == '') {
- addServerWarning("Unable to load Intel AMT TLS root certificate for default domain.", 5);
- } else {
- addServerWarning("Unable to load Intel AMT TLS root certificate for domain " + i + ".", 6, [i]);
- }
- }
- }
- }
-
- // Check agentfileinfo
- if (typeof obj.config.domains[i].agentfileinfo == 'object') {
- if ((obj.config.domains[i].agentfileinfo.fileversionnumber != null) && (obj.common.parseVersion(obj.config.domains[i].agentfileinfo.fileversionnumber) == null)) { delete obj.config.domains[i].agentfileinfo.fileversionnumber; }
- if ((obj.config.domains[i].agentfileinfo.productversionnumber != null) && (obj.common.parseVersion(obj.config.domains[i].agentfileinfo.productversionnumber) == null)) { delete obj.config.domains[i].agentfileinfo.productversionnumber; }
- if ((obj.config.domains[i].agentfileinfo.fileversionnumber == null) && (typeof obj.config.domains[i].agentfileinfo.fileversion == 'string') && (obj.common.parseVersion(obj.config.domains[i].agentfileinfo.fileversion) != null)) { obj.config.domains[i].agentfileinfo.fileversionnumber = obj.config.domains[i].agentfileinfo.fileversion; }
- if (typeof obj.config.domains[i].agentfileinfo.icon == 'string') {
- // Load the agent .ico file
- var icon = null;
- try { icon = require('./authenticode.js').loadIcon(obj.path.join(obj.datapath, obj.config.domains[i].agentfileinfo.icon)); } catch (ex) { }
- if (icon != null) {
- // The icon file was correctly loaded
- obj.config.domains[i].agentfileinfo.icon = icon;
- } else {
- // Failed to load the icon file, display a server warning
- addServerWarning("Unable to load agent icon file: " + obj.config.domains[i].agentfileinfo.icon + ".", 23, [obj.config.domains[i].agentfileinfo.icon]);
- delete obj.config.domains[i].agentfileinfo.icon;
- }
- } else {
- // Invalid icon file path
- delete obj.config.domains[i].agentfileinfo.icon;
- }
- if (typeof obj.config.domains[i].agentfileinfo.logo == 'string') {
- // Load the agent .bmp file
- var logo = null;
- try { logo = require('./authenticode.js').loadBitmap(obj.path.join(obj.datapath, obj.config.domains[i].agentfileinfo.logo)); } catch (ex) { }
- if (logo != null) {
- // The logo file was correctly loaded
- obj.config.domains[i].agentfileinfo.logo = logo;
- } else {
- // Failed to load the icon file, display a server warning
- addServerWarning("Unable to load agent logo file: " + obj.config.domains[i].agentfileinfo.logo + ".", 24, [obj.config.domains[i].agentfileinfo.logo]);
- delete obj.config.domains[i].agentfileinfo.logo;
- }
- } else {
- // Invalid icon file path
- delete obj.config.domains[i].agentfileinfo.logo;
- }
- }
- }
-
- // Log passed arguments into Windows Service Log
- //if (obj.servicelog != null) { var s = ''; for (i in obj.args) { if (i != '_') { if (s.length > 0) { s += ', '; } s += i + "=" + obj.args[i]; } } logInfoEvent('MeshServer started with arguments: ' + s); }
-
- // Look at passed in arguments
- if ((obj.args.user != null) && (typeof obj.args.user != 'string')) { delete obj.args.user; }
- if ((obj.args.ciralocalfqdn != null) && ((obj.args.lanonly == true) || (obj.args.wanonly == true))) { addServerWarning("CIRA local FQDN's ignored when server in LAN-only or WAN-only mode.", 7); }
- if ((obj.args.ciralocalfqdn != null) && (obj.args.ciralocalfqdn.split(',').length > 4)) { addServerWarning("Can't have more than 4 CIRA local FQDN's. Ignoring value.", 8); obj.args.ciralocalfqdn = null; }
- if (obj.args.ignoreagenthashcheck === true) { addServerWarning("Agent hash checking is being skipped, this is unsafe.", 9); }
- if (obj.args.port == null || typeof obj.args.port != 'number') { obj.args.port = 443; }
- if (obj.args.aliasport != null && (typeof obj.args.aliasport != 'number')) obj.args.aliasport = null;
- if (obj.args.mpsport == null || typeof obj.args.mpsport != 'number') obj.args.mpsport = 4433;
- if (obj.args.mpsaliasport != null && (typeof obj.args.mpsaliasport != 'number')) obj.args.mpsaliasport = null;
- if (obj.args.rediraliasport != null && (typeof obj.args.rediraliasport != 'number')) obj.args.rediraliasport = null;
- if (obj.args.redirport == null) obj.args.redirport = 80;
- if (obj.args.minifycore == null) obj.args.minifycore = false;
- if (typeof obj.args.agentidletimeout != 'number') { obj.args.agentidletimeout = 150000; } else { obj.args.agentidletimeout *= 1000 } // Default agent idle timeout is 2m, 30sec.
- if ((obj.args.lanonly != true) && (typeof obj.args.webrtconfig == 'object')) { // fix incase you are using an old mis-spelt webrtconfig
- obj.args.webrtcconfig = obj.args.webrtconfig;
- delete obj.args.webrtconfig;
- }
- if ((obj.args.lanonly != true) && (obj.args.webrtcconfig == null)) { obj.args.webrtcconfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun.cloudflare.com:3478' }] }; } // Setup default WebRTC STUN servers
- else if ((obj.args.lanonly != true) && (typeof obj.args.webrtcconfig == 'object')) {
- if (obj.args.webrtcconfig.iceservers) { // webrtc is case-sensitive, so must rename iceservers to iceServers!
- obj.args.webrtcconfig.iceServers = obj.args.webrtcconfig.iceservers;
- delete obj.args.webrtcconfig.iceservers;
- }
- }
- if (typeof obj.args.ignoreagenthashcheck == 'string') { if (obj.args.ignoreagenthashcheck == '') { delete obj.args.ignoreagenthashcheck; } else { obj.args.ignoreagenthashcheck = obj.args.ignoreagenthashcheck.split(','); } }
-
- // Setup a site administrator
- if ((obj.args.admin) && (typeof obj.args.admin == 'string')) {
- var adminname = obj.args.admin.split('/');
- if (adminname.length == 1) { adminname = 'user//' + adminname[0]; }
- else if (adminname.length == 2) { adminname = 'user/' + adminname[0] + '/' + adminname[1]; }
- else { console.log("Invalid administrator name."); process.exit(); return; }
- obj.db.Get(adminname, function (err, user) {
- if (user.length != 1) { console.log("Invalid user name."); process.exit(); return; }
- user[0].siteadmin = 4294967295; // 0xFFFFFFFF
- obj.db.Set(user[0], function () {
- if (user[0].domain == '') { console.log('User ' + user[0].name + ' set to site administrator.'); } else { console.log("User " + user[0].name + " of domain " + user[0].domain + " set to site administrator."); }
- process.exit();
- return;
- });
- });
- return;
- }
-
- // Remove a site administrator
- if ((obj.args.unadmin) && (typeof obj.args.unadmin == 'string')) {
- var adminname = obj.args.unadmin.split('/');
- if (adminname.length == 1) { adminname = 'user//' + adminname[0]; }
- else if (adminname.length == 2) { adminname = 'user/' + adminname[0] + '/' + adminname[1]; }
- else { console.log("Invalid administrator name."); process.exit(); return; }
- obj.db.Get(adminname, function (err, user) {
- if (user.length != 1) { console.log("Invalid user name."); process.exit(); return; }
- if (user[0].siteadmin) { delete user[0].siteadmin; }
- obj.db.Set(user[0], function () {
- if (user[0].domain == '') { console.log("User " + user[0].name + " is not a site administrator."); } else { console.log("User " + user[0].name + " of domain " + user[0].domain + " is not a site administrator."); }
- process.exit();
- return;
- });
- });
- return;
- }
-
- // Setup agent error log
- if ((obj.config) && (obj.config.settings) && (obj.config.settings.agentlogdump)) {
- obj.fs.open(obj.path.join(obj.datapath, 'agenterrorlogs.txt'), 'a', function (err, fd) { obj.agentErrorLog = fd; })
- }
-
- // Perform other database cleanup
- obj.db.cleanup();
-
- // Set all nodes to power state of unknown (0)
- obj.db.storePowerEvent({ time: new Date(), nodeid: '*', power: 0, s: 1 }, obj.multiServer); // s:1 indicates that the server is starting up.
-
- // Read or setup database configuration values
- obj.db.Get('dbconfig', function (err, dbconfig) {
- if ((dbconfig != null) && (dbconfig.length == 1)) { obj.dbconfig = dbconfig[0]; } else { obj.dbconfig = { _id: 'dbconfig', version: 1 }; }
- if (obj.dbconfig.amtWsEventSecret == null) { obj.crypto.randomBytes(32, function (err, buf) { obj.dbconfig.amtWsEventSecret = buf.toString('hex'); obj.db.Set(obj.dbconfig); }); }
-
- // This is used by the user to create a username/password for a Intel AMT WSMAN event subscription
- if (obj.args.getwspass) {
- if (obj.args.getwspass.length == 64) {
- obj.crypto.randomBytes(6, function (err, buf) {
- while (obj.dbconfig.amtWsEventSecret == null) { process.nextTick(); }
- const username = buf.toString('hex');
- const nodeid = obj.args.getwspass;
- const pass = obj.crypto.createHash('sha384').update(username.toLowerCase() + ':' + nodeid + ':' + obj.dbconfig.amtWsEventSecret).digest('base64').substring(0, 12).split('/').join('x').split('\\').join('x');
- console.log("--- Intel(r) AMT WSMAN eventing credentials ---");
- console.log("Username: " + username);
- console.log("Password: " + pass);
- console.log("Argument: " + nodeid);
- process.exit();
- });
- } else {
- console.log("Invalid NodeID.");
- process.exit();
- }
- return;
- }
-
- // Setup the task manager
- if ((obj.config) && (obj.config.settings) && (obj.config.settings.taskmanager == true)) {
- obj.taskManager = require('./taskmanager').createTaskManager(obj);
- }
-
- // Start plugin manager if configuration allows this.
- if ((obj.config) && (obj.config.settings) && (obj.config.settings.plugins != null) && (obj.config.settings.plugins != false) && ((typeof obj.config.settings.plugins != 'object') || (obj.config.settings.plugins.enabled != false))) {
- obj.pluginHandler = require('./pluginHandler.js').pluginHandler(obj);
- }
-
- // Load the default meshcore and meshcmd
- obj.updateMeshCore();
- obj.updateMeshCmd();
-
- // Setup and start the redirection server if needed. We must start the redirection server before Let's Encrypt.
- if ((obj.args.redirport != null) && (typeof obj.args.redirport == 'number') && (obj.args.redirport != 0)) {
- obj.redirserver = require('./redirserver.js').CreateRedirServer(obj, obj.db, obj.args, obj.StartEx2);
- } else {
- obj.StartEx2(); // If not needed, move on.
- }
- });
- }
-
- // Done starting the redirection server, go on to load the server certificates
- obj.StartEx2 = function () {
- // Load server certificates
- obj.certificateOperations.GetMeshServerCertificate(obj.args, obj.config, function (certs) {
- // Get the current node version
- if ((obj.config.letsencrypt == null) || (obj.redirserver == null)) {
- obj.StartEx3(certs); // Just use the configured certificates
- } else if ((obj.config.letsencrypt != null) && (obj.config.letsencrypt.nochecks == true)) {
- // Use Let's Encrypt with no checking
- obj.letsencrypt = require('./letsencrypt.js').CreateLetsEncrypt(obj);
- obj.letsencrypt.getCertificate(certs, obj.StartEx3); // Use Let's Encrypt with no checking, use at your own risk.
- } else {
- // Check Let's Encrypt settings
- var leok = true;
- if ((typeof obj.config.letsencrypt.names != 'string') && (typeof obj.config.settings.cert == 'string')) { obj.config.letsencrypt.names = obj.config.settings.cert; }
- if (typeof obj.config.letsencrypt.email != 'string') { leok = false; addServerWarning("Missing Let's Encrypt email address.", 10); }
- else if (typeof obj.config.letsencrypt.names != 'string') { leok = false; addServerWarning("Invalid Let's Encrypt host names.", 11); }
- else if (obj.config.letsencrypt.names.indexOf('*') >= 0) { leok = false; addServerWarning("Invalid Let's Encrypt names, can't contain a *.", 12); }
- else if (obj.config.letsencrypt.email.split('@').length != 2) { leok = false; addServerWarning("Invalid Let's Encrypt email address.", 10); }
- else if (obj.config.letsencrypt.email.trim() !== obj.config.letsencrypt.email) { leok = false; addServerWarning("Invalid Let's Encrypt email address.", 10); }
- else {
- const le = require('./letsencrypt.js');
- try { obj.letsencrypt = le.CreateLetsEncrypt(obj); } catch (ex) { console.log(ex); }
- if (obj.letsencrypt == null) { addServerWarning("Unable to setup Let's Encrypt module.", 13); leok = false; }
- }
- if (leok == true) {
- // Check that the email address domain MX resolves.
- require('dns').resolveMx(obj.config.letsencrypt.email.split('@')[1], function (err, addresses) {
- if (err == null) {
- // Check that all names resolve
- checkResolveAll(obj.config.letsencrypt.names.split(','), function (err) {
- if (err == null) {
- obj.letsencrypt.getCertificate(certs, obj.StartEx3); // Use Let's Encrypt
- } else {
- for (var i in err) { addServerWarning("Invalid Let's Encrypt names, unable to resolve: " + err[i], 14, [err[i]]); }
- obj.StartEx3(certs); // Let's Encrypt did not load, just use the configured certificates
- }
- });
- } else {
- addServerWarning("Invalid Let's Encrypt email address, unable to resolve: " + obj.config.letsencrypt.email.split('@')[1], 15, [obj.config.letsencrypt.email.split('@')[1]]);
- obj.StartEx3(certs); // Let's Encrypt did not load, just use the configured certificates
- }
- });
- } else {
- obj.StartEx3(certs); // Let's Encrypt did not load, just use the configured certificates
- }
- }
- });
- };
-
- // Start the server with the given certificates, but check if we have web certificates to load
- obj.StartEx3 = function (certs) {
- obj.certificates = certs;
- obj.certificateOperations.acceleratorStart(certs); // Set the state of the accelerators
-
- // Load any domain web certificates
- for (var i in obj.config.domains) {
- // Load any Intel AMT ACM activation certificates
- if (obj.config.domains[i].amtacmactivation == null) { obj.config.domains[i].amtacmactivation = {}; }
- obj.certificateOperations.loadIntelAmtAcmCerts(obj.config.domains[i].amtacmactivation);
- if (obj.config.domains[i].amtacmactivation.acmCertErrors != null) { for (var j in obj.config.domains[i].amtacmactivation.acmCertErrors) { obj.addServerWarning(obj.config.domains[i].amtacmactivation.acmCertErrors[j]); } }
- if (typeof obj.config.domains[i].certurl == 'string') {
- obj.supportsProxyCertificatesRequest = true; // If a certurl is set, enable proxy cert requests
- // Then, fix the URL and add 'https://' if needed
- if (obj.config.domains[i].certurl.indexOf('://') < 0) { obj.config.domains[i].certurl = 'https://' + obj.config.domains[i].certurl; }
- }
- }
-
- // Load CloudFlare trusted proxies list if needed
- if ((obj.config.settings.trustedproxy != null) && (typeof obj.config.settings.trustedproxy == 'string') && (obj.config.settings.trustedproxy.toLowerCase() == 'cloudflare')) {
- obj.config.settings.extrascriptsrc = 'ajax.cloudflare.com'; // Add CloudFlare as a trusted script source. This allows for CloudFlare's RocketLoader feature.
- delete obj.args.trustedproxy;
- delete obj.config.settings.trustedproxy;
- obj.certificateOperations.loadTextFile('https://www.cloudflare.com/ips-v4', null, function (url, data, tag) {
- if (data != null) {
- if (Array.isArray(obj.args.trustedproxy) == false) { obj.args.trustedproxy = []; }
- const ipranges = data.split('\n');
- for (var i in ipranges) { if (ipranges[i] != '') { obj.args.trustedproxy.push(ipranges[i]); } }
- obj.certificateOperations.loadTextFile('https://www.cloudflare.com/ips-v6', null, function (url, data, tag) {
- if (data != null) {
- var ipranges = data.split('\n');
- for (var i in ipranges) { if (ipranges[i] != '') { obj.args.trustedproxy.push(ipranges[i]); } }
- obj.config.settings.trustedproxy = obj.args.trustedproxy;
- } else {
- addServerWarning("Unable to load CloudFlare trusted proxy IPv6 address list.", 16);
- }
- obj.StartEx4(); // Keep going
- });
- } else {
- addServerWarning("Unable to load CloudFlare trusted proxy IPv4 address list.", 16);
- obj.StartEx4(); // Keep going
- }
- });
- } else {
- obj.StartEx4(); // Keep going
- }
- }
-
- // Start the server with the given certificates
- obj.StartEx4 = function () {
- var i;
-
- // If the certificate is un-configured, force LAN-only mode
- if (obj.certificates.CommonName.indexOf('.') == -1) { /*console.log('Server name not configured, running in LAN-only mode.');*/ obj.args.lanonly = true; }
-
- // Write server version and run mode
- const productionMode = (process.env.NODE_ENV && (process.env.NODE_ENV == 'production'));
- const runmode = (obj.args.lanonly ? 2 : (obj.args.wanonly ? 1 : 0));
- console.log("MeshCentral v" + getCurrentVersion() + ', ' + (["Hybrid (LAN + WAN) mode", "WAN mode", "LAN mode"][runmode]) + (productionMode ? ", Production mode." : '.'));
-
- // Check that no sub-domains have the same DNS as the parent
- for (i in obj.config.domains) {
- if ((obj.config.domains[i].dns != null) && (obj.certificates.CommonName.toLowerCase() === obj.config.domains[i].dns.toLowerCase())) {
- console.log("ERROR: Server sub-domain can't have same DNS name as the parent."); process.exit(0); return;
- }
- }
-
- // Load the list of MeshCentral tools
- obj.updateMeshTools();
-
- // Load MeshAgent translation strings
- try {
- var translationpath = obj.path.join(__dirname, 'agents', 'agent-translations.json');
- const translationpath2 = obj.path.join(obj.datapath, 'agents', 'agent-translations.json');
- if (obj.fs.existsSync(translationpath2)) { translationpath = translationpath2; } // If the agent is present in "meshcentral-data/agents", use that one instead.
- var translations = JSON.parse(obj.fs.readFileSync(translationpath).toString());
- if (translations['zh-chs']) { translations['zh-hans'] = translations['zh-chs']; delete translations['zh-chs']; }
- if (translations['zh-cht']) { translations['zh-hant'] = translations['zh-cht']; delete translations['zh-cht']; }
-
- // If there is domain customizations to the agent strings, do this here.
- for (var i in obj.config.domains) {
- var domainTranslations = translations;
- if ((typeof obj.config.domains[i].agentcustomization == 'object') && (typeof obj.config.domains[i].agentcustomization.installtext == 'string')) {
- domainTranslations = Object.assign({}, domainTranslations); // Shallow clone
- for (var j in domainTranslations) { delete domainTranslations[j].description; }
- domainTranslations.en.description = obj.config.domains[i].agentcustomization.installtext;
- }
- obj.config.domains[i].agentTranslations = JSON.stringify(domainTranslations);
- }
- } catch (ex) { }
-
- // Load any domain specific agents
- for (var i in obj.config.domains) { if ((i != '') && (obj.config.domains[i].share == null)) { obj.updateMeshAgentsTable(obj.config.domains[i], function () { }); } }
-
- // Load the list of mesh agents and install scripts
- if ((obj.args.noagentupdate == 1) || (obj.args.noagentupdate == true)) { for (i in obj.meshAgentsArchitectureNumbers) { obj.meshAgentsArchitectureNumbers[i].update = false; } }
- obj.signMeshAgents(obj.config.domains[''], function () {
- obj.updateMeshAgentsTable(obj.config.domains[''], function () {
- obj.updateMeshAgentInstallScripts();
-
- // Setup and start the web server
- obj.crypto.randomBytes(48, function (err, buf) {
- // Setup Mesh Multi-Server if needed
- obj.multiServer = require('./multiserver.js').CreateMultiServer(obj, obj.args);
- if (obj.multiServer != null) {
- if ((obj.db.databaseType != 3) || (obj.db.changeStream != true)) { console.log("ERROR: Multi-server support requires use of MongoDB with ReplicaSet and ChangeStream enabled."); process.exit(0); return; }
- if (typeof obj.args.sessionkey != 'string') { console.log("ERROR: Multi-server support requires \"SessionKey\" be set in the settings section of config.json, same key for all servers."); process.exit(0); return; }
- obj.serverId = obj.multiServer.serverid;
- for (var serverid in obj.config.peers.servers) { obj.peerConnectivityByNode[serverid] = {}; }
- }
-
- // If the server is set to "nousers", allow only loopback unless IP filter is set
- if ((obj.args.nousers == true) && (obj.args.userallowedip == null)) { obj.args.userallowedip = "::1,127.0.0.1"; }
-
- // Set the session length to 60 minutes if not set and set a random key if needed
- if ((obj.args.sessiontime != null) && ((typeof obj.args.sessiontime != 'number') || (obj.args.sessiontime < 1))) { delete obj.args.sessiontime; }
- if (typeof obj.args.sessionkey != 'string') { obj.args.sessionkey = buf.toString('hex').toUpperCase(); }
-
- // Create MQTT Broker to hook into webserver and mpsserver
- if ((typeof obj.config.settings.mqtt == 'object') && (typeof obj.config.settings.mqtt.auth == 'object') && (typeof obj.config.settings.mqtt.auth.keyid == 'string') && (typeof obj.config.settings.mqtt.auth.key == 'string')) { obj.mqttbroker = require("./mqttbroker.js").CreateMQTTBroker(obj, obj.db, obj.args); }
-
- // Start the web server and if needed, the redirection web server.
- obj.webserver = require('./webserver.js').CreateWebServer(obj, obj.db, obj.args, obj.certificates, obj.StartEx5);
- if (obj.redirserver != null) { obj.redirserver.hookMainWebServer(obj.certificates); }
-
- // Change RelayDNS to a array of strings
- if (typeof obj.args.relaydns == 'string') { obj.args.relaydns = [obj.args.relaydns]; }
- if (obj.common.validateStrArray(obj.args.relaydns, 1) == false) { delete obj.args.relaydns; }
-
- // Start the HTTP relay web server if needed
- if ((obj.args.relaydns == null) && (typeof obj.args.relayport == 'number') && (obj.args.relayport != 0)) {
- obj.webrelayserver = require('./webrelayserver.js').CreateWebRelayServer(obj, obj.db, obj.args, obj.certificates, function () { });
- }
-
- // Update proxy certificates
- if (obj.supportsProxyCertificatesRequest == true) { obj.updateProxyCertificates(true); }
-
- // Setup the Intel AMT event handler
- obj.amtEventHandler = require('./amtevents.js').CreateAmtEventsHandler(obj);
-
- // Setup the Intel AMT local network scanner
- if (obj.args.wanonly != true) {
- if (obj.args.amtscanner != false) { obj.amtScanner = require('./amtscanner.js').CreateAmtScanner(obj).start(); }
- if (obj.args.meshscanner != false) { obj.meshScanner = require('./meshscanner.js').CreateMeshScanner(obj).start(); }
- }
-
- // Setup and start the MPS server
- obj.mpsserver = require('./mpsserver.js').CreateMpsServer(obj, obj.db, obj.args, obj.certificates);
-
- // Setup the Intel AMT manager
- if (obj.args.amtmanager !== false) {
- obj.amtManager = require('./amtmanager.js').CreateAmtManager(obj);
- }
-
- // Setup and start the legacy swarm server
- if ((obj.certificates.swarmserver != null) && (obj.args.swarmport != null) && (obj.args.swarmport !== 0)) {
- obj.swarmserver = require('./swarmserver.js').CreateSwarmServer(obj, obj.db, obj.args, obj.certificates);
- }
-
- // Setup the main email server
- if (obj.config.sendgrid != null) {
- // Sendgrid server
- obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
- obj.mailserver.verify();
- if (obj.args.lanonly == true) { addServerWarning("SendGrid server has limited use in LAN mode.", 17); }
- } else if (obj.config.smtp != null) {
- // SMTP server
- obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
- obj.mailserver.verify();
- if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
- } else if (obj.config.sendmail != null) {
- // Sendmail server
- obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
- obj.mailserver.verify();
- if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
- }
-
- // Setup the email server for each domain
- for (i in obj.config.domains) {
- if (obj.config.domains[i].sendgrid != null) {
- // Sendgrid server
- obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
- obj.config.domains[i].mailserver.verify();
- if (obj.args.lanonly == true) { addServerWarning("SendGrid server has limited use in LAN mode.", 17); }
- } else if ((obj.config.domains[i].smtp != null) && (obj.config.domains[i].smtp.host != null) && (obj.config.domains[i].smtp.from != null)) {
- // SMTP server
- obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
- obj.config.domains[i].mailserver.verify();
- if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
- } else if (obj.config.domains[i].sendmail != null) {
- // Sendmail server
- obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
- obj.config.domains[i].mailserver.verify();
- if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
- } else {
- // Setup the parent mail server for this domain
- if (obj.mailserver != null) { obj.config.domains[i].mailserver = obj.mailserver; }
- }
- }
-
- // Setup SMS gateway
- if (config.sms != null) {
- obj.smsserver = require('./meshsms.js').CreateMeshSMS(obj);
- if ((obj.smsserver != null) && (obj.args.lanonly == true)) { addServerWarning("SMS gateway has limited use in LAN mode.", 19); }
- }
-
- // Setup user messaging
- if (config.messaging != null) {
- obj.msgserver = require('./meshmessaging.js').CreateServer(obj);
- }
-
- // Setup web based push notifications
- if ((typeof config.settings.webpush == 'object') && (typeof config.settings.webpush.email == 'string')) {
- obj.webpush = require('web-push');
- var vapidKeys = null;
- try { vapidKeys = JSON.parse(obj.fs.readFileSync(obj.path.join(obj.datapath, 'vapid.json')).toString()); } catch (ex) { }
- if ((vapidKeys == null) || (typeof vapidKeys.publicKey != 'string') || (typeof vapidKeys.privateKey != 'string')) {
- console.log("Generating web push VAPID keys...");
- vapidKeys = obj.webpush.generateVAPIDKeys();
- obj.common.moveOldFiles([obj.path.join(obj.datapath, 'vapid.json')]);
- obj.fs.writeFileSync(obj.path.join(obj.datapath, 'vapid.json'), JSON.stringify(vapidKeys));
- }
- obj.webpush.vapidPublicKey = vapidKeys.publicKey;
- obj.webpush.setVapidDetails('mailto:' + config.settings.webpush.email, vapidKeys.publicKey, vapidKeys.privateKey);
- if (typeof config.settings.webpush.gcmapi == 'string') { webpush.setGCMAPIKey(config.settings.webpush.gcmapi); }
- }
-
- // Get the current node version
- const verSplit = process.version.substring(1).split('.');
- var nodeVersion = parseInt(verSplit[0]) + (parseInt(verSplit[1]) / 100);
-
- // Setup Firebase
- if ((config.firebase != null) && (typeof config.firebase.senderid == 'string') && (typeof config.firebase.serverkey == 'string')) {
- addServerWarning('Firebase now requires a service account JSON file, Firebase disabled.', 27);
- } else if ((config.firebase != null) && (typeof config.firebase.serviceaccountfile == 'string')) {
- var serviceAccount;
- try { serviceAccount = JSON.parse(obj.fs.readFileSync(obj.path.join(obj.datapath, config.firebase.serviceaccountfile)).toString()); } catch (ex) { console.log(ex); }
- if (serviceAccount != null) { obj.firebase = require('./firebase').CreateFirebase(obj, serviceAccount); }
- } else if ((typeof config.firebaserelay == 'object') && (typeof config.firebaserelay.url == 'string')) {
- // Setup the push messaging relay
- obj.firebase = require('./firebase').CreateFirebaseRelay(obj, config.firebaserelay.url, config.firebaserelay.key);
- } else if (obj.config.settings.publicpushnotifications === true) {
- // Setup the Firebase push messaging relay using https://alt.meshcentral.com, this is the public push notification server.
- obj.firebase = require('./firebase').CreateFirebaseRelay(obj, 'https://alt.meshcentral.com/firebaserelay.aspx');
- }
-
- // Setup monitoring
- obj.monitoring = require('./monitoring.js').CreateMonitoring(obj, obj.args);
-
- // Start periodic maintenance
- obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 60 * 60); // Run this every hour
- //obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 10 * 1); // DEBUG: Run this more often
-
- // Dispatch an event that the server is now running
- obj.DispatchEvent(['*'], obj, { etype: 'server', action: 'started', msg: 'Server started' });
-
- // Plugin hook. Need to run something at server startup? This is the place.
- if (obj.pluginHandler) { obj.pluginHandler.callHook('server_startup'); }
-
- // Setup the login cookie encryption key
- if ((obj.config) && (obj.config.settings) && (typeof obj.config.settings.logincookieencryptionkey == 'string')) {
- // We have a string, hash it and use that as a key
- try { obj.loginCookieEncryptionKey = Buffer.from(obj.config.settings.logincookieencryptionkey, 'hex'); } catch (ex) { }
- if ((obj.loginCookieEncryptionKey == null) || (obj.loginCookieEncryptionKey.length != 80)) { addServerWarning("Invalid \"LoginCookieEncryptionKey\" in config.json.", 20); obj.loginCookieEncryptionKey = null; }
- }
-
- // Login cookie encryption key not set, use one from the database
- if (obj.loginCookieEncryptionKey == null) {
- obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
- if ((docs != null) && (docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 160)) {
- obj.loginCookieEncryptionKey = Buffer.from(docs[0].key, 'hex');
- } else {
- obj.loginCookieEncryptionKey = obj.generateCookieKey(); obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() });
- }
- });
- }
-
- // Load the invitation link encryption key from the database
- obj.db.Get('InvitationLinkEncryptionKey', function (err, docs) {
- if ((docs != null) && (docs.length > 0) && (docs[0].key != null) && (docs[0].key.length >= 160)) {
- obj.invitationLinkEncryptionKey = Buffer.from(docs[0].key, 'hex');
- } else {
- obj.invitationLinkEncryptionKey = obj.generateCookieKey(); obj.db.Set({ _id: 'InvitationLinkEncryptionKey', key: obj.invitationLinkEncryptionKey.toString('hex'), time: Date.now() });
- }
- });
-
- // Setup Intel AMT hello server
- if ((typeof config.settings.amtprovisioningserver == 'object') && (typeof config.settings.amtprovisioningserver.devicegroup == 'string') && (typeof config.settings.amtprovisioningserver.newmebxpassword == 'string') && (typeof config.settings.amtprovisioningserver.trustedfqdn == 'string') && (typeof config.settings.amtprovisioningserver.ip == 'string')) {
- obj.amtProvisioningServer = require('./amtprovisioningserver').CreateAmtProvisioningServer(obj, config.settings.amtprovisioningserver);
- }
-
- // Start collecting server stats every 5 minutes
- obj.trafficStats = obj.webserver.getTrafficStats();
- setInterval(function () {
- obj.serverStatsCounter++;
- var hours = 720; // Start with all events lasting 30 days.
- if (((obj.serverStatsCounter) % 2) == 1) { hours = 3; } // Half of the event get removed after 3 hours.
- else if ((Math.floor(obj.serverStatsCounter / 2) % 2) == 1) { hours = 8; } // Another half of the event get removed after 8 hours.
- else if ((Math.floor(obj.serverStatsCounter / 4) % 2) == 1) { hours = 24; } // Another half of the event get removed after 24 hours.
- else if ((Math.floor(obj.serverStatsCounter / 8) % 2) == 1) { hours = 48; } // Another half of the event get removed after 48 hours.
- else if ((Math.floor(obj.serverStatsCounter / 16) % 2) == 1) { hours = 72; } // Another half of the event get removed after 72 hours.
- const expire = new Date();
- expire.setTime(expire.getTime() + (60 * 60 * 1000 * hours));
-
- // Get traffic data
- var trafficStats = obj.webserver.getTrafficDelta(obj.trafficStats);
- obj.trafficStats = trafficStats.current;
-
- var data = {
- time: new Date(),
- expire: expire,
- mem: process.memoryUsage(),
- conn: {
- ca: Object.keys(obj.webserver.wsagents).length,
- cu: Object.keys(obj.webserver.wssessions).length,
- us: Object.keys(obj.webserver.wssessions2).length,
- rs: obj.webserver.relaySessionCount,
- am: 0
- },
- traffic: trafficStats.delta
- };
- try { data.cpu = require('os').loadavg(); } catch (ex) { }
- if (obj.mpsserver != null) {
- data.conn.amc = 0;
- for (var i in obj.mpsserver.ciraConnections) { data.conn.amc += obj.mpsserver.ciraConnections[i].length; }
- }
- for (var i in obj.connectivityByNode) {
- const node = obj.connectivityByNode[i];
- if (node && typeof node.connectivity !== 'undefined' && node.connectivity === 4) { data.conn.am++; }
- }
- if (obj.firstStats === true) { delete obj.firstStats; data.first = true; }
- if (obj.multiServer != null) { data.s = obj.multiServer.serverid; }
- obj.db.SetServerStats(data); // Save the stats to the database
- obj.DispatchEvent(['*'], obj, { action: 'servertimelinestats', data: data }); // Event the server stats
- }, 300000);
-
- obj.debug('main', "Server started");
- if (obj.args.nousers == true) { obj.updateServerState('nousers', '1'); }
- obj.updateServerState('state', "running");
-
- // Setup auto-backup defaults. Unless autobackup is set to false try to make a backup.
- if (obj.config.settings.autobackup == false || obj.config.settings.autobackup == 'false') { obj.config.settings.autobackup = {backupintervalhours: -1}; } //block all autobackup functions
- else {
- if (typeof obj.config.settings.autobackup != 'object') { obj.config.settings.autobackup = {}; };
- if (typeof obj.config.settings.autobackup.backupintervalhours != 'number') { obj.config.settings.autobackup.backupintervalhours = 24; };
- if (typeof obj.config.settings.autobackup.keeplastdaysbackup != 'number') { obj.config.settings.autobackup.keeplastdaysbackup = 10; };
- if (obj.config.settings.autobackup.backuphour != null ) { obj.config.settings.autobackup.backupintervalhours = 24; if ((typeof obj.config.settings.autobackup.backuphour != 'number') || (obj.config.settings.autobackup.backuphour > 23 || obj.config.settings.autobackup.backuphour < 0 )) { obj.config.settings.autobackup.backuphour = 0; }}
- else {obj.config.settings.autobackup.backuphour = -1 };
- //arrayfi in case of string and remove possible ', ' space. !! If a string instead of an array is passed, it will be split by ',' so *{.txt,.log} won't work in that case !!
- if (!obj.config.settings.autobackup.backupignorefilesglob) {obj.config.settings.autobackup.backupignorefilesglob = []}
- else if (typeof obj.config.settings.autobackup.backupignorefilesglob == 'string') { obj.config.settings.autobackup.backupignorefilesglob = obj.config.settings.autobackup.backupignorefilesglob.replaceAll(', ', ',').split(','); };
- if (!obj.config.settings.autobackup.backupskipfoldersglob) {obj.config.settings.autobackup.backupskipfoldersglob = []}
- else if (typeof obj.config.settings.autobackup.backupskipfoldersglob == 'string') { obj.config.settings.autobackup.backupskipfoldersglob = obj.config.settings.autobackup.backupskipfoldersglob.replaceAll(', ', ',').split(','); };
- if (typeof obj.config.settings.autobackup.backuppath == 'string') { obj.backuppath = (obj.config.settings.autobackup.backuppath = (obj.path.resolve(obj.config.settings.autobackup.backuppath))) } else { obj.config.settings.autobackup.backuppath = obj.backuppath };
- if (typeof obj.config.settings.autobackup.backupname != 'string') { obj.config.settings.autobackup.backupname = 'meshcentral-autobackup-'};
- if (typeof obj.config.settings.autobackup.webdav == 'object') {
- //make webdav compliant: http://www.webdav.org/specs/rfc4918.html#rfc.section.5.2, http://www.webdav.org/specs/rfc2518.html#METHOD_MKCOL
- // So with leading and trailing slash in the foldername, and no double and backslashes
- if (typeof obj.config.settings.autobackup.webdav.foldername != 'string') {obj.config.settings.autobackup.webdav.foldername = '/MeshCentral-Backups/'}
- else {obj.config.settings.autobackup.webdav.foldername = ('/' + obj.config.settings.autobackup.webdav.foldername + '/').replaceAll("\\", "/").replaceAll("//", "/").replaceAll("//", "/")};
- }
- }
-
- // Check if the database is capable of performing a backup
- obj.db.checkBackupCapability(function (err, msg) { if (msg != null) { obj.addServerWarning(msg, true) } });
-
- // Load Intel AMT passwords from the "amtactivation.log" file
- obj.loadAmtActivationLogPasswords(function (amtPasswords) {
- obj.amtPasswords = amtPasswords;
- });
-
- // Setup users that can see all device groups
- if (typeof obj.config.settings.managealldevicegroups == 'string') { obj.config.settings.managealldevicegroups = obj.config.settings.managealldevicegroups.split(','); }
- else if (Array.isArray(obj.config.settings.managealldevicegroups) == false) { obj.config.settings.managealldevicegroups = []; }
- for (i in obj.config.domains) {
- if (Array.isArray(obj.config.domains[i].managealldevicegroups)) {
- for (var j in obj.config.domains[i].managealldevicegroups) {
- if (typeof obj.config.domains[i].managealldevicegroups[j] == 'string') {
- const u = 'user/' + i + '/' + obj.config.domains[i].managealldevicegroups[j];
- if (obj.config.settings.managealldevicegroups.indexOf(u) == -1) { obj.config.settings.managealldevicegroups.push(u); }
- }
- }
- }
- }
- obj.config.settings.managealldevicegroups.sort();
-
- // Start watchdog timer if needed
- // This is used to monitor if NodeJS is servicing IO correctly or getting held up a lot. Add this line to the settings section of config.json
- // "watchDog": { "interval": 100, "timeout": 150 }
- // This will check every 100ms, if the timer is more than 150ms late, it will warn.
- if ((typeof config.settings.watchdog == 'object') && (typeof config.settings.watchdog.interval == 'number') && (typeof config.settings.watchdog.timeout == 'number') && (config.settings.watchdog.interval >= 50) && (config.settings.watchdog.timeout >= 50)) {
- obj.watchdogtime = Date.now();
- obj.watchdogmax = 0;
- obj.watchdogmaxtime = null;
- obj.watchdogtable = [];
- obj.watchdog = setInterval(function () {
- const now = Date.now(), delta = now - obj.watchdogtime - config.settings.watchdog.interval;
- if (delta > obj.watchdogmax) { obj.watchdogmax = delta; obj.watchdogmaxtime = new Date().toLocaleString(); }
- if (delta > config.settings.watchdog.timeout) {
- const msg = obj.common.format("Watchdog timer timeout, {0}ms.", delta);
- obj.watchdogtable.push(new Date().toLocaleString() + ', ' + delta + 'ms');
- while (obj.watchdogtable.length > 10) { obj.watchdogtable.shift(); }
- obj.debug('main', msg);
- try {
- var errlogpath = null;
- if (typeof obj.args.mesherrorlogpath == 'string') { errlogpath = obj.path.join(obj.args.mesherrorlogpath, 'mesherrors.txt'); } else { errlogpath = obj.getConfigFilePath('mesherrors.txt'); }
- obj.fs.appendFileSync(errlogpath, new Date().toLocaleString() + ': ' + msg + '\r\n');
- } catch (ex) { console.log('ERROR: Unable to write to mesherrors.txt.'); }
- }
- obj.watchdogtime = now;
- }, config.settings.watchdog.interval);
- obj.debug('main', "Started watchdog timer.");
- }
-
- });
- });
- });
- };
-
- // Called when the web server finished loading
- obj.StartEx5 = function () {
- // Setup the email server for each domain
- var ipKvmSupport = false;
- for (var i in obj.config.domains) { if (obj.config.domains[i].ipkvm == true) { ipKvmSupport = true; } }
- if (ipKvmSupport) { obj.ipKvmManager = require('./meshipkvm').CreateIPKVMManager(obj); }
-
- // Run the server start script if present
- if (typeof obj.config.settings.runonserverstarted == 'string') {
- const child_process = require('child_process');
- var parentpath = __dirname;
- if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) { parentpath = require('path').join(__dirname, '../..'); }
- child_process.exec(obj.config.settings.runonserverstarted + ' ' + getCurrentVersion(), { maxBuffer: 512000, timeout: 120000, cwd: parentpath }, function (error, stdout, stderr) { });
- }
- }
-
- // Refresh any certificate hashs from the reverse proxy
- obj.pendingProxyCertificatesRequests = 0;
- obj.lastProxyCertificatesRequest = null;
- obj.supportsProxyCertificatesRequest = false;
- obj.updateProxyCertificates = function (force) {
- if (force !== true) {
- if ((obj.pendingProxyCertificatesRequests > 0) || (obj.supportsProxyCertificatesRequest == false)) return;
- if ((obj.lastProxyCertificatesRequest != null) && ((Date.now() - obj.lastProxyCertificatesRequest) < 120000)) return; // Don't allow this call more than every 2 minutes.
- obj.lastProxyCertificatesRequest = Date.now();
- }
-
- // Load any domain web certificates
- for (var i in obj.config.domains) {
- if (obj.config.domains[i].certurl != null) {
- // Load web certs
- obj.pendingProxyCertificatesRequests++;
- var dnsname = obj.config.domains[i].dns;
- if ((dnsname == null) && (obj.config.settings.cert != null)) { dnsname = obj.config.settings.cert; }
- obj.certificateOperations.loadCertificate(obj.config.domains[i].certurl, dnsname, obj.config.domains[i], function (url, cert, xhostname, xdomain) {
- obj.pendingProxyCertificatesRequests--;
- if (cert != null) {
- // Hash the entire cert
- const hash = obj.crypto.createHash('sha384').update(Buffer.from(cert, 'binary')).digest('hex');
- if (xdomain.certhash != hash) { // The certificate has changed.
- xdomain.certkeyhash = hash;
- xdomain.certhash = hash;
-
- try {
- // Decode a RSA certificate and hash the public key, if this is not RSA, skip this.
- const forgeCert = obj.certificateOperations.forge.pki.certificateFromAsn1(obj.certificateOperations.forge.asn1.fromDer(cert));
- xdomain.certkeyhash = obj.certificateOperations.forge.pki.getPublicKeyFingerprint(forgeCert.publicKey, { md: obj.certificateOperations.forge.md.sha384.create(), encoding: 'hex' });
- obj.webserver.webCertificateExpire[xdomain.id] = Date.parse(forgeCert.validity.notAfter); // Update certificate expire time
- //console.log('V1: ' + xdomain.certkeyhash);
- } catch (ex) {
- delete obj.webserver.webCertificateExpire[xdomain.id]; // Remove certificate expire time
- delete xdomain.certkeyhash;
- }
-
- if (obj.webserver) {
- obj.webserver.webCertificateHashs[xdomain.id] = obj.webserver.webCertificateFullHashs[xdomain.id] = Buffer.from(hash, 'hex').toString('binary');
- if (xdomain.certkeyhash != null) { obj.webserver.webCertificateHashs[xdomain.id] = Buffer.from(xdomain.certkeyhash, 'hex').toString('binary'); }
-
- // Disconnect all agents with bad web certificates
- for (var i in obj.webserver.wsagentsWithBadWebCerts) { obj.webserver.wsagentsWithBadWebCerts[i].close(1); }
- }
-
- console.log(obj.common.format("Loaded web certificate from \"{0}\", host: \"{1}\"", url, xhostname));
- console.log(obj.common.format(" SHA384 cert hash: {0}", xdomain.certhash));
- if ((xdomain.certkeyhash != null) && (xdomain.certhash != xdomain.certkeyhash)) { console.log(obj.common.format(" SHA384 key hash: {0}", xdomain.certkeyhash)); }
- }
- } else {
- console.log(obj.common.format("Failed to load web certificate at: \"{0}\", host: \"{1}\"", url, xhostname));
- }
- });
- }
- }
- }
-
- // Perform maintenance operations (called every hour)
- obj.maintenanceActions = function () {
- // Perform database maintenance
- obj.db.maintenance();
-
- // Clean up any temporary files
- const removeTime = new Date(Date.now()).getTime() - (30 * 60 * 1000); // 30 minutes
- const dir = obj.fs.readdir(obj.path.join(obj.filespath, 'tmp'), function (err, files) {
- if (err != null) return;
- for (var i in files) { try { const filepath = obj.path.join(obj.filespath, 'tmp', files[i]); if (obj.fs.statSync(filepath).mtime.getTime() < removeTime) { obj.fs.unlink(filepath, function () { }); } } catch (ex) { } }
- });
-
- // Check for self-update that targets a specific version
- if ((typeof obj.args.selfupdate == 'string') && (getCurrentVersion() === obj.args.selfupdate)) { obj.args.selfupdate = false; }
-
- // Check if we need to perform server self-update
- if ((obj.args.selfupdate) && (obj.serverSelfWriteAllowed == true)) {
- obj.db.getValueOfTheDay('performSelfUpdate', 1, function (performSelfUpdate) {
- if (performSelfUpdate.value > 0) {
- performSelfUpdate.value--;
- obj.db.Set(performSelfUpdate);
- obj.getLatestServerVersion(function (currentVer, latestVer) { if (currentVer != latestVer) { obj.performServerUpdate(); return; } });
- } else {
- checkAutobackup();
- }
- });
- } else {
- checkAutobackup();
- }
- };
-
- // Check if we need to perform an automatic backup
- function checkAutobackup() {
- if (obj.config.settings.autobackup.backupintervalhours >= 1 ) {
- obj.db.Get('LastAutoBackupTime', function (err, docs) {
- if (err != null) { console.error("checkAutobackup: Error getting LastBackupTime from DB"); return}
- var lastBackup = 0;
- const currentdate = new Date();
- let currentHour = currentdate.getHours();
- let now = currentdate.getTime();
- if (docs.length == 1) { lastBackup = docs[0].value; }
- const delta = now - lastBackup;
- //const delta = 9999999999; // DEBUG: backup always
- obj.debug ('backup', 'Entering checkAutobackup, lastAutoBackupTime: ' + new Date(lastBackup).toLocaleString('default', { dateStyle: 'medium', timeStyle: 'short' }) + ', delta: ' + (delta/(1000*60*60)).toFixed(2) + ' hours');
- //start autobackup if interval has passed or at configured hour, whichever comes first. When an hour schedule is missed, it will make a backup immediately.
- if ((delta > (obj.config.settings.autobackup.backupintervalhours * 60 * 60 * 1000)) || ((currentHour == obj.config.settings.autobackup.backuphour) && (delta >= 2 * 60 * 60 * 1000))) {
- // A new auto-backup is required.
- obj.db.Set({ _id: 'LastAutoBackupTime', value: now }); // Save the current time in the database
- obj.db.performBackup(); // Perform the backup
- }
- });
- }
- }
-
- // Stop the Meshcentral server
- obj.Stop = function (restoreFile) {
- // If the database is not setup, exit now.
- if (!obj.db) return;
-
- // Dispatch an event saying the server is now stopping
- obj.DispatchEvent(['*'], obj, { etype: 'server', action: 'stopped', msg: "Server stopped" });
-
- // Set all nodes to power state of unknown (0)
- obj.db.storePowerEvent({ time: new Date(), nodeid: '*', power: 0, s: 2 }, obj.multiServer, function () { // s:2 indicates that the server is shutting down.
- if (restoreFile) {
- obj.debug('main', obj.common.format("Server stopped, updating settings: {0}", restoreFile));
- console.log("Updating settings folder...");
-
- const yauzl = require('yauzl');
- yauzl.open(restoreFile, { lazyEntries: true }, function (err, zipfile) {
- if (err) throw err;
- zipfile.readEntry();
- zipfile.on('entry', function (entry) {
- if (/\/$/.test(entry.fileName)) {
- // Directory file names end with '/'.
- // Note that entires for directories themselves are optional.
- // An entry's fileName implicitly requires its parent directories to exist.
- zipfile.readEntry();
- } else {
- // File entry
- zipfile.openReadStream(entry, function (err, readStream) {
- if (err) throw err;
- readStream.on('end', function () { zipfile.readEntry(); });
- var directory = obj.path.dirname(entry.fileName);
- if (directory != '.') {
- directory = obj.getConfigFilePath(directory)
- if (obj.fs.existsSync(directory) == false) { obj.fs.mkdirSync(directory); }
- }
- //console.log('Extracting:', obj.getConfigFilePath(entry.fileName));
- readStream.pipe(obj.fs.createWriteStream(obj.getConfigFilePath(entry.fileName)));
- });
- }
- });
- zipfile.on('end', function () { setTimeout(function () { obj.fs.unlinkSync(restoreFile); process.exit(123); }); });
- });
- } else {
- obj.debug('main', "Server stopped");
- process.exit(0);
- }
- });
-
- // Update the server state
- obj.updateServerState('state', "stopped");
- };
-
- // Event Dispatch
- obj.AddEventDispatch = function (ids, target) {
- obj.debug('dispatch', 'AddEventDispatch', ids);
- for (var i in ids) { var id = ids[i]; if (!obj.eventsDispatch[id]) { obj.eventsDispatch[id] = [target]; } else { obj.eventsDispatch[id].push(target); } }
- };
- obj.RemoveEventDispatch = function (ids, target) {
- obj.debug('dispatch', 'RemoveEventDispatch', ids);
- for (var i in ids) {
- const id = ids[i];
- if (obj.eventsDispatch[id]) {
- var j = obj.eventsDispatch[id].indexOf(target);
- if (j >= 0) {
- if (obj.eventsDispatch[id].length == 1) {
- delete obj.eventsDispatch[id];
- } else {
- const newList = []; // We create a new list so not to modify the original list. Allows this function to be called during an event dispatch.
- for (var k in obj.eventsDispatch[i]) { if (obj.eventsDispatch[i][k] != target) { newList.push(obj.eventsDispatch[i][k]); } }
- obj.eventsDispatch[i] = newList;
- }
- }
- }
- }
- };
- obj.RemoveEventDispatchId = function (id) {
- obj.debug('dispatch', 'RemoveEventDispatchId', id);
- if (obj.eventsDispatch[id] != null) { delete obj.eventsDispatch[id]; }
- };
- obj.RemoveAllEventDispatch = function (target) {
- obj.debug('dispatch', 'RemoveAllEventDispatch');
- for (var i in obj.eventsDispatch) {
- const j = obj.eventsDispatch[i].indexOf(target);
- if (j >= 0) {
- if (obj.eventsDispatch[i].length == 1) {
- delete obj.eventsDispatch[i];
- } else {
- const newList = []; // We create a new list so not to modify the original list. Allows this function to be called during an event dispatch.
- for (var k in obj.eventsDispatch[i]) { if (obj.eventsDispatch[i][k] != target) { newList.push(obj.eventsDispatch[i][k]); } }
- obj.eventsDispatch[i] = newList;
- }
- }
- }
- };
- obj.DispatchEvent = function (ids, source, event, fromPeerServer) {
- // If the database is not setup, exit now.
- if (!obj.db) return;
-
- // Send event to syslog if needed
- if (obj.syslog && event.msg) { obj.syslog.log(obj.syslog.LOG_INFO, event.msg); }
- if (obj.syslogjson) { obj.syslogjson.log(obj.syslogjson.LOG_INFO, JSON.stringify(event)); }
- if (obj.syslogtcp && event.msg) { obj.syslogtcp.log(event.msg, obj.syslogtcp.LOG_INFO); }
-
- obj.debug('dispatch', 'DispatchEvent', ids);
- if ((typeof event == 'object') && (!event.nolog)) {
- event.time = new Date();
- // The event we store is going to skip some of the fields so we don't store too much stuff in the database.
- const storeEvent = Object.assign({}, event);
- if (storeEvent.node) { delete storeEvent.node; } // Skip the "node" field. May skip more in the future.
- if (storeEvent.links) {
- // Escape "links" names that may have "." and/or "$"
- storeEvent.links = Object.assign({}, storeEvent.links);
- for (var i in storeEvent.links) { var ue = obj.common.escapeFieldName(i); if (ue !== i) { storeEvent.links[ue] = storeEvent.links[i]; delete storeEvent.links[i]; } }
- }
- if (storeEvent.mesh) {
- // Escape "mesh" names that may have "." and/or "$"
- storeEvent.mesh = obj.common.escapeLinksFieldNameEx(storeEvent.mesh);
- }
- storeEvent.ids = ids;
- obj.db.StoreEvent(storeEvent);
- }
- const targets = []; // List of targets we dispatched the event to, we don't want to dispatch to the same target twice.
- for (var j in ids) {
- const id = ids[j];
- const eventsDispatch = obj.eventsDispatch[id];
- if (eventsDispatch) {
- for (var i in eventsDispatch) {
- if (targets.indexOf(eventsDispatch[i]) == -1) { // Check if we already displatched to this target
- targets.push(eventsDispatch[i]);
- try { eventsDispatch[i].HandleEvent(source, event, ids, id); } catch (ex) { console.log(ex, eventsDispatch[i]); }
- }
- }
- }
- }
- if ((fromPeerServer == null) && (obj.multiServer != null) && ((typeof event != 'object') || (event.nopeers != 1))) { obj.multiServer.DispatchEvent(ids, source, event); }
- };
-
- // Get the connection state of a node
- obj.GetConnectivityState = function (nodeid) { return obj.connectivityByNode[nodeid]; };
-
- // Get the routing server id for a given node and connection type, can never be self.
- obj.GetRoutingServerIdNotSelf = function (nodeid, connectType) {
- if (obj.multiServer == null) return null;
- for (var serverid in obj.peerConnectivityByNode) {
- if (serverid == obj.serverId) continue;
- var state = obj.peerConnectivityByNode[serverid][nodeid];
- if ((state != null) && ((state.connectivity & connectType) != 0)) { return { serverid: serverid, meshid: state.meshid }; }
- }
- return null;
- };
-
- // Get the routing server id for a given node and connection type, self first
- obj.GetRoutingServerId = function (nodeid, connectType) {
- if (obj.multiServer == null) return null;
-
- // Look at our own server first
- var connections = obj.peerConnectivityByNode[obj.serverId];
- if (connections != null) {
- var state = connections[nodeid];
- if ((state != null) && ((state.connectivity & connectType) != 0)) { return { serverid: obj.serverId, meshid: state.meshid }; }
- }
-
- // Look at other servers
- for (var serverid in obj.peerConnectivityByNode) {
- if (serverid == obj.serverId) continue;
- var state = obj.peerConnectivityByNode[serverid][nodeid];
- if ((state != null) && ((state.connectivity & connectType) != 0)) { return { serverid: serverid, meshid: state.meshid }; }
- }
- return null;
- };
-
- // Update the connection state of a node when in multi-server mode
- // Update obj.connectivityByNode using obj.peerConnectivityByNode for the list of nodes in argument
- obj.UpdateConnectivityState = function (nodeids) {
- for (var nodeid in nodeids) {
- var meshid = null, state = null, oldConnectivity = 0, oldPowerState = 0, newConnectivity = 0, newPowerState = 0;
- var oldState = obj.connectivityByNode[nodeid];
- if (oldState != null) { meshid = oldState.meshid; oldConnectivity = oldState.connectivity; oldPowerState = oldState.powerState; }
- for (var serverid in obj.peerConnectivityByNode) {
- var peerState = obj.peerConnectivityByNode[serverid][nodeid];
- if (peerState != null) {
- if (state == null) {
- // Copy the state
- state = {};
- newConnectivity = state.connectivity = peerState.connectivity;
- newPowerState = state.powerState = peerState.powerState;
- meshid = state.meshid = peerState.meshid;
- //if (peerState.agentPower) { state.agentPower = peerState.agentPower; }
- //if (peerState.ciraPower) { state.ciraPower = peerState.ciraPower; }
- //if (peerState.amtPower) { state.amtPower = peerState.amtPower; }
- } else {
- // Merge the state
- state.connectivity |= peerState.connectivity;
- newConnectivity = state.connectivity;
- if ((peerState.powerState != 0) && ((state.powerState == 0) || (peerState.powerState < state.powerState))) { newPowerState = state.powerState = peerState.powerState; }
- meshid = state.meshid = peerState.meshid;
- //if (peerState.agentPower) { state.agentPower = peerState.agentPower; }
- //if (peerState.ciraPower) { state.ciraPower = peerState.ciraPower; }
- //if (peerState.amtPower) { state.amtPower = peerState.amtPower; }
- }
- }
- }
- obj.connectivityByNode[nodeid] = state;
-
- //console.log('xx', nodeid, meshid, newConnectivity, oldPowerState, newPowerState, oldPowerState);
-
- // Event any changes on this server only
- if ((newConnectivity != oldPowerState) || (newPowerState != oldPowerState)) {
- obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: newConnectivity, pwr: newPowerState, nolog: 1, nopeers: 1, id: Math.random() });
- }
- }
- };
-
- // See if we need to notifiy any user of device state change
- obj.NotifyUserOfDeviceStateChange = function (meshid, nodeid, connectTime, connectType, powerState, serverid, stateSet, extraInfo) {
- // Check if there is a email server for this domain
- const meshSplit = meshid.split('/');
- if (meshSplit.length != 3) return;
- const domainId = meshSplit[1];
- if (obj.config.domains[domainId] == null) return;
- const mailserver = obj.config.domains[domainId].mailserver;
- if ((mailserver == null) && (obj.msgserver == null)) return;
-
- // Get the device group for this device
- const mesh = obj.webserver.meshes[meshid];
- if ((mesh == null) || (mesh.links == null)) return;
-
- // Get the list of users that have visibility to this device
- // This includes users that are part of user groups
- const users = [];
- for (var i in mesh.links) {
- if (i.startsWith('user/') && (users.indexOf(i) < 0)) { users.push(i); }
- if (i.startsWith('ugrp/')) {
- var usergrp = obj.webserver.userGroups[i];
- if (usergrp.links != null) { for (var j in usergrp.links) { if (j.startsWith('user/') && (users.indexOf(j) < 0)) { users.push(j); } } }
- }
- }
-
- // Check if any user needs email notification
- for (var i in users) {
- const user = obj.webserver.users[users[i]];
- if (user != null) {
- var notify = 0;
-
- // Device group notifications
- const meshLinks = user.links[meshid];
- if ((meshLinks != null) && (meshLinks.notify != null)) { notify |= meshLinks.notify; }
-
- // User notifications
- if (user.notify != null) {
- if (user.notify[meshid] != null) { notify |= user.notify[meshid]; }
- if (user.notify[nodeid] != null) { notify |= user.notify[nodeid]; }
- }
-
- // Email notifications
- if ((user.email != null) && (user.emailVerified == true) && (mailserver != null) && ((notify & 48) != 0)) {
- if (stateSet == true) {
- if ((notify & 16) != 0) {
- mailserver.notifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
- } else {
- mailserver.cancelNotifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
- }
- }
- else if (stateSet == false) {
- if ((notify & 32) != 0) {
- mailserver.notifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
- } else {
- mailserver.cancelNotifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
- }
- }
- }
-
- // Messaging notifications
- if ((obj.msgserver != null) && ((notify & 384) != 0)) {
- if (stateSet == true) {
- if ((notify & 128) != 0) {
- obj.msgserver.notifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
- } else {
- obj.msgserver.cancelNotifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
- }
- }
- else if (stateSet == false) {
- if ((notify & 256) != 0) {
- obj.msgserver.notifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
- } else {
- obj.msgserver.cancelNotifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
- }
- }
- }
- }
- }
- }
-
- // See if we need to notifiy any user of device requested help
- //if (typeof device.name == 'string') { parent.parent.NotifyUserOfDeviceHelpRequest(domain, device._id, device.meshid, device.name, command.msgArgs[0], command.msgArgs[1]); }
-
- obj.NotifyUserOfDeviceHelpRequest = function (domain, meshid, nodeid, devicename, helpusername, helprequest) {
- // Check if there is a email server for this domain
- const meshSplit = meshid.split('/');
- if (meshSplit.length != 3) return;
- const domainId = meshSplit[1];
- if (obj.config.domains[domainId] == null) return;
- const mailserver = obj.config.domains[domainId].mailserver;
- if ((mailserver == null) && (obj.msgserver == null)) return;
-
- // Get the device group for this device
- const mesh = obj.webserver.meshes[meshid];
- if ((mesh == null) || (mesh.links == null)) return;
-
- // Get the list of users that have visibility to this device
- // This includes users that are part of user groups
- const users = [];
- for (var i in mesh.links) {
- if (i.startsWith('user/') && (users.indexOf(i) < 0)) { users.push(i); }
- if (i.startsWith('ugrp/')) {
- var usergrp = obj.webserver.userGroups[i];
- if (usergrp.links != null) { for (var j in usergrp.links) { if (j.startsWith('user/') && (users.indexOf(j) < 0)) { users.push(j); } } }
- }
- }
-
- // Check if any user needs email notification
- for (var i in users) {
- const user = obj.webserver.users[users[i]];
- if (user != null) {
- var notify = 0;
-
- // Device group notifications
- const meshLinks = user.links[meshid];
- if ((meshLinks != null) && (meshLinks.notify != null)) { notify |= meshLinks.notify; }
-
- // User notifications
- if (user.notify != null) {
- if (user.notify[meshid] != null) { notify |= user.notify[meshid]; }
- if (user.notify[nodeid] != null) { notify |= user.notify[nodeid]; }
- }
-
- // Mail help request
- if ((user.email != null) && (user.emailVerified == true) && ((notify & 64) != 0)) { mailserver.sendDeviceHelpMail(domain, user.name, user.email, devicename, nodeid, helpusername, helprequest, user.llang); }
-
- // Message help request
- if ((user.msghandle != null) && ((notify & 512) != 0)) { obj.msgserver.sendDeviceHelpRequest(domain, user.name, user.msghandle, devicename, nodeid, helpusername, helprequest, user.llang); }
- }
- }
- }
-
- // Set the connectivity state of a node and setup the server so that messages can be routed correctly.
- // meshId: mesh identifier of format mesh/domain/meshidhex
- // nodeId: node identifier of format node/domain/nodeidhex
- // connectTime: time of connection, milliseconds elapsed since the UNIX epoch.
- // connectType: Bitmask, 1 = MeshAgent, 2 = Intel AMT CIRA, 4 = Intel AMT local, 8 = Intel AMT Relay, 16 = MQTT
- // powerState: Value, 0 = Unknown, 1 = S0 power on, 2 = S1 Sleep, 3 = S2 Sleep, 4 = S3 Sleep, 5 = S4 Hibernate, 6 = S5 Soft-Off, 7 = Present, 8 = Off
- //var connectTypeStrings = ['', 'MeshAgent', 'Intel AMT CIRA', '', 'Intel AMT local', '', '', '', 'Intel AMT Relay', '', '', '', '', '', '', '', 'MQTT'];
- //var powerStateStrings = ['Unknown', 'Powered', 'Sleep', 'Sleep', 'Deep Sleep', 'Hibernating', 'Soft-Off', 'Present', 'Off'];
- obj.SetConnectivityState = function (meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
- //console.log('SetConnectivity for ' + nodeid.substring(0, 16) + ', Type: ' + connectTypeStrings[connectType] + ', Power: ' + powerStateStrings[powerState] + (serverid == null ? ('') : (', ServerId: ' + serverid)));
- if ((serverid == null) && (obj.multiServer != null)) { obj.multiServer.DispatchMessage({ action: 'SetConnectivityState', meshid: meshid, nodeid: nodeid, connectTime: connectTime, connectType: connectType, powerState: powerState, extraInfo: extraInfo }); }
-
- if (obj.multiServer == null) {
- // Single server mode
-
- // Change the node connection state
- var eventConnectChange = 0;
- var state = obj.connectivityByNode[nodeid];
- if (state) {
- // Change the connection in the node and mesh state lists
- if ((state.connectivity & connectType) == 0) { state.connectivity |= connectType; eventConnectChange = 1; }
- state.meshid = meshid;
- } else {
- // Add the connection to the node and mesh state list
- obj.connectivityByNode[nodeid] = state = { connectivity: connectType, meshid: meshid };
- eventConnectChange = 1;
- }
-
- // Set node power state
- if (connectType == 1) { state.agentPower = powerState; } else if (connectType == 2) { state.ciraPower = powerState; } else if (connectType == 4) { state.amtPower = powerState; }
- var powerState = 0, oldPowerState = state.powerState;
- if ((state.connectivity & 1) != 0) { powerState = state.agentPower; } else if ((state.connectivity & 2) != 0) { powerState = state.ciraPower; } else if ((state.connectivity & 4) != 0) { powerState = state.amtPower; }
- if ((state.powerState == null) || (state.powerState == undefined) || (state.powerState != powerState)) {
- state.powerState = powerState;
- eventConnectChange = 1;
-
- // Set new power state in database
- const record = { time: new Date(connectTime), nodeid: nodeid, power: powerState };
- if (oldPowerState != null) { record.oldPower = oldPowerState; }
- obj.db.storePowerEvent(record, obj.multiServer);
- }
-
- // Event the node connection change
- if (eventConnectChange == 1) {
- obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: state.connectivity, pwr: state.powerState, ct: connectTime, nolog: 1, nopeers: 1, id: Math.random() });
-
- // Save indication of node connection change
- const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 1, connectType: connectType };
- if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
- obj.db.Set(lc);
-
- // Notify any users of device connection
- obj.NotifyUserOfDeviceStateChange(meshid, nodeid, connectTime, connectType, powerState, serverid, true, extraInfo);
- }
- } else {
- // Multi server mode
-
- // Change the node connection state
- if (serverid == null) { serverid = obj.serverId; }
- if (obj.peerConnectivityByNode[serverid] == null) return; // Guard against unknown serverid's
- var eventConnectChange = 0;
- var state = obj.peerConnectivityByNode[serverid][nodeid];
- if (state) {
- // Change the connection in the node and mesh state lists
- if ((state.connectivity & connectType) == 0) { state.connectivity |= connectType; eventConnectChange = 1; }
- state.meshid = meshid;
- } else {
- // Add the connection to the node and mesh state list
- obj.peerConnectivityByNode[serverid][nodeid] = state = { connectivity: connectType, meshid: meshid };
- eventConnectChange = 1;
- }
-
- // Set node power state
- if (connectType == 1) { state.agentPower = powerState; } else if (connectType == 2) { state.ciraPower = powerState; } else if (connectType == 4) { state.amtPower = powerState; }
- var powerState = 0, oldPowerState = state.powerState;
- if ((state.connectivity & 1) != 0) { powerState = state.agentPower; } else if ((state.connectivity & 2) != 0) { powerState = state.ciraPower; } else if ((state.connectivity & 4) != 0) { powerState = state.amtPower; }
- if ((state.powerState == null) || (state.powerState == undefined) || (state.powerState != powerState)) {
- state.powerState = powerState;
- eventConnectChange = 1;
-
- // Set new power state in database
- var record = { time: new Date(connectTime), nodeid: nodeid, power: powerState, server: obj.multiServer.serverid };
- if (oldPowerState != null) { record.oldPower = oldPowerState; }
- obj.db.storePowerEvent(record, obj.multiServer);
- }
-
- if (eventConnectChange == 1) {
- // Update the combined node state
- var x = {}; x[nodeid] = 1;
- obj.UpdateConnectivityState(x);
-
- // Save indication of node connection change
- if (serverid == obj.serverId) {
- const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 1, connectType: connectType, serverid: obj.serverId };
- if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
- obj.db.Set(lc);
- }
-
- // Notify any users of device connection
- obj.NotifyUserOfDeviceStateChange(meshid, nodeid, connectTime, connectType, powerState, serverid, true, extraInfo);
- }
- }
- };
-
- // Clear the connectivity state of a node and setup the server so that messages can be routed correctly.
- // meshId: mesh identifier of format mesh/domain/meshidhex
- // nodeId: node identifier of format node/domain/nodeidhex
- // connectType: Bitmask, 1 = MeshAgent, 2 = Intel AMT CIRA, 4 = Intel AMT local.
- obj.ClearConnectivityState = function (meshid, nodeid, connectType, serverid, extraInfo) {
- //console.log('ClearConnectivity for ' + nodeid.substring(0, 16) + ', Type: ' + connectTypeStrings[connectType] + (serverid == null?(''):(', ServerId: ' + serverid)));
- if ((serverid == null) && (obj.multiServer != null)) { obj.multiServer.DispatchMessage({ action: 'ClearConnectivityState', meshid: meshid, nodeid: nodeid, connectType: connectType, extraInfo: extraInfo }); }
-
- if (obj.multiServer == null) {
- // Single server mode
- var eventConnectChange = 0;
-
- // Remove the agent connection from the nodes connection list
- const state = obj.connectivityByNode[nodeid];
- if (state == null) return;
-
- if ((state.connectivity & connectType) != 0) {
- state.connectivity -= connectType;
-
- // Save indication of node connection change
- const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 0, connectType: connectType };
- if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
- obj.db.Set(lc);
-
- // If the node is completely disconnected, clean it up completely
- if (state.connectivity == 0) { delete obj.connectivityByNode[nodeid]; }
- eventConnectChange = 1;
- }
-
- // Clear node power state
- var powerState = 0;
- const oldPowerState = state.powerState;
- if (connectType == 1) { state.agentPower = 0; } else if (connectType == 2) { state.ciraPower = 0; } else if (connectType == 4) { state.amtPower = 0; }
- if ((state.connectivity & 1) != 0) { powerState = state.agentPower; } else if ((state.connectivity & 2) != 0) { powerState = state.ciraPower; } else if ((state.connectivity & 4) != 0) { powerState = state.amtPower; }
- if ((state.powerState == null) || (state.powerState != powerState)) {
- state.powerState = powerState;
- eventConnectChange = 1;
-
- // Set new power state in database
- obj.db.storePowerEvent({ time: new Date(), nodeid: nodeid, power: powerState, oldPower: oldPowerState }, obj.multiServer);
- }
-
- // Event the node connection change
- if (eventConnectChange == 1) {
- obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: state.connectivity, pwr: state.powerState, nolog: 1, nopeers: 1, id: Math.random() });
-
- // Notify any users of device disconnection
- obj.NotifyUserOfDeviceStateChange(meshid, nodeid, Date.now(), connectType, -1, serverid, false, extraInfo);
- }
- } else {
- // Multi server mode
-
- // Remove the agent connection from the nodes connection list
- if (serverid == null) { serverid = obj.serverId; }
- if (obj.peerConnectivityByNode[serverid] == null) return; // Guard against unknown serverid's
- var state = obj.peerConnectivityByNode[serverid][nodeid];
- if (state == null) return;
-
- // If existing state exist, remove this connection
- if ((state.connectivity & connectType) != 0) {
- state.connectivity -= connectType; // Remove one connectivity mode
-
- // Save indication of node connection change
- if (serverid == obj.serverId) {
- const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 0, connectType: connectType, serverid: obj.serverId };
- if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
- obj.db.Set(lc);
- }
-
- // If the node is completely disconnected, clean it up completely
- if (state.connectivity == 0) { delete obj.peerConnectivityByNode[serverid][nodeid]; state.powerState = 0; }
-
- // Notify any users of device disconnection
- obj.NotifyUserOfDeviceStateChange(meshid, nodeid, Date.now(), connectType, -1, serverid, false, extraInfo);
- }
-
- // Clear node power state
- if (connectType == 1) { state.agentPower = 0; } else if (connectType == 2) { state.ciraPower = 0; } else if (connectType == 4) { state.amtPower = 0; }
- var powerState = 0;
- if ((state.connectivity & 1) != 0) { powerState = state.agentPower; } else if ((state.connectivity & 2) != 0) { powerState = state.ciraPower; } else if ((state.connectivity & 4) != 0) { powerState = state.amtPower; }
- if ((state.powerState == null) || (state.powerState != powerState)) { state.powerState = powerState; }
-
- // Update the combined node state
- var x = {}; x[nodeid] = 1;
- obj.UpdateConnectivityState(x);
- }
- };
-
- // Escape a code string
- obj.escapeCodeString = function (str, keepUtf8) {
- const escapeCodeStringTable = { '\'': '\\\'', '\"': '\\"', '\\': '\\\\', '\b': '\\b', '\f': '\\f', '\n': '\\n', '\r': '\\r', '\t': '\\t' };
- var r = '', c, cr, table;
- for (var i = 0; i < str.length; i++) {
- c = str[i];
- table = escapeCodeStringTable[c];
- if (table != null) {
- r += table;
- } else if (keepUtf8 === true) {
- r += c;
- } else {
- cr = c.charCodeAt(0);
- if ((cr >= 32) && (cr <= 127)) { r += c; }
- }
- }
- return r;
- }
-
- // Update the default mesh core
- obj.updateMeshCore = function (func, dumpToFile) {
- // Figure out where meshcore.js is
- var meshcorePath = obj.datapath;
- if (obj.fs.existsSync(obj.path.join(meshcorePath, 'meshcore.js')) == false) {
- meshcorePath = obj.path.join(__dirname, 'agents');
- if (obj.fs.existsSync(obj.path.join(meshcorePath, 'meshcore.js')) == false) {
- obj.defaultMeshCores = obj.defaultMeshCoresHash = {}; if (func != null) { func(false); } // meshcore.js not found
- }
- }
-
- // Read meshcore.js and all .js files in the modules folder.
- var meshCore = null, modulesDir = null;
- const modulesAdd = {
- 'windows-amt': ['var addedModules = [];\r\n'],
- 'linux-amt': ['var addedModules = [];\r\n'],
- 'linux-noamt': ['var addedModules = [];\r\n']
- };
-
- // Read the recovery core if present
- var meshRecoveryCore = null;
- if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'recoverycore.js')) == true) {
- try { meshRecoveryCore = obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'recoverycore.js')).toString(); } catch (ex) { }
- if (meshRecoveryCore != null) {
- modulesAdd['windows-recovery'] = ['var addedModules = [];\r\n'];
- modulesAdd['linux-recovery'] = ['var addedModules = [];\r\n'];
- }
- }
-
- // Read the agent recovery core if present
- var meshAgentRecoveryCore = null;
- if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'meshcore_diagnostic.js')) == true) {
- try { meshAgentRecoveryCore = obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'meshcore_diagnostic.js')).toString(); } catch (ex) { }
- if (meshAgentRecoveryCore != null) {
- modulesAdd['windows-agentrecovery'] = ['var addedModules = [];\r\n'];
- modulesAdd['linux-agentrecovery'] = ['var addedModules = [];\r\n'];
- }
- }
-
- // Read the tiny core if present
- var meshTinyCore = null;
- if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'tinycore.js')) == true) {
- try { meshTinyCore = obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'tinycore.js')).toString(); } catch (ex) { }
- if (meshTinyCore != null) {
- modulesAdd['windows-tiny'] = ['var addedModules = [];\r\n'];
- modulesAdd['linux-tiny'] = ['var addedModules = [];\r\n'];
- }
- }
-
- if (obj.args.minifycore !== false) { try { meshCore = obj.fs.readFileSync(obj.path.join(meshcorePath, 'meshcore.min.js')).toString(); } catch (ex) { } } // Favor minified meshcore if present.
- if (meshCore == null) { try { meshCore = obj.fs.readFileSync(obj.path.join(meshcorePath, 'meshcore.js')).toString(); } catch (ex) { } } // Use non-minified meshcore.
- if (meshCore != null) {
- var moduleDirPath = null;
- if (obj.args.minifycore !== false) { try { moduleDirPath = obj.path.join(meshcorePath, 'modules_meshcore_min'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Favor minified modules if present.
- if (modulesDir == null) { try { moduleDirPath = obj.path.join(meshcorePath, 'modules_meshcore'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Use non-minified mofules.
- if (modulesDir != null) {
- for (var i in modulesDir) {
- if (modulesDir[i].toLowerCase().endsWith('.json')) {
- // We are adding a JSON file to the meshcores
- var moduleName = modulesDir[i].substring(0, modulesDir[i].length - 5);
- if (moduleName.endsWith('.min')) { moduleName = moduleName.substring(0, moduleName.length - 6); } // Remove the ".min" for ".min.json" files.
- const jsonData = obj.escapeCodeString(obj.fs.readFileSync(obj.path.join(moduleDirPath, modulesDir[i])).toString('utf8'), true);
- const moduleData = ['var ', moduleName, ' = JSON.parse(\'', jsonData, '\');\r\n'];
-
- // Add to all major cores
- modulesAdd['windows-amt'].push(...moduleData);
- modulesAdd['linux-amt'].push(...moduleData);
- modulesAdd['linux-noamt'].push(...moduleData);
- }
- if (modulesDir[i].toLowerCase().endsWith('.js')) {
- // We are adding a JS file to the meshcores
- var moduleName = modulesDir[i].substring(0, modulesDir[i].length - 3);
- if (moduleName.endsWith('.min')) { moduleName = moduleName.substring(0, moduleName.length - 4); } // Remove the ".min" for ".min.js" files.
- const moduleData = ['try { addModule("', moduleName, '", "', obj.escapeCodeString(obj.fs.readFileSync(obj.path.join(moduleDirPath, modulesDir[i])).toString('binary')), '"); addedModules.push("', moduleName, '"); } catch (ex) { }\r\n'];
-
- // Merge this module
- // NOTE: "smbios" module makes some non-AI Linux segfault, only include for IA platforms.
- if (moduleName.startsWith('amt-') || (moduleName == 'smbios')) {
- // Add to IA / Intel AMT cores only
- modulesAdd['windows-amt'].push(...moduleData);
- modulesAdd['linux-amt'].push(...moduleData);
- } else if (moduleName.startsWith('win-')) {
- // Add to Windows cores only
- modulesAdd['windows-amt'].push(...moduleData);
- } else if (moduleName.startsWith('linux-')) {
- // Add to Linux cores only
- modulesAdd['linux-amt'].push(...moduleData);
- modulesAdd['linux-noamt'].push(...moduleData);
- } else {
- // Add to all cores
- modulesAdd['windows-amt'].push(...moduleData);
- modulesAdd['linux-amt'].push(...moduleData);
- modulesAdd['linux-noamt'].push(...moduleData);
- }
-
- // Merge this module to recovery modules if needed
- if (modulesAdd['windows-recovery'] != null) {
- if ((moduleName == 'win-console') || (moduleName == 'win-message-pump') || (moduleName == 'win-terminal') || (moduleName == 'win-virtual-terminal')) {
- modulesAdd['windows-recovery'].push(...moduleData);
- }
- }
-
- // Merge this module to agent recovery modules if needed
- if (modulesAdd['windows-agentrecovery'] != null) {
- if ((moduleName == 'win-console') || (moduleName == 'win-message-pump') || (moduleName == 'win-terminal') || (moduleName == 'win-virtual-terminal')) {
- modulesAdd['windows-agentrecovery'].push(...moduleData);
- }
- }
- }
- }
- }
-
- // Add plugins to cores
- if (obj.pluginHandler) { obj.pluginHandler.addMeshCoreModules(modulesAdd); }
-
- // If we need to dump modules to file, create a meshcores folder
- if (dumpToFile) { try { obj.fs.mkdirSync('meshcores'); } catch (ex) { } }
-
- // Merge the cores and compute the hashes
- for (var i in modulesAdd) {
- if ((i == 'windows-recovery') || (i == 'linux-recovery')) {
- obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshRecoveryCore].join('');
- } else if ((i == 'windows-agentrecovery') || (i == 'linux-agentrecovery')) {
- obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshAgentRecoveryCore].join('');
- } else if ((i == 'windows-tiny') || (i == 'linux-tiny')) {
- obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshTinyCore].join('');
- } else {
- obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshCore].join('');
- }
- obj.defaultMeshCores[i] = Buffer.from(obj.defaultMeshCores[i], 'utf8');
- obj.defaultMeshCoresHash[i] = obj.crypto.createHash('sha384').update(obj.defaultMeshCores[i]).digest('binary');
- obj.debug('main', 'Core module ' + i + ' is ' + obj.defaultMeshCores[i].length + ' bytes.');
-
- // Write all modules to files. Great for debugging.
- if (dumpToFile) {
- console.log('Core module ' + i + ' is ' + obj.defaultMeshCores[i].length + ' bytes, saving to meshcores/' + i + '.js.'); // Print the core size and filename
- obj.fs.writeFile('meshcores/' + i + '.js', obj.defaultMeshCores[i].slice(4), function () { }); // Write the core to file
- }
-
- // Compress the mesh cores with DEFLATE
- const callback = function MeshCoreDeflateCb(err, buffer) { if (err == null) { obj.defaultMeshCoresDeflate[MeshCoreDeflateCb.i] = buffer; } }
- callback.i = i;
- require('zlib').deflate(obj.defaultMeshCores[i], { level: require('zlib').Z_BEST_COMPRESSION }, callback);
- }
- }
-
- // We are done creating all the mesh cores.
- if (func != null) { func(true); }
- };
-
- // Update the default meshcmd
- obj.updateMeshCmdTimer = 'notset';
- obj.updateMeshCmd = function (func) {
- // Figure out where meshcmd.js is and read it.
- var meshCmd = null, meshcmdPath, moduleAdditions = ['var addedModules = [];\r\n'], moduleDirPath, modulesDir = null;
- if ((obj.args.minifycore !== false) && (obj.fs.existsSync(obj.path.join(obj.datapath, 'meshcmd.min.js')))) { meshcmdPath = obj.path.join(obj.datapath, 'meshcmd.min.js'); meshCmd = obj.fs.readFileSync(meshcmdPath).toString(); }
- else if (obj.fs.existsSync(obj.path.join(obj.datapath, 'meshcmd.js'))) { meshcmdPath = obj.path.join(obj.datapath, 'meshcmd.js'); meshCmd = obj.fs.readFileSync(meshcmdPath).toString(); }
- else if ((obj.args.minifycore !== false) && (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'meshcmd.min.js')))) { meshcmdPath = obj.path.join(__dirname, 'agents', 'meshcmd.min.js'); meshCmd = obj.fs.readFileSync(meshcmdPath).toString(); }
- else if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'meshcmd.js'))) { meshcmdPath = obj.path.join(__dirname, 'agents', 'meshcmd.js'); meshCmd = obj.fs.readFileSync(meshcmdPath).toString(); }
- else { obj.defaultMeshCmd = null; if (func != null) { func(false); } return; } // meshcmd.js not found
- meshCmd = meshCmd.replace("'***Mesh*Cmd*Version***'", '\'' + getCurrentVersion() + '\'');
-
- // Figure out where the modules_meshcmd folder is.
- if (obj.args.minifycore !== false) { try { moduleDirPath = obj.path.join(meshcmdPath, 'modules_meshcmd_min'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Favor minified modules if present.
- if (modulesDir == null) { try { moduleDirPath = obj.path.join(meshcmdPath, 'modules_meshcmd'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Use non-minified mofules.
- if (obj.args.minifycore !== false) { if (modulesDir == null) { try { moduleDirPath = obj.path.join(__dirname, 'agents', 'modules_meshcmd_min'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } } // Favor minified modules if present.
- if (modulesDir == null) { try { moduleDirPath = obj.path.join(__dirname, 'agents', 'modules_meshcmd'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Use non-minified mofules.
-
- // Read all .js files in the meshcmd modules folder.
- if (modulesDir != null) {
- for (var i in modulesDir) {
- if (modulesDir[i].toLowerCase().endsWith('.js')) {
- // Merge this module
- var moduleName = modulesDir[i].substring(0, modulesDir[i].length - 3);
- if (moduleName.endsWith('.min')) { moduleName = moduleName.substring(0, moduleName.length - 4); } // Remove the ".min" for ".min.js" files.
- moduleAdditions.push('try { addModule("', moduleName, '", "', obj.escapeCodeString(obj.fs.readFileSync(obj.path.join(moduleDirPath, modulesDir[i])).toString('binary')), '"); addedModules.push("', moduleName, '"); } catch (ex) { }\r\n');
- }
- }
- }
-
- // Set the new default meshcmd.js
- moduleAdditions.push(meshCmd);
- obj.defaultMeshCmd = moduleAdditions.join('');
- //console.log('MeshCmd is ' + obj.defaultMeshCmd.length + ' bytes.'); // DEBUG, Print the merged meshcmd.js size
- //obj.fs.writeFile("C:\\temp\\meshcmd.js", obj.defaultMeshCmd.substring(4)); // DEBUG, Write merged meshcmd.js to file
- if (func != null) { func(true); }
-
- // Monitor for changes in meshcmd.js
- if (obj.updateMeshCmdTimer === 'notset') {
- obj.updateMeshCmdTimer = null;
- obj.fs.watch(meshcmdPath, function (eventType, filename) {
- if (obj.updateMeshCmdTimer != null) { clearTimeout(obj.updateMeshCmdTimer); obj.updateMeshCmdTimer = null; }
- obj.updateMeshCmdTimer = setTimeout(function () { obj.updateMeshCmd(); }, 5000);
- });
- }
- };
-
- // List of possible mesh agent install scripts
- const meshToolsList = {
- 'MeshCentralRouter': { localname: 'MeshCentralRouter.exe', dlname: 'winrouter' },
- 'MeshCentralAssistant': { localname: 'MeshCentralAssistant.exe', dlname: 'winassistant', winhash: true }
- //'MeshCentralRouterMacOS': { localname: 'MeshCentralRouter.dmg', dlname: 'MeshCentralRouter.dmg' }
- };
-
- // Update the list of available mesh agents
- obj.updateMeshTools = function () {
- for (var toolname in meshToolsList) {
- if (meshToolsList[toolname].winhash === true) {
- var toolpath = obj.path.join(__dirname, 'agents', meshToolsList[toolname].localname);
- const toolpath2 = obj.path.join(obj.datapath, 'agents', meshToolsList[toolname].localname);
- if (obj.fs.existsSync(toolpath2)) { toolpath = toolpath2; } // If the tool is present in "meshcentral-data/agents", use that one instead.
-
- var hashStream = obj.crypto.createHash('sha384');
- hashStream.toolname = toolname;
- hashStream.toolpath = toolpath;
- hashStream.dlname = meshToolsList[toolname].dlname;
- hashStream.hashx = 0;
- hashStream.on('data', function (data) {
- obj.meshToolsBinaries[this.toolname] = { hash: data.toString('hex'), hashx: this.hashx, path: this.toolpath, dlname: this.dlname, url: this.url };
- obj.meshToolsBinaries[this.toolname].url = 'https://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?meshaction=' + this.dlname;
- var stats = null;
- try { stats = obj.fs.statSync(this.toolpath); } catch (ex) { }
- if (stats != null) { obj.meshToolsBinaries[this.toolname].size = stats.size; }
- });
- const options = { sourcePath: toolpath, targetStream: hashStream };
- obj.exeHandler.hashExecutableFile(options);
- } else {
- var toolpath = obj.path.join(__dirname, 'agents', meshToolsList[toolname].localname);
- const toolpath2 = obj.path.join(obj.datapath, 'agents', meshToolsList[toolname].localname);
- if (obj.fs.existsSync(toolpath2)) { toolpath = toolpath2; } // If the tool is present in "meshcentral-data/agents", use that one instead.
-
- var stream = null;
- try {
- stream = obj.fs.createReadStream(toolpath);
- stream.on('data', function (data) { this.hash.update(data, 'binary'); this.hashx += data.length; });
- stream.on('error', function (data) {
- // If there is an error reading this file, make sure this agent is not in the agent table
- if (obj.meshToolsBinaries[this.toolname] != null) { delete obj.meshToolsBinaries[this.toolname]; }
- });
- stream.on('end', function () {
- // Add the agent to the agent table with all information and the hash
- obj.meshToolsBinaries[this.toolname] = {};
- obj.meshToolsBinaries[this.toolname].hash = this.hash.digest('hex');
- obj.meshToolsBinaries[this.toolname].hashx = this.hashx;
- obj.meshToolsBinaries[this.toolname].path = this.agentpath;
- obj.meshToolsBinaries[this.toolname].dlname = this.dlname;
- obj.meshToolsBinaries[this.toolname].url = 'https://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?meshaction=' + this.dlname;
- var stats = null;
- try { stats = obj.fs.statSync(this.agentpath); } catch (ex) { }
- if (stats != null) { obj.meshToolsBinaries[this.toolname].size = stats.size; }
- });
- stream.toolname = toolname;
- stream.agentpath = toolpath;
- stream.dlname = meshToolsList[toolname].dlname;
- stream.hash = obj.crypto.createHash('sha384', stream);
- stream.hashx = 0;
- } catch (ex) { }
- }
- }
- };
-
- // List of possible mesh agent install scripts
- const meshAgentsInstallScriptList = {
- 1: { id: 1, localname: 'meshinstall-linux.sh', rname: 'meshinstall.sh', linux: true },
- 2: { id: 2, localname: 'meshinstall-initd.sh', rname: 'meshagent', linux: true },
- 5: { id: 5, localname: 'meshinstall-bsd-rcd.sh', rname: 'meshagent', linux: true },
- 6: { id: 6, localname: 'meshinstall-linux.js', rname: 'meshinstall.js', linux: true }
- };
-
- // Update the list of available mesh agents
- obj.updateMeshAgentInstallScripts = function () {
- for (var scriptid in meshAgentsInstallScriptList) {
- var scriptpath = obj.path.join(__dirname, 'agents', meshAgentsInstallScriptList[scriptid].localname);
- var stream = null;
- try {
- stream = obj.fs.createReadStream(scriptpath);
- stream.xdata = '';
- stream.on('data', function (data) { this.hash.update(data, 'binary'); this.xdata += data; });
- stream.on('error', function (data) {
- // If there is an error reading this file, make sure this agent is not in the agent table
- if (obj.meshAgentInstallScripts[this.info.id] != null) { delete obj.meshAgentInstallScripts[this.info.id]; }
- });
- stream.on('end', function () {
- // Add the agent to the agent table with all information and the hash
- obj.meshAgentInstallScripts[this.info.id] = Object.assign({}, this.info);
- obj.meshAgentInstallScripts[this.info.id].hash = this.hash.digest('hex');
- obj.meshAgentInstallScripts[this.info.id].path = this.agentpath;
- obj.meshAgentInstallScripts[this.info.id].data = this.xdata;
- obj.meshAgentInstallScripts[this.info.id].url = 'https://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?script=' + this.info.id;
- var stats = null;
- try { stats = obj.fs.statSync(this.agentpath); } catch (ex) { }
- if (stats != null) { obj.meshAgentInstallScripts[this.info.id].size = stats.size; }
-
- // Place Unit line breaks on Linux scripts if not already present.
- if (obj.meshAgentInstallScripts[this.info.id].linux === true) { obj.meshAgentInstallScripts[this.info.id].data = obj.meshAgentInstallScripts[this.info.id].data.split('\r\n').join('\n') }
- });
- stream.info = meshAgentsInstallScriptList[scriptid];
- stream.agentpath = scriptpath;
- stream.hash = obj.crypto.createHash('sha384', stream);
- } catch (ex) { }
- }
- };
-
- // List of possible mesh agents
- obj.meshAgentsArchitectureNumbers = {
- 0: { id: 0, localname: 'Unknown', rname: 'meshconsole.exe', desc: 'Unknown agent', update: false, amt: true, platform: 'unknown', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 1: { id: 1, localname: 'MeshConsole.exe', rname: 'meshconsole32.exe', desc: 'Windows x86-32 console', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
- 2: { id: 2, localname: 'MeshConsole64.exe', rname: 'meshconsole64.exe', desc: 'Windows x86-64 console', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
- 3: { id: 3, localname: 'MeshService.exe', rname: 'meshagent32.exe', desc: 'Windows x86-32 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', codesign: true },
- 4: { id: 4, localname: 'MeshService64.exe', rname: 'meshagent64.exe', desc: 'Windows x86-64 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', codesign: true },
- 5: { id: 5, localname: 'meshagent_x86', rname: 'meshagent', desc: 'Linux x86-32', update: true, amt: true, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 6: { id: 6, localname: 'meshagent_x86-64', rname: 'meshagent', desc: 'Linux x86-64', update: true, amt: true, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 7: { id: 7, localname: 'meshagent_mips', rname: 'meshagent', desc: 'Linux MIPS', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 8: { id: 8, localname: 'MeshAgent-Linux-XEN-x86-32', rname: 'meshagent', desc: 'XEN x86-64', update: true, amt: false, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 9: { id: 9, localname: 'meshagent_arm', rname: 'meshagent', desc: 'Linux ARM5', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 10: { id: 10, localname: 'MeshAgent-Linux-ARM-PlugPC', rname: 'meshagent', desc: 'Linux ARM PlugPC', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 11: { id: 11, localname: 'meshagent_osx-x86-32', rname: 'meshosx', desc: 'Apple macOS x86-32', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Apple x86-32 binary, no longer supported.
- 12: { id: 12, localname: 'MeshAgent-Android-x86', rname: 'meshandroid', desc: 'Android x86-32', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 13: { id: 13, localname: 'meshagent_pogo', rname: 'meshagent', desc: 'Linux ARM PogoPlug', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 14: { id: 14, localname: 'meshagent_android.apk', rname: 'meshandroid.apk', desc: 'Android', update: false, amt: false, platform: 'android', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Get this one from Google Play
- 15: { id: 15, localname: 'meshagent_poky', rname: 'meshagent', desc: 'Linux Poky x86-32', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 16: { id: 16, localname: 'meshagent_osx-x86-64', rname: 'meshagent', desc: 'Apple macOS x86-64', update: true, amt: false, platform: 'osx', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Apple x86-64 binary
- 17: { id: 17, localname: 'MeshAgent-ChromeOS', rname: 'meshagent', desc: 'Google ChromeOS', update: false, amt: false, platform: 'chromeos', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Get this one from Chrome store
- 18: { id: 18, localname: 'meshagent_poky64', rname: 'meshagent', desc: 'Linux Poky x86-64', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 19: { id: 19, localname: 'meshagent_x86_nokvm', rname: 'meshagent', desc: 'Linux x86-32 NoKVM', update: true, amt: true, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 20: { id: 20, localname: 'meshagent_x86-64_nokvm', rname: 'meshagent', desc: 'Linux x86-64 NoKVM', update: true, amt: true, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 21: { id: 21, localname: 'MeshAgent-WinMinCore-Console-x86-32.exe', rname: 'meshagent.exe', desc: 'Windows MinCore Console x86-32', update: true, amt: false, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
- 22: { id: 22, localname: 'MeshAgent-WinMinCore-Service-x86-64.exe', rname: 'meshagent.exe', desc: 'Windows MinCore Service x86-32', update: true, amt: false, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
- 23: { id: 23, localname: 'MeshAgent-NodeJS', rname: 'meshagent', desc: 'NodeJS', update: false, amt: false, platform: 'node', core: 'nodejs', rcore: 'nodejs', arcore: 'nodejs', tcore: 'nodejs' }, // NodeJS based agent
- 24: { id: 24, localname: 'meshagent_arm-linaro', rname: 'meshagent', desc: 'Linux ARM Linaro', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 25: { id: 25, localname: 'meshagent_armhf', rname: 'meshagent', desc: 'Linux ARM - HardFloat', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // "armv6l" and "armv7l"
- 26: { id: 26, localname: 'meshagent_aarch64', rname: 'meshagent', desc: 'Linux ARM 64 bit (glibc/2.24)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // This is replaced by ARCHID 32
- 27: { id: 27, localname: 'meshagent_armhf2', rname: 'meshagent', desc: 'Linux ARM - HardFloat', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Raspbian 7 2015-02-02 for old Raspberry Pi.
- 28: { id: 28, localname: 'meshagent_mips24kc', rname: 'meshagent', desc: 'Linux MIPS24KC/MUSL (OpenWRT)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // MIPS Router with OpenWRT
- 29: { id: 29, localname: 'meshagent_osx-arm-64', rname: 'meshagent', desc: 'Apple macOS ARM-64', update: true, amt: false, platform: 'osx', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Apple Silicon ARM 64bit
- 30: { id: 30, localname: 'meshagent_freebsd_x86-64', rname: 'meshagent', desc: 'FreeBSD x86-64', update: true, amt: false, platform: 'freebsd', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // FreeBSD x64
- 32: { id: 32, localname: 'meshagent_aarch64', rname: 'meshagent', desc: 'Linux ARM 64 bit (glibc/2.24)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 33: { id: 33, localname: 'meshagent_openwrt_x86_64', rname: 'meshagent', desc: 'OpenWRT x86-64', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // This is replaced with ARCHID 36.
- 34: { id: 34, localname: 'assistant_windows', rname: 'meshassistant', desc: 'MeshCentral Assistant (Windows)', update: false, amt: false, platform: 'assistant', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // MeshCentral Assistant for Windows
- 35: { id: 35, localname: 'meshagent_linux-armada370-hf', rname: 'meshagent', desc: 'Armada370 - ARM32/HF (libc/2.26)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Armada370
- 36: { id: 36, localname: 'meshagent_openwrt_x86_64', rname: 'meshagent', desc: 'OpenWRT x86-64', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // OpenWRT x86-64
- 37: { id: 37, localname: 'meshagent_openbsd_x86-64', rname: 'meshagent', desc: 'OpenBSD x86-64', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // OpenBSD x86-64
- 40: { id: 40, localname: 'meshagent_mipsel24kc', rname: 'meshagent', desc: 'Linux MIPSEL24KC (OpenWRT)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // MIPS Router with OpenWRT
- 41: { id: 41, localname: 'meshagent_aarch64-cortex-a53', rname: 'meshagent', desc: 'ARMADA/CORTEX-A53/MUSL (OpenWRT)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // OpenWRT Routers
- 42: { id: 42, localname: 'MeshConsoleARM64.exe', rname: 'meshconsolearm64.exe', desc: 'Windows ARM-64 console', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
- 43: { id: 43, localname: 'MeshServiceARM64.exe', rname: 'meshagentarm64.exe', desc: 'Windows ARM-64 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', codesign: true },
- // 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)
- 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
- 10003: { id: 10003, localname: 'MeshService.exe', rname: 'meshagent32.exe', desc: 'Windows x86-32 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', unsigned: true },
- 10004: { id: 10004, localname: 'MeshService64.exe', rname: 'meshagent64.exe', desc: 'Windows x86-64 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', unsigned: true },
- 10005: { id: 10005, localname: 'meshagent_osx-universal-64', rname: 'meshagent', desc: 'Apple macOS Universal Binary', update: true, amt: false, platform: 'osx', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Apple Silicon + x86 universal binary
- 10006: { id: 10006, localname: 'MeshCentralAssistant.exe', rname: 'MeshCentralAssistant.exe', desc: 'MeshCentral Assistant for Windows', update: false, amt: false, platform: 'win32' }, // MeshCentral Assistant
- 11000: { id: 11000, localname: 'MeshCmd.exe', rname: 'MeshCmd.exe', desc: 'Windows x86-32 meshcmd', update: false, amt: true, platform: 'win32', codesign: true }, // MeshCMD for Windows x86 32-bit
- 11001: { id: 11001, localname: 'MeshCmd64.exe', rname: 'MeshCmd64.exe', desc: 'Windows x86-64 meshcmd', update: false, amt: true, platform: 'win32', codesign: true }, // MeshCMD for Windows x86 64-bit
- 11002: { id: 11002, localname: 'MeshCmdARM64.exe', rname: 'MeshCmdARM64.exe', desc: 'Windows ARM-64 meshcmd', update: false, amt: true, platform: 'win32', codesign: true } // MeshCMD for Windows ARM 64-bit
- };
-
- // Sign windows agents
- obj.signMeshAgents = function (domain, func) {
- // Setup the domain is specified
- var objx = domain, suffix = '';
- if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
-
- // Check if a custom agent signing certificate is available
- var agentSignCertInfo = require('./authenticode.js').loadCertificates([obj.path.join(obj.datapath, 'agentsigningcert.pem')]);
-
- // If not using a custom signing cert, get agent code signature certificate ready with the full cert chain
- if ((agentSignCertInfo == null) && (obj.certificates.codesign != null)) {
- agentSignCertInfo = {
- cert: obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.codesign.cert),
- key: obj.certificateOperations.forge.pki.privateKeyFromPem(obj.certificates.codesign.key),
- extraCerts: [obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.root.cert)]
- }
- }
- if (agentSignCertInfo == null) { func(); return; } // No code signing certificate, nothing to do.
-
- // Setup the domain is specified
- var objx = domain, suffix = '';
- if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
-
- // Generate the agent signature description and URL
- const serverSignedAgentsPath = obj.path.join(obj.datapath, 'signedagents' + suffix);
- const signDesc = (domain.title ? domain.title : agentSignCertInfo.cert.subject.hash);
- const httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
- var httpsHost = ((domain.dns != null) ? domain.dns : obj.certificates.CommonName);
- if (obj.args.agentaliasdns != null) { httpsHost = obj.args.agentaliasdns; }
- var signUrl = 'https://' + httpsHost;
- if (httpsPort != 443) { signUrl += ':' + httpsPort; }
- var xdomain = (domain.dns == null) ? domain.id : '';
- if (xdomain != '') xdomain += '/';
- signUrl += '/' + xdomain;
-
- // If requested, lock the agent to this server
- if (obj.config.settings.agentsignlock) { signUrl += '?ServerID=' + obj.certificateOperations.getPublicKeyHash(obj.certificates.agent.cert).toUpperCase(); }
-
- // Setup the time server
- var timeStampUrl = 'http://timestamp.comodoca.com/authenticode';
- if (obj.args.agenttimestampserver === false) { timeStampUrl = null; }
- else if (typeof obj.args.agenttimestampserver == 'string') { timeStampUrl = obj.args.agenttimestampserver; }
-
- // Setup the time server proxy
- var timeStampProxy = null;
- if (typeof obj.args.agenttimestampproxy == 'string') { timeStampProxy = obj.args.agenttimestampproxy; }
- else if ((obj.args.agenttimestampproxy !== false) && (typeof obj.args.npmproxy == 'string')) { timeStampProxy = obj.args.npmproxy; }
-
- // Setup the pending operations counter
- var pendingOperations = 1;
-
- for (var archid in obj.meshAgentsArchitectureNumbers) {
- if (obj.meshAgentsArchitectureNumbers[archid].codesign !== true) continue;
-
- var agentpath;
- if (domain.id == '') {
- // Load all agents when processing the default domain
- agentpath = obj.path.join(__dirname, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
- var agentpath2 = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
- if (obj.fs.existsSync(agentpath2)) { agentpath = agentpath2; delete obj.meshAgentsArchitectureNumbers[archid].codesign; } // If the agent is present in "meshcentral-data/agents", use that one instead.
- } else {
- // When processing an extra domain, only load agents that are specific to that domain
- agentpath = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
- if (obj.fs.existsSync(agentpath)) { delete obj.meshAgentsArchitectureNumbers[archid].codesign; } else { continue; } // If the agent is not present in "meshcentral-data/agents" skip.
- }
-
- // Open the original agent with authenticode
- const signeedagentpath = obj.path.join(serverSignedAgentsPath, obj.meshAgentsArchitectureNumbers[archid].localname);
- const originalAgent = require('./authenticode.js').createAuthenticodeHandler(agentpath);
- if (originalAgent != null) {
- // Check if the agent is already signed correctly
- const destinationAgent = require('./authenticode.js').createAuthenticodeHandler(signeedagentpath);
- var destinationAgentOk = (
- (destinationAgent != null) &&
- (destinationAgent.fileHashSigned != null) &&
- (Buffer.compare(destinationAgent.fileHashSigned, destinationAgent.fileHashActual) == 0) &&
- (destinationAgent.signingAttribs.indexOf(signUrl) >= 0) &&
- (destinationAgent.signingAttribs.indexOf(signDesc) >= 0)
- );
-
- if (destinationAgent != null) {
- // If the agent is signed correctly, look to see if the resources in the destination agent are correct
- var orgVersionStrings = originalAgent.getVersionInfo();
- if (destinationAgentOk == true) {
- const versionStrings = destinationAgent.getVersionInfo();
- const versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
- for (var i in versionProperties) {
- const prop = versionProperties[i], propl = prop.toLowerCase();
- if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo[propl] == 'string')) {
- if (domain.agentfileinfo[propl] != versionStrings[prop]) { destinationAgentOk = false; break; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
- } else {
- if (orgVersionStrings[prop] != versionStrings[prop]) { destinationAgentOk = false; break; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
- }
- }
-
- // Check file version number
- if (destinationAgentOk == true) {
- if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo['fileversionnumber'] == 'string')) {
- if (domain.agentfileinfo['fileversionnumber'] != versionStrings['~FileVersion']) { destinationAgentOk = false; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
- } else {
- if (orgVersionStrings['~FileVersion'] != versionStrings['~FileVersion']) { destinationAgentOk = false; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
- }
- }
-
- // Check product version number
- if (destinationAgentOk == true) {
- if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo['productversionnumber'] == 'string')) {
- if (domain.agentfileinfo['productversionnumber'] != versionStrings['~ProductVersion']) { destinationAgentOk = false; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
- } else {
- if (orgVersionStrings['~ProductVersion'] != versionStrings['~ProductVersion']) { destinationAgentOk = false; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
- }
- }
-
- // Check the agent icon
- if (destinationAgentOk == true) {
- if ((domain.agentfileinfo != null) && (domain.agentfileinfo.icon != null)) {
- // Check if the destination agent matches the icon we want
- const agentIconGroups = destinationAgent.getIconInfo();
- if (agentIconGroups != null) {
- const agentIconGroupNames = Object.keys(agentIconGroups);
- if (agentIconGroupNames.length > 0) {
- const agentMainIconGroup = agentIconGroups[agentIconGroupNames[0]];
- if (agentMainIconGroup.resCount != domain.agentfileinfo.icon.resCount) {
- destinationAgentOk = false; // The icon image count is different, don't bother hashing to see if the icons are different.
- } else {
- const agentMainIconGroupHash = require('./authenticode.js').hashObject(agentMainIconGroup);
- const iconHash = require('./authenticode.js').hashObject(domain.agentfileinfo.icon);
- if (agentMainIconGroupHash != iconHash) { destinationAgentOk = false; } // If the existing agent icon does not match the desired icon, we need to re-sign the agent.
- }
- }
- }
- } else {
- // Check if the destination agent has the default icon
- const agentIconGroups1 = destinationAgent.getIconInfo();
- const agentIconGroups2 = originalAgent.getIconInfo();
- if (agentIconGroups1.resCount != agentIconGroups2.resCount) {
- destinationAgentOk = false; // The icon image count is different, don't bother hashing to see if the icons are different.
- } else {
- const iconHash1 = require('./authenticode.js').hashObject(agentIconGroups1);
- const iconHash2 = require('./authenticode.js').hashObject(agentIconGroups2);
- if (iconHash1 != iconHash2) { destinationAgentOk = false; } // If the existing agent icon does not match the desired icon, we need to re-sign the agent.
- }
- }
- }
-
- // Check the agent logo
- if (destinationAgentOk == true) {
- if ((domain.agentfileinfo != null) && (domain.agentfileinfo.logo != null)) {
- // Check if the destination agent matches the logo we want
- const agentBitmaps = destinationAgent.getBitmapInfo();
- if (agentBitmaps != null) {
- const agentBitmapNames = Object.keys(agentBitmaps);
- if (agentBitmapNames.length > 0) {
- const agentMainBitmap = agentBitmaps[agentBitmapNames[0]];
- const agentMainBitmapHash = require('./authenticode.js').hashObject(agentMainBitmap);
- const bitmapHash = require('./authenticode.js').hashObject(domain.agentfileinfo.logo);
- if (agentMainBitmapHash != bitmapHash) { destinationAgentOk = false; } // If the existing agent logo does not match the desired logo, we need to re-sign the agent.
- }
- }
- } else {
- // Check if the destination agent has the default icon
- const agentBitmaps1 = destinationAgent.getBitmapInfo();
- const agentBitmaps2 = originalAgent.getBitmapInfo();
- const agentBitmapNames = Object.keys(agentBitmaps1);
- if (agentBitmapNames.length == 0) {
- destinationAgentOk = false;
- } else {
- const iconHash1 = require('./authenticode.js').hashObject(agentBitmaps1[agentBitmapNames[0]]);
- const iconHash2 = require('./authenticode.js').hashObject(agentBitmaps2[agentBitmapNames[0]]);
- if (iconHash1 != iconHash2) { destinationAgentOk = false; } // If the existing agent icon does not match the desired icon, we need to re-sign the agent.
- }
- }
- }
- }
-
- // If everything looks ok, runs a hash of the original and destination agent .text, .data and .rdata sections. If different, sign the agent again.
- if ((destinationAgentOk == true) && (originalAgent.getHashOfSection('sha384', '.text').compare(destinationAgent.getHashOfSection('sha384', '.text')) != 0)) { destinationAgentOk = false; }
- if ((destinationAgentOk == true) && (originalAgent.getHashOfSection('sha384', '.data').compare(destinationAgent.getHashOfSection('sha384', '.data')) != 0)) { destinationAgentOk = false; }
- if ((destinationAgentOk == true) && (originalAgent.getHashOfSection('sha384', '.rdata').compare(destinationAgent.getHashOfSection('sha384', '.rdata')) != 0)) { destinationAgentOk = false; }
-
- // We are done comparing the destination agent, close it.
- destinationAgent.close();
- }
-
- if (destinationAgentOk == false) {
- // If not signed correctly, sign it. First, create the server signed agent folder if needed
- try { obj.fs.mkdirSync(serverSignedAgentsPath); } catch (ex) { }
- const xagentSignedFunc = function agentSignedFunc(err, size) {
- if (err == null) {
- // Agent was signed succesfuly
- console.log(obj.common.format('Code signed {0}.', agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname));
- } else {
- // Failed to sign agent
- addServerWarning('Failed to sign \"' + agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname + '\": ' + err, 22, [agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname, err]);
- }
- obj.callExternalSignJob(agentSignedFunc.signingArguments); // Call external signing job regardless of success or failure
- if (--pendingOperations === 0) { agentSignedFunc.func(); }
- }
- pendingOperations++;
- xagentSignedFunc.func = func;
- xagentSignedFunc.objx = objx;
- xagentSignedFunc.archid = archid;
- xagentSignedFunc.signeedagentpath = signeedagentpath;
-
- // Parse the resources in the executable and make any required changes
- var resChanges = false, versionStrings = null;
- if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object')) {
- versionStrings = originalAgent.getVersionInfo();
- var versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
- // Change the agent string properties
- for (var i in versionProperties) {
- const prop = versionProperties[i], propl = prop.toLowerCase();
- if (domain.agentfileinfo[propl] && (domain.agentfileinfo[propl] != versionStrings[prop])) { versionStrings[prop] = domain.agentfileinfo[propl]; resChanges = true; }
- }
- // Change the agent file version
- if (domain.agentfileinfo['fileversionnumber'] && (domain.agentfileinfo['fileversionnumber'] != versionStrings['~FileVersion'])) {
- versionStrings['~FileVersion'] = domain.agentfileinfo['fileversionnumber']; resChanges = true;
- }
- // Change the agent product version
- if (domain.agentfileinfo['productversionnumber'] && (domain.agentfileinfo['productversionnumber'] != versionStrings['~ProductVersion'])) {
- versionStrings['~ProductVersion'] = domain.agentfileinfo['productversionnumber']; resChanges = true;
- }
- if (resChanges == true) { originalAgent.setVersionInfo(versionStrings); }
-
- // Change the agent icon
- if (domain.agentfileinfo.icon != null) {
- const agentIconGroups = originalAgent.getIconInfo();
- if (agentIconGroups != null) {
- const agentIconGroupNames = Object.keys(agentIconGroups);
- if (agentIconGroupNames.length > 0) {
- const agentMainIconGroupName = agentIconGroupNames[0];
- agentIconGroups[agentIconGroupNames[0]] = domain.agentfileinfo.icon;
- originalAgent.setIconInfo(agentIconGroups);
- }
- }
- }
-
- // Change the agent logo
- if (domain.agentfileinfo.logo != null) {
- const agentBitmaps = originalAgent.getBitmapInfo();
- if (agentBitmaps != null) {
- const agentBitmapNames = Object.keys(agentBitmaps);
- if (agentBitmapNames.length > 0) {
- agentBitmaps[agentBitmapNames[0]] = domain.agentfileinfo.logo;
- originalAgent.setBitmapInfo(agentBitmaps);
- }
- }
- }
- }
-
- const signingArguments = { out: signeedagentpath, desc: signDesc, url: signUrl, time: timeStampUrl, proxy: timeStampProxy }; // Shallow clone
- signingArguments.resChanges = resChanges;
-
- obj.debug('main', "Code signing with arguments: " + JSON.stringify(signingArguments));
- xagentSignedFunc.signingArguments = signingArguments; // Attach the signing arguments to the callback function
- if (resChanges == false) {
- // Sign the agent the simple way, without changing any resources.
- originalAgent.sign(agentSignCertInfo, signingArguments, xagentSignedFunc);
- } else {
- // Change the agent resources and sign the agent, this is a much more involved process.
- // NOTE: This is experimental and could corupt the agent.
- originalAgent.writeExecutable(signingArguments, agentSignCertInfo, xagentSignedFunc);
- }
-
- } else {
- // Signed agent is already ok, use it.
- originalAgent.close();
- }
-
-
- }
- }
-
- if (--pendingOperations === 0) { func(); }
- }
-
- obj.callExternalSignJob = function (signingArguments) {
- if (obj.config.settings && !obj.config.settings.externalsignjob) {
- return;
- }
- obj.debug('main', "External signing job called for file: " + signingArguments.out);
-
- const { spawnSync } = require('child_process');
-
- const signResult = spawnSync('"' + obj.config.settings.externalsignjob + '"', ['"' + signingArguments.out + '"'], {
- encoding: 'utf-8',
- shell: true,
- stdio: 'inherit'
- });
-
- if (signResult.error || signResult.status !== 0) {
- obj.debug('main', "External signing failed for file: " + signingArguments.out);
- console.error("External signing failed for file: " + signingArguments.out);
- return;
- }
- }
-
- // Update the list of available mesh agents
- obj.updateMeshAgentsTable = function (domain, func) {
- // Check if a custom agent signing certificate is available
- var agentSignCertInfo = require('./authenticode.js').loadCertificates([obj.path.join(obj.datapath, 'agentsigningcert.pem')]);
-
- // If not using a custom signing cert, get agent code signature certificate ready with the full cert chain
- if ((agentSignCertInfo == null) && (obj.certificates.codesign != null)) {
- agentSignCertInfo = {
- cert: obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.codesign.cert),
- key: obj.certificateOperations.forge.pki.privateKeyFromPem(obj.certificates.codesign.key),
- extraCerts: [obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.root.cert)]
- }
- }
-
- // Setup the domain is specified
- var objx = domain, suffix = '';
- if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
-
- // Load agent information file. This includes the data & time of the agent.
- const agentInfo = [];
- try { agentInfo = JSON.parse(obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'hashagents.json'), 'utf8')); } catch (ex) { }
-
- var archcount = 0;
- for (var archid in obj.meshAgentsArchitectureNumbers) {
- var agentpath;
- if (domain.id == '') {
- // Load all agents when processing the default domain
- agentpath = obj.path.join(__dirname, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
- if (obj.meshAgentsArchitectureNumbers[archid].unsigned !== true) {
- const agentpath2 = obj.path.join(obj.datapath, 'signedagents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
- if (obj.fs.existsSync(agentpath2)) { agentpath = agentpath2; } // If the agent is present in "meshcentral-data/signedagents", use that one instead.
- const agentpath3 = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
- if (obj.fs.existsSync(agentpath3)) { agentpath = agentpath3; } // If the agent is present in "meshcentral-data/agents", use that one instead.
- }
- } else {
- // When processing an extra domain, only load agents that are specific to that domain
- agentpath = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
- if (obj.fs.existsSync(agentpath)) { delete obj.meshAgentsArchitectureNumbers[archid].codesign; } else { continue; } // If the agent is not present in "meshcentral-data/agents" skip.
- }
-
- // Fetch agent binary information
- var stats = null;
- try { stats = obj.fs.statSync(agentpath); } catch (ex) { }
- if ((stats == null)) continue; // If this agent does not exist, skip it.
-
- // Setup agent information
- archcount++;
- objx.meshAgentBinaries[archid] = Object.assign({}, obj.meshAgentsArchitectureNumbers[archid]);
- objx.meshAgentBinaries[archid].path = agentpath;
- objx.meshAgentBinaries[archid].url = 'http://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?id=' + archid;
- objx.meshAgentBinaries[archid].size = stats.size;
- if ((agentInfo[archid] != null) && (agentInfo[archid].mtime != null)) { objx.meshAgentBinaries[archid].mtime = new Date(agentInfo[archid].mtime); } // Set agent time if available
-
- // If this is a windows binary, pull binary information
- if (obj.meshAgentsArchitectureNumbers[archid].platform == 'win32') {
- try { objx.meshAgentBinaries[archid].pe = obj.exeHandler.parseWindowsExecutable(agentpath); } catch (ex) { }
- }
-
- // If agents must be stored in RAM or if this is a Windows 32/64 agent, load the agent in RAM.
- if ((obj.args.agentsinram === true) || (((archid == 3) || (archid == 4)) && (obj.args.agentsinram !== false))) {
- if ((archid == 3) || (archid == 4)) {
- // Load the agent with a random msh added to it.
- const outStream = new require('stream').Duplex();
- outStream.meshAgentBinary = objx.meshAgentBinaries[archid];
- if (agentSignCertInfo) { outStream.meshAgentBinary.randomMsh = agentSignCertInfo.cert.subject.hash; } else { outStream.meshAgentBinary.randomMsh = obj.crypto.randomBytes(16).toString('hex'); }
- outStream.bufferList = [];
- outStream._write = function (chunk, encoding, callback) { this.bufferList.push(chunk); if (callback) callback(); }; // Append the chuck.
- outStream._read = function (size) { }; // Do nothing, this is not going to be called.
- outStream.on('finish', function () {
- // Merge all chunks
- this.meshAgentBinary.data = Buffer.concat(this.bufferList);
- this.meshAgentBinary.size = this.meshAgentBinary.data.length;
- delete this.bufferList;
-
- // Hash the uncompressed binary
- const hash = obj.crypto.createHash('sha384').update(this.meshAgentBinary.data);
- this.meshAgentBinary.fileHash = hash.digest('binary');
- this.meshAgentBinary.fileHashHex = Buffer.from(this.meshAgentBinary.fileHash, 'binary').toString('hex');
-
- // Compress the agent using ZIP
- const archive = require('archiver')('zip', { level: 9 }); // Sets the compression method.
- const onZipData = function onZipData(buffer) { onZipData.x.zacc.push(buffer); }
- const onZipEnd = function onZipEnd() {
- // Concat all the buffer for create compressed zip agent
- const concatData = Buffer.concat(onZipData.x.zacc);
- delete onZipData.x.zacc;
-
- // Hash the compressed binary
- const hash = obj.crypto.createHash('sha384').update(concatData);
- onZipData.x.zhash = hash.digest('binary');
- onZipData.x.zhashhex = Buffer.from(onZipData.x.zhash, 'binary').toString('hex');
-
- // Set the agent
- onZipData.x.zdata = concatData;
- onZipData.x.zsize = concatData.length;
- }
- const onZipError = function onZipError() { delete onZipData.x.zacc; }
- this.meshAgentBinary.zacc = [];
- onZipData.x = this.meshAgentBinary;
- onZipEnd.x = this.meshAgentBinary;
- onZipError.x = this.meshAgentBinary;
- archive.on('data', onZipData);
- archive.on('end', onZipEnd);
- archive.on('error', onZipError);
-
- // Starting with NodeJS v16, passing in a buffer at archive.append() will result a compressed file with zero byte length. To fix this, we pass in the buffer as a stream.
- // archive.append(this.meshAgentBinary.data, { name: 'meshagent' }); // This is the version that does not work on NodeJS v16.
- const ReadableStream = require('stream').Readable;
- const zipInputStream = new ReadableStream();
- zipInputStream.push(this.meshAgentBinary.data);
- zipInputStream.push(null);
- archive.append(zipInputStream, { name: 'meshagent' });
-
- archive.finalize();
- })
- obj.exeHandler.streamExeWithMeshPolicy(
- {
- platform: 'win32',
- sourceFileName: agentpath,
- destinationStream: outStream,
- randomPolicy: true, // Indicates that the msh policy is random data.
- msh: outStream.meshAgentBinary.randomMsh,
- peinfo: objx.meshAgentBinaries[archid].pe
- });
- } else {
- // Load the agent as-is
- objx.meshAgentBinaries[archid].data = obj.fs.readFileSync(agentpath);
-
- // Compress the agent using ZIP
- const archive = require('archiver')('zip', { level: 9 }); // Sets the compression method.
-
- const onZipData = function onZipData(buffer) { onZipData.x.zacc.push(buffer); }
- const onZipEnd = function onZipEnd() {
- // Concat all the buffer for create compressed zip agent
- const concatData = Buffer.concat(onZipData.x.zacc);
- delete onZipData.x.zacc;
-
- // Hash the compressed binary
- const hash = obj.crypto.createHash('sha384').update(concatData);
- onZipData.x.zhash = hash.digest('binary');
- onZipData.x.zhashhex = Buffer.from(onZipData.x.zhash, 'binary').toString('hex');
-
- // Set the agent
- onZipData.x.zdata = concatData;
- onZipData.x.zsize = concatData.length;
-
- //console.log('Packed', onZipData.x.size, onZipData.x.zsize);
- }
- const onZipError = function onZipError() { delete onZipData.x.zacc; }
- objx.meshAgentBinaries[archid].zacc = [];
- onZipData.x = objx.meshAgentBinaries[archid];
- onZipEnd.x = objx.meshAgentBinaries[archid];
- onZipError.x = objx.meshAgentBinaries[archid];
- archive.on('data', onZipData);
- archive.on('end', onZipEnd);
- archive.on('error', onZipError);
- archive.append(objx.meshAgentBinaries[archid].data, { name: 'meshagent' });
- archive.finalize();
- }
- }
-
- // Hash the binary
- const hashStream = obj.crypto.createHash('sha384');
- hashStream.archid = archid;
- hashStream.on('data', function (data) {
- objx.meshAgentBinaries[this.archid].hash = data.toString('binary');
- objx.meshAgentBinaries[this.archid].hashhex = data.toString('hex');
- if ((--archcount == 0) && (func != null)) { func(); }
- });
- const options = { sourcePath: agentpath, targetStream: hashStream, platform: obj.meshAgentsArchitectureNumbers[archid].platform };
- if (objx.meshAgentBinaries[archid].pe != null) { options.peinfo = objx.meshAgentBinaries[archid].pe; }
- obj.exeHandler.hashExecutableFile(options);
-
- // If we are not loading Windows binaries to RAM, compute the RAW file hash of the signed binaries here.
- if ((obj.args.agentsinram === false) && ((archid == 3) || (archid == 4))) {
- const hash = obj.crypto.createHash('sha384').update(obj.fs.readFileSync(agentpath));
- objx.meshAgentBinaries[archid].fileHash = hash.digest('binary');
- objx.meshAgentBinaries[archid].fileHashHex = Buffer.from(objx.meshAgentBinaries[archid].fileHash, 'binary').toString('hex');
- }
- }
- };
-
- // Generate a time limited user login token
- obj.getLoginToken = function (userid, func) {
- if ((userid == null) || (typeof userid != 'string')) { func('Invalid userid.'); return; }
- const x = userid.split('/');
- if (x == null || x.length != 3 || x[0] != 'user') { func('Invalid userid.'); return; }
- obj.db.Get(userid, function (err, docs) {
- if (err != null || docs == null || docs.length == 0) {
- func('User ' + userid + ' not found.'); return;
- } else {
- // Load the login cookie encryption key from the database
- obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
- if ((docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 160)) {
- // Key is present, use it.
- obj.loginCookieEncryptionKey = Buffer.from(docs[0].key, 'hex');
- func(obj.encodeCookie({ u: userid, a: 3 }, obj.loginCookieEncryptionKey));
- } else {
- // Key is not present, generate one.
- obj.loginCookieEncryptionKey = obj.generateCookieKey();
- obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() }, function () { func(obj.encodeCookie({ u: userid, a: 3 }, obj.loginCookieEncryptionKey)); });
- }
- });
- }
- });
- };
-
- // Show the user login token generation key
- obj.showLoginTokenKey = function (func) {
- // Load the login cookie encryption key from the database
- obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
- if ((docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 160)) {
- // Key is present, use it.
- func(docs[0].key);
- } else {
- // Key is not present, generate one.
- obj.loginCookieEncryptionKey = obj.generateCookieKey();
- obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() }, function () { func(obj.loginCookieEncryptionKey.toString('hex')); });
- }
- });
- };
-
- // Load the list of Intel AMT UUID and passwords from "amtactivation.log"
- obj.loadAmtActivationLogPasswords = function (func) {
- const amtlogfilename = obj.path.join(obj.datapath, 'amtactivation.log');
- obj.fs.readFile(amtlogfilename, 'utf8', function (err, data) {
- const amtPasswords = {}; // UUID --> [Passwords]
- if ((err == null) && (data != null)) {
- const lines = data.split('\n');
- for (var i in lines) {
- const line = lines[i];
- if (line.startsWith('{')) {
- var j = null;
- try { j = JSON.parse(line); } catch (ex) { }
- if ((j != null) && (typeof j == 'object')) {
- if ((typeof j.amtUuid == 'string') && (typeof j.password == 'string')) {
- if (amtPasswords[j.amtUuid] == null) {
- amtPasswords[j.amtUuid] = [j.password]; // Add password to array
- } else {
- amtPasswords[j.amtUuid].unshift(j.password); // Add password at the start of the array
- }
- }
- }
- }
- }
- // Remove all duplicates and only keep the 3 last passwords for any given device
- for (var i in amtPasswords) {
- amtPasswords[i] = [...new Set(amtPasswords[i])];
- while (amtPasswords[i].length > 3) { amtPasswords[i].pop(); }
- }
- }
- func(obj.common.sortObj(amtPasswords)); // Sort by UUID
- });
- }
-
- // Encrypt session data
- obj.encryptSessionData = function (data, key) {
- if (data == null) return null;
- if (key == null) { key = obj.loginCookieEncryptionKey; }
- try {
- const iv = Buffer.from(obj.crypto.randomBytes(12), 'binary'), cipher = obj.crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv);
- const crypted = Buffer.concat([cipher.update(JSON.stringify(data), 'utf8'), cipher.final()]);
- return Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString(obj.args.cookieencoding ? obj.args.cookieencoding : 'base64').replace(/\+/g, '@').replace(/\//g, '$');
- } catch (ex) { return null; }
- }
-
- // Decrypt the session data
- obj.decryptSessionData = function (data, key) {
- if ((typeof data != 'string') || (data.length < 13)) return {};
- if (key == null) { key = obj.loginCookieEncryptionKey; }
- try {
- const buf = Buffer.from(data.replace(/\@/g, '+').replace(/\$/g, '/'), obj.args.cookieencoding ? obj.args.cookieencoding : 'base64');
- const decipher = obj.crypto.createDecipheriv('aes-256-gcm', key.slice(0, 32), buf.slice(0, 12));
- decipher.setAuthTag(buf.slice(12, 28));
- return JSON.parse(decipher.update(buf.slice(28), 'binary', 'utf8') + decipher.final('utf8'));
- } catch (ex) { return {}; }
- }
-
- // Generate a cryptographic key used to encode and decode cookies
- obj.generateCookieKey = function () {
- return Buffer.from(obj.crypto.randomBytes(80), 'binary');
- //return Buffer.alloc(80, 0); // Sets the key to zeros, debug only.
- };
-
- // Encode an object as a cookie using a key using AES-GCM. (key must be 32 bytes or more)
- obj.encodeCookie = function (o, key) {
- try {
- if (key == null) { key = obj.serverKey; }
- o.time = Math.floor(Date.now() / 1000); // Add the cookie creation time
- const iv = Buffer.from(obj.crypto.randomBytes(12), 'binary'), cipher = obj.crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv);
- const crypted = Buffer.concat([cipher.update(JSON.stringify(o), 'utf8'), cipher.final()]);
- const r = Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString(obj.args.cookieencoding ? obj.args.cookieencoding : 'base64').replace(/\+/g, '@').replace(/\//g, '$');
- obj.debug('cookie', 'Encoded AESGCM cookie: ' + JSON.stringify(o));
- return r;
- } catch (ex) { obj.debug('cookie', 'ERR: Failed to encode AESGCM cookie due to exception: ' + ex); return null; }
- };
-
- // Decode a cookie back into an object using a key using AES256-GCM or AES128-CBC/HMAC-SHA384. Return null if it's not a valid cookie. (key must be 32 bytes or more)
- obj.decodeCookie = function (cookie, key, timeout) {
- if (cookie == null) return null;
- var r = obj.decodeCookieAESGCM(cookie, key, timeout);
- if (r === -1) { r = obj.decodeCookieAESSHA(cookie, key, timeout); } // If decodeCookieAESGCM() failed to decode, try decodeCookieAESSHA()
- if ((r == null) && (obj.args.cookieencoding == null) && (cookie.length != 64) && ((cookie == cookie.toLowerCase()) || (cookie == cookie.toUpperCase()))) {
- obj.debug('cookie', 'Upper/Lowercase cookie, try "CookieEncoding":"hex" in settings section of config.json.');
- console.log('Upper/Lowercase cookie, try "CookieEncoding":"hex" in settings section of config.json.');
- }
- if ((r != null) && (typeof r.once == 'string') && (r.once.length > 0)) {
- // This cookie must only be used once.
- if (timeout == null) { timeout = 2; }
- if (obj.cookieUseOnceTable[r.once] == null) {
- const ctimeout = (((r.expire) == null || (typeof r.expire != 'number')) ? (r.time + ((timeout + 3) * 60000)) : (r.time + ((r.expire + 3) * 60000)));
-
- // Store the used cookie in RAM
- obj.cookieUseOnceTable[r.once] = ctimeout;
-
- // Store the used cookie in the database
- // TODO
-
- // Send the used cookie to peer servers
- // TODO
-
- // Clean up the used table
- if (++obj.cookieUseOnceTableCleanCounter > 20) {
- const now = Date.now();
- for (var i in obj.cookieUseOnceTable) { if (obj.cookieUseOnceTable[i] < now) { delete obj.cookieUseOnceTable[i]; } }
- obj.cookieUseOnceTableCleanCounter = 0;
- }
- } else { return null; }
- }
- return r;
- }
-
- // Decode a cookie back into an object using a key using AES256-GCM. Return null if it's not a valid cookie. (key must be 32 bytes or more)
- obj.decodeCookieAESGCM = function (cookie, key, timeout) {
- try {
- if (key == null) { key = obj.serverKey; }
- cookie = Buffer.from(cookie.replace(/\@/g, '+').replace(/\$/g, '/'), obj.args.cookieencoding ? obj.args.cookieencoding : 'base64');
- const decipher = obj.crypto.createDecipheriv('aes-256-gcm', key.slice(0, 32), cookie.slice(0, 12));
- decipher.setAuthTag(cookie.slice(12, 28));
- const o = JSON.parse(decipher.update(cookie.slice(28), 'binary', 'utf8') + decipher.final('utf8'));
- if ((o.time == null) || (o.time == null) || (typeof o.time != 'number')) { obj.debug('cookie', 'ERR: Bad cookie due to invalid time'); return null; }
- o.time = o.time * 1000; // Decode the cookie creation time
- o.dtime = Date.now() - o.time; // Decode how long ago the cookie was created (in milliseconds)
- if ((o.expire) == null || (typeof o.expire != 'number')) {
- // Use a fixed cookie expire time
- if (timeout == null) { timeout = 2; }
- if ((o.dtime > (timeout * 60000)) || (o.dtime < -30000)) { obj.debug('cookie', 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right)
- } else {
- // An expire time is included in the cookie (in minutes), use this.
- if ((o.expire !== 0) && ((o.dtime > (o.expire * 60000)) || (o.dtime < -30000))) { obj.debug('cookie', 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right)
- }
- obj.debug('cookie', 'Decoded AESGCM cookie: ' + JSON.stringify(o));
- return o;
- } catch (ex) { obj.debug('cookie', 'ERR: Bad AESGCM cookie due to exception: ' + ex); return -1; }
- };
-
- // Decode a cookie back into an object using a key using AES256 / HMAC-SHA384. Return null if it's not a valid cookie. (key must be 80 bytes or more)
- // We do this because poor .NET does not support AES256-GCM.
- obj.decodeCookieAESSHA = function (cookie, key, timeout) {
- try {
- if (key == null) { key = obj.serverKey; }
- if (key.length < 80) { return null; }
- cookie = Buffer.from(cookie.replace(/\@/g, '+').replace(/\$/g, '/'), obj.args.cookieencoding ? obj.args.cookieencoding : 'base64');
- const decipher = obj.crypto.createDecipheriv('aes-256-cbc', key.slice(48, 80), cookie.slice(0, 16));
- const rawmsg = decipher.update(cookie.slice(16), 'binary', 'binary') + decipher.final('binary');
- const hmac = obj.crypto.createHmac('sha384', key.slice(0, 48));
- hmac.update(rawmsg.slice(48));
- if (Buffer.compare(hmac.digest(), Buffer.from(rawmsg.slice(0, 48))) == false) { return null; }
- const o = JSON.parse(rawmsg.slice(48).toString('utf8'));
- if ((o.time == null) || (o.time == null) || (typeof o.time != 'number')) { obj.debug('cookie', 'ERR: Bad cookie due to invalid time'); return null; }
- o.time = o.time * 1000; // Decode the cookie creation time
- o.dtime = Date.now() - o.time; // Decode how long ago the cookie was created (in milliseconds)
- if ((o.expire) == null || (typeof o.expire != 'number')) {
- // Use a fixed cookie expire time
- if (timeout == null) { timeout = 2; }
- if ((o.dtime > (timeout * 60000)) || (o.dtime < -30000)) { obj.debug('cookie', 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right)
- } else {
- // An expire time is included in the cookie (in minutes), use this.
- if ((o.expire !== 0) && ((o.dtime > (o.expire * 60000)) || (o.dtime < -30000))) { obj.debug('cookie', 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right)
- }
- obj.debug('cookie', 'Decoded AESSHA cookie: ' + JSON.stringify(o));
- return o;
- } catch (ex) { obj.debug('cookie', 'ERR: Bad AESSHA cookie due to exception: ' + ex); return null; }
- };
-
- // Debug
- obj.debug = function (source, ...args) {
- // Send event to console
- if ((obj.debugSources != null) && ((obj.debugSources == '*') || (obj.debugSources.indexOf(source) >= 0))) { console.log(source.toUpperCase() + ':', ...args); }
-
- // Send event to log file
- if (obj.config.settings && obj.config.settings.log) {
- if (typeof obj.args.log == 'string') { obj.args.log = obj.args.log.split(','); }
- if ((obj.args.log.indexOf(source) >= 0) || (obj.args.log[0] == '*')) {
- const d = new Date();
- if (obj.xxLogFile == null) {
- try {
- obj.xxLogFile = obj.fs.openSync(obj.getConfigFilePath('log.txt'), 'a+', 0o666);
- obj.fs.writeSync(obj.xxLogFile, '---- Log start at ' + new Date().toLocaleString() + ' ----\r\n');
- obj.xxLogDateStr = d.toLocaleDateString();
- } catch (ex) { }
- }
- if (obj.xxLogFile != null) {
- try {
- if (obj.xxLogDateStr != d.toLocaleDateString()) { obj.xxLogDateStr = d.toLocaleDateString(); obj.fs.writeSync(obj.xxLogFile, '---- ' + d.toLocaleDateString() + ' ----\r\n'); }
- const formattedArgs = args.map(function (arg) { return (typeof arg === 'object' && arg !== null) ? JSON.stringify(arg) : arg; });
- obj.fs.writeSync(obj.xxLogFile, new Date().toLocaleTimeString() + ' - ' + source + ': ' + formattedArgs.join(', ') + '\r\n');
- } catch (ex) { }
- }
- }
- }
-
- // Send the event to logged in administrators
- if ((obj.debugRemoteSources != null) && ((obj.debugRemoteSources == '*') || (obj.debugRemoteSources.indexOf(source) >= 0))) {
- var sendcount = 0;
- for (var sessionid in obj.webserver.wssessions2) {
- const ws = obj.webserver.wssessions2[sessionid];
- if ((ws != null) && (ws.userid != null)) {
- const user = obj.webserver.users[ws.userid];
- if ((user != null) && (user.siteadmin == 4294967295)) {
- try { ws.send(JSON.stringify({ action: 'trace', source: source, args: args, time: Date.now() })); sendcount++; } catch (ex) { }
- }
- }
- }
- if (sendcount == 0) { obj.debugRemoteSources = null; } // If there are no listeners, remove debug sources.
- }
- };
-
- // Update server state. Writes a server state file.
- const meshServerState = {};
- obj.updateServerState = function (name, val) {
- //console.log('updateServerState', name, val);
- try {
- if ((name != null) && (val != null)) {
- var changed = false;
- if ((name != null) && (meshServerState[name] != val)) { if ((val == null) && (meshServerState[name] != null)) { delete meshServerState[name]; changed = true; } else { if (meshServerState[name] != val) { meshServerState[name] = val; changed = true; } } }
- if (changed == false) return;
- }
- var r = 'time=' + Date.now() + '\r\n';
- for (var i in meshServerState) { r += (i + '=' + meshServerState[i] + '\r\n'); }
- try {
- obj.fs.writeFileSync(obj.getConfigFilePath('serverstate.txt'), r); // Try to write the server state, this may fail if we don't have permission.
- } catch (ex) { obj.serverSelfWriteAllowed = false; }
- } catch (ex) { } // Do nothing since this is not a critical feature.
- };
-
- // Read a list of IP addresses from a file
- function readIpListFromFile(arg) {
- if ((typeof arg != 'string') || (!arg.startsWith('file:'))) return arg;
- var lines = null;
- try { lines = obj.fs.readFileSync(obj.path.join(obj.datapath, arg.substring(5))).toString().split(/\r?\n/).join('\r').split('\r'); } catch (ex) { }
- if (lines == null) return null;
- const validLines = [];
- for (var i in lines) { if ((lines[i].length > 0) && (((lines[i].charAt(0) > '0') && (lines[i].charAt(0) < '9')) || (lines[i].charAt(0) == ':'))) validLines.push(lines[i]); }
- return validLines;
- }
-
- // Logging funtions
- function logException(e) { e += ''; logErrorEvent(e); }
- function logInfoEvent(msg) { if (obj.servicelog != null) { obj.servicelog.info(msg); } console.log(msg); }
- function logWarnEvent(msg) { if (obj.servicelog != null) { obj.servicelog.warn(msg); } console.log(msg); }
- function logErrorEvent(msg) { if (obj.servicelog != null) { obj.servicelog.error(msg); } console.error(msg); }
- obj.getServerWarnings = function () { return serverWarnings; }
- // TODO: migrate from other addServerWarning function and add timestamp
- obj.addServerWarning = function (msg, id, args, print) { serverWarnings.push({ msg: msg, id: id, args: args }); if (print !== false) { console.log("WARNING: " + msg); } }
-
- // auth.log functions
- obj.authLog = function (server, msg, args) {
- if (typeof msg != 'string') return;
- var str = msg;
- if (args != null) {
- if (typeof args.sessionid == 'string') { str += ', SessionID: ' + args.sessionid; }
- if (typeof args.useragent == 'string') { const userAgentInfo = obj.webserver.getUserAgentInfo(args.useragent); str += ', Browser: ' + userAgentInfo.browserStr + ', OS: ' + userAgentInfo.osStr; }
- }
- obj.debug('authlog', str);
- if (obj.syslogauth != null) { try { obj.syslogauth.log(obj.syslogauth.LOG_INFO, str); } catch (ex) { } }
- if (obj.authlogfile != null) { // Write authlog to file
- try {
- const d = new Date(), month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][d.getMonth()];
- str = month + ' ' + d.getDate() + ' ' + obj.common.zeroPad(d.getHours(), 2) + ':' + obj.common.zeroPad(d.getMinutes(), 2) + ':' + d.getSeconds() + ' meshcentral ' + server + '[' + process.pid + ']: ' + msg + ((obj.platform == 'win32') ? '\r\n' : '\n');
- obj.fs.write(obj.authlogfile, str, function (err, written, string) { if (err) { console.error(err); } });
- } catch (ex) { console.error(ex); }
- }
- }
-
- // Return the path of a file into the meshcentral-data path
- obj.getConfigFilePath = function (filename) {
- if ((obj.config != null) && (obj.config.configfiles != null) && (obj.config.configfiles[filename] != null) && (typeof obj.config.configfiles[filename] == 'string')) {
- //console.log('getConfigFilePath(\"' + filename + '\") = ' + obj.config.configfiles[filename]);
- return obj.config.configfiles[filename];
- }
- //console.log('getConfigFilePath(\"' + filename + '\") = ' + obj.path.join(obj.datapath, filename));
- return obj.path.join(obj.datapath, filename);
- };
-
- return obj;
-}
-
-// Resolve a list of names, call back with list of failed resolves.
-function checkResolveAll(names, func) {
- const dns = require('dns'), state = { func: func, count: names.length, err: null };
- for (var i in names) {
- dns.lookup(names[i], { all: true }, function (err, records) {
- if (err != null) { if (this.state.err == null) { this.state.err = [this.name]; } else { this.state.err.push(this.name); } }
- if (--this.state.count == 0) { this.state.func(this.state.err); }
- }.bind({ name: names[i], state: state }))
- }
-}
-
-// Resolve a list of domains to IP addresses, return a flat array of IPs.
-async function resolveDomainsToIps(originalArray) {
- if (!Array.isArray(originalArray)) { return undefined; }
- const flatResult = [];
- for (const item of originalArray) {
- if (new require('ipcheck')(item).valid) {
- flatResult.push(item);
- continue;
- }
- try {
- const results = await require('dns').promises.lookup(item, { all: true });
- flatResult.push(...results.map(r => r.address));
- } catch (err) {
- console.log(`Could not resolve ${item}`);
- }
- }
- if (flatResult.length == 0) { return undefined; }
- return flatResult;
-}
-
-// Return the server configuration
-function getConfig(createSampleConfig) {
- // Figure out the datapath location
- var i, datapath = null;
- const fs = require('fs'), path = require('path'), args = require('minimist')(process.argv.slice(2));
- if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) {
- datapath = path.join(__dirname, '../../meshcentral-data');
- } else {
- datapath = path.join(__dirname, '../meshcentral-data');
- }
- if (args.datapath) { datapath = args.datapath; }
- try { fs.mkdirSync(datapath); } catch (ex) { }
-
- // Read configuration file if present and change arguments.
- var config = {}, configFilePath = path.join(datapath, 'config.json');
- if (args.configfile) { configFilePath = common.joinPath(datapath, args.configfile); }
- if (fs.existsSync(configFilePath)) {
- // Load and validate the configuration file
- try { config = require(configFilePath); } catch (ex) { console.log('ERROR: Unable to parse ' + configFilePath + '.'); return null; }
- if (config.domains == null) { config.domains = {}; }
- for (i in config.domains) { if ((i.split('/').length > 1) || (i.split(' ').length > 1)) { console.log("ERROR: Error in config.json, domain names can't have spaces or /."); return null; } }
- } else {
- if (createSampleConfig === true) {
- // Copy the "sample-config.json" to give users a starting point
- const sampleConfigPath = path.join(__dirname, 'sample-config.json');
- if (fs.existsSync(sampleConfigPath)) { fs.createReadStream(sampleConfigPath).pipe(fs.createWriteStream(configFilePath)); }
- }
- }
-
- // Set the command line arguments to the config file if they are not present
- if (!config.settings) { config.settings = {}; }
- for (i in args) { config.settings[i] = args[i]; }
-
- // Lower case all keys in the config file
- try {
- require('./common.js').objKeysToLower(config, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'telegram/proxy']);
- } catch (ex) {
- console.log('CRITICAL ERROR: Unable to access the file \"./common.js\".\r\nCheck folder & file permissions.');
- process.exit();
- }
-
- return config;
-}
-
-// Check if a list of modules are present and install any missing ones
-function InstallModules(modules, args, func) {
- var missingModules = [];
- if (modules.length > 0) {
- const dependencies = require('./package.json').dependencies;
- for (var i in modules) {
- // Modules may contain a version tag (foobar@1.0.0), remove it so the module can be found using require
- const moduleNameAndVersion = modules[i];
- const moduleInfo = moduleNameAndVersion.split('@', 3);
- var moduleName = null;
- var moduleVersion = null;
- if(moduleInfo.length == 1){ // normal package without version
- moduleName = moduleInfo[0];
- } else if (moduleInfo.length == 2) { // normal package with a version OR custom repo package with no version
- moduleName = moduleInfo[0] === '' ? moduleNameAndVersion : moduleInfo[0];
- moduleVersion = moduleInfo[0] === '' ? null : moduleInfo[1];
- } else if (moduleInfo.length == 3) { // custom repo package and package with a version
- moduleName = "@" + moduleInfo[1];
- moduleVersion = moduleInfo[2];
- }
- try {
- // Does the module need a specific version?
- if (moduleVersion) {
- var versionMatch = false;
- var modulePath = null;
- // This is the first way to test if a module is already installed.
- try { versionMatch = (require(`${moduleName}/package.json`).version == moduleVersion) } catch (ex) {
- if (ex.code == "ERR_PACKAGE_PATH_NOT_EXPORTED") { modulePath = ("" + ex).split(' ').at(-1); } else { throw new Error(); }
- }
- // If the module is not installed, but we get the ERR_PACKAGE_PATH_NOT_EXPORTED error, try a second way.
- if ((versionMatch == false) && (modulePath != null)) {
- if (JSON.parse(require('fs').readFileSync(modulePath, 'utf8')).version != moduleVersion) { throw new Error(); }
- } else if (versionMatch == false) {
- throw new Error();
- }
- } else {
- // For all other modules, do the check here.
- // Is the module in package.json? Install exact version.
- if (typeof dependencies[moduleName] != null) { moduleVersion = dependencies[moduleName]; }
- require(moduleName);
- }
- } catch (ex) {
- missingModules.push(moduleNameAndVersion);
- }
- }
-
- if (missingModules.length > 0) { if (args.debug) { console.log('Missing Modules: ' + missingModules.join(', ')); } InstallModuleEx(missingModules, args, func); } else { func(); }
- }
-}
-
-// Install all missing modules at once. We will be running "npm install" once, with a full list of all modules we need, no matter if they area already installed or not,
-// this is to make sure NPM gives us exactly what we need. Also, we install the meshcentral with current version, so that NPM does not update it - which it will do if obmitted.
-function InstallModuleEx(modulenames, args, func) {
- var names = modulenames.join(' ');
- console.log('Installing modules', modulenames);
- const child_process = require('child_process');
- var parentpath = __dirname;
- function getCurrentVersion() { try { return JSON.parse(require('fs').readFileSync(require('path').join(__dirname, 'package.json'), 'utf8')).version; } catch (ex) { } return null; } // Fetch server version
- //const meshCentralVersion = getCurrentVersion();
- //if ((meshCentralVersion != null) && (args.dev == null)) { names = 'meshcentral@' + getCurrentVersion() + ' ' + names; }
-
- // Get the working directory
- if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) { parentpath = require('path').join(__dirname, '../..'); }
-
- if (args.debug) { console.log('NPM Command Line: ' + npmpath + ` install --save-exact --no-audit --omit=optional --no-fund ${names}`); }
- // always use --save-exact - https://stackoverflow.com/a/64507176/1210734
- child_process.exec(npmpath + ` install --save-exact --no-audit --no-optional --omit=optional ${names}`, { maxBuffer: 512000, timeout: 300000, cwd: parentpath }, function (error, stdout, stderr) {
- if ((error != null) && (error != '')) {
- var mcpath = __dirname;
- if (mcpath.endsWith('\\node_modules\\meshcentral') || mcpath.endsWith('/node_modules/meshcentral')) { mcpath = require('path').join(mcpath, '..', '..'); }
- console.log('ERROR: Unable to install required modules. MeshCentral may not have access to npm, or npm may not have suffisent rights to load the new module. To manualy install this module try:\r\n\r\n cd "' + mcpath + '"\r\n npm install --no-audit --no-optional --omit=optional ' + names + '\r\n node node_modules' + ((require('os').platform() == 'win32') ? '\\' : '/') + 'meshcentral');
- process.exit();
- return;
- }
- func();
- return;
- });
-}
-
-// Detect CTRL-C on Linux and stop nicely
-process.on('SIGINT', function () { if (meshserver != null) { meshserver.Stop(); meshserver = null; } console.log('Server Ctrl-C exit...'); process.exit(); });
-
-// Add a server warning, warnings will be shown to the administrator on the web application
-// TODO: migrate to obj.addServerWarning?
-const serverWarnings = [];
-function addServerWarning(msg, id, args, print) { serverWarnings.push({ msg: msg, id: id, args: args }); if (print !== false) { console.log("WARNING: " + msg); } }
-
-/*
-var ServerWarnings = {
- 1: "",
- 2: "Missing WebDAV parameters.",
- 3: "Unrecognized configuration option \"{0}\".",
- 4: "WebSocket compression is disabled, this feature is broken in NodeJS v11.11 to v12.15 and v13.2",
- 5: "Unable to load Intel AMT TLS root certificate for default domain.",
- 6: "Unable to load Intel AMT TLS root certificate for domain {0}.",
- 7: "CIRA local FQDN's ignored when server in LAN-only or WAN-only mode.",
- 8: "Can't have more than 4 CIRA local FQDN's. Ignoring value.",
- 9: "Agent hash checking is being skipped, this is unsafe.",
- 10: "Missing Let's Encrypt email address.",
- 11: "Invalid Let's Encrypt host names.",
- 12: "Invalid Let's Encrypt names, can't contain a *.",
- 13: "Unable to setup Let's Encrypt module.",
- 14: "Invalid Let's Encrypt names, unable to resolve: {0}",
- 15: "Invalid Let's Encrypt email address, unable to resolve: {0}",
- 16: "Unable to load CloudFlare trusted proxy IPv6 address list.",
- 17: "SendGrid server has limited use in LAN mode.",
- 18: "SMTP server has limited use in LAN mode.",
- 19: "SMS gateway has limited use in LAN mode.",
- 20: "Invalid \"LoginCookieEncryptionKey\" in config.json.",
- 21: "Backup path can't be set within meshcentral-data folder, backup settings ignored.",
- 22: "Failed to sign agent {0}: {1}",
- 23: "Unable to load agent icon file: {0}.",
- 24: "Unable to load agent logo file: {0}.",
- 25: "This NodeJS version does not support OpenID.",
- 26: "This NodeJS version does not support Discord.js.",
- 27: "Firebase now requires a service account JSON file, Firebase disabled."
-};
-*/
-
-// Load the really basic modules
-var npmpath = 'npm';
-var meshserver = null;
-var childProcess = null;
-var previouslyInstalledModules = {};
-function mainStart() {
- // Check the NodeJS is version 16 or better.
- if (Number(process.version.match(/^v(\d+\.\d+)/)[1]) < 16) { console.log("MeshCentral requires Node v16 or above, current version is " + process.version + "."); return; }
-
- // If running within the node_modules folder, move working directory to the parent of the node_modules folder.
- if (__dirname.endsWith('\\node_modules\\meshcentral') || __dirname.endsWith('/node_modules/meshcentral')) { process.chdir(require('path').join(__dirname, '..', '..')); }
-
- // Check for any missing modules.
- InstallModules(['minimist'], {}, function () {
- // Parse inbound arguments
- const args = require('minimist')(process.argv.slice(2));
-
- // Setup the NPM path
- if (args.npmpath == null) {
- try {
- var xnodepath = process.argv[0];
- var xnpmpath = require('path').join(require('path').dirname(process.argv[0]), 'npm');
- if (require('fs').existsSync(xnodepath) && require('fs').existsSync(xnpmpath)) {
- if (xnodepath.indexOf(' ') >= 0) { xnodepath = '"' + xnodepath + '"'; }
- if (xnpmpath.indexOf(' ') >= 0) { xnpmpath = '"' + xnpmpath + '"'; }
- if (require('os').platform() == 'win32') { npmpath = xnpmpath; } else { npmpath = (xnodepath + ' ' + xnpmpath); }
- }
- } catch (ex) { console.log(ex); }
- } else {
- npmpath = args.npmpath;
- }
-
- // Get the server configuration
- var config = getConfig(false);
- if (config == null) { process.exit(); }
-
- // Lowercase the auth value if present
- for (var i in config.domains) { if (typeof config.domains[i].auth == 'string') { config.domains[i].auth = config.domains[i].auth.toLowerCase(); } }
-
- // Get the current node version
- const verSplit = process.version.substring(1).split('.');
- var nodeVersion = parseInt(verSplit[0]) + (parseInt(verSplit[1]) / 100);
-
- // Check if RDP support if present
- var mstsc = true;
- try { require('./rdp') } catch (ex) { mstsc = false; }
-
- // Check if Windows SSPI, LDAP, Passport and YubiKey OTP will be used
- var sspi = false;
- var ldap = false;
- var passport = [];
- var allsspi = true;
- var yubikey = false;
- var ssh = false;
- var sessionRecording = false;
- var domainCount = 0;
- var wildleek = false;
- var nodemailer = false;
- var sendgrid = false;
- var captcha = false;
- if (require('os').platform() == 'win32') { for (var i in config.domains) { domainCount++; if (config.domains[i].auth == 'sspi') { sspi = true; } else { allsspi = false; } } } else { allsspi = false; }
- if (domainCount == 0) { allsspi = false; }
- for (var i in config.domains) {
- if (i.startsWith('_')) continue;
- if (((config.domains[i].smtp != null) && (config.domains[i].smtp.name != 'console')) || (config.domains[i].sendmail != null)) { nodemailer = true; }
- if (config.domains[i].sendgrid != null) { sendgrid = true; }
- if (config.domains[i].yubikey != null) { yubikey = true; }
- if (config.domains[i].auth == 'ldap') { ldap = true; }
- if (mstsc == false) { config.domains[i].mstsc = false; }
- if (config.domains[i].ssh == true) { ssh = true; }
- if ((typeof config.domains[i].authstrategies == 'object')) {
- if (passport.indexOf('passport') == -1) { passport.push('passport','connect-flash'); } // Passport v0.6.0 requires a patch, see https://github.com/jaredhanson/passport/issues/904 and include connect-flash here to display errors
- if ((typeof config.domains[i].authstrategies.twitter == 'object') && (typeof config.domains[i].authstrategies.twitter.clientid == 'string') && (typeof config.domains[i].authstrategies.twitter.clientsecret == 'string') && (passport.indexOf('passport-twitter') == -1)) { passport.push('passport-twitter'); }
- if ((typeof config.domains[i].authstrategies.google == 'object') && (typeof config.domains[i].authstrategies.google.clientid == 'string') && (typeof config.domains[i].authstrategies.google.clientsecret == 'string') && (passport.indexOf('passport-google-oauth20') == -1)) { passport.push('passport-google-oauth20'); }
- if ((typeof config.domains[i].authstrategies.github == 'object') && (typeof config.domains[i].authstrategies.github.clientid == 'string') && (typeof config.domains[i].authstrategies.github.clientsecret == 'string') && (passport.indexOf('passport-github2') == -1)) { passport.push('passport-github2'); }
- if ((typeof config.domains[i].authstrategies.azure == 'object') && (typeof config.domains[i].authstrategies.azure.clientid == 'string') && (typeof config.domains[i].authstrategies.azure.clientsecret == 'string') && (typeof config.domains[i].authstrategies.azure.tenantid == 'string') && (passport.indexOf('passport-azure-oauth2') == -1)) { passport.push('passport-azure-oauth2'); passport.push('jwt-simple'); }
- if ((typeof config.domains[i].authstrategies.oidc == 'object') && (passport.indexOf('openid-client@5.7.1') == -1)) {
- if ((nodeVersion >= 17)
- || ((Math.floor(nodeVersion) == 16) && (nodeVersion >= 16.13))
- || ((Math.floor(nodeVersion) == 14) && (nodeVersion >= 14.15))
- || ((Math.floor(nodeVersion) == 12) && (nodeVersion >= 12.19))) {
- passport.push('openid-client@5.7.1');
- } else {
- addServerWarning('This NodeJS version does not support OpenID Connect on MeshCentral.', 25);
- delete config.domains[i].authstrategies.oidc;
- }
- }
- if ((typeof config.domains[i].authstrategies.saml == 'object') || (typeof config.domains[i].authstrategies.jumpcloud == 'object')) { passport.push('passport-saml'); }
- }
- if (config.domains[i].sessionrecording != null) { sessionRecording = true; }
- if ((config.domains[i].passwordrequirements != null) && (config.domains[i].passwordrequirements.bancommonpasswords == true)) { wildleek = true; }
- if ((config.domains[i].newaccountscaptcha != null) && (config.domains[i].newaccountscaptcha !== false)) { captcha = true; }
- if ((typeof config.domains[i].duo2factor == 'object') && (passport.indexOf('@duosecurity/duo_universal') == -1)) { passport.push('@duosecurity/duo_universal@2.1.0'); }
- }
-
- // Build the list of required modules
- // NOTE: ALL MODULES MUST HAVE A VERSION NUMBER AND THE VERSION MUST MATCH THAT USED IN Dockerfile
- var modules = ['archiver@7.0.1', 'body-parser@1.20.3', 'cbor@5.2.0', 'compression@1.8.1', 'cookie-session@2.1.1', 'express@4.21.2', 'express-handlebars@7.1.3', 'express-ws@5.0.2', 'ipcheck@0.1.0', 'minimist@1.2.8', 'multiparty@4.2.3', '@seald-io/nedb@4.1.2', 'node-forge@1.3.1', 'ua-parser-js@1.0.40', 'ua-client-hints-js@0.1.2', 'ws@8.18.3', 'yauzl@2.10.0'];
- if (require('os').platform() == 'win32') { modules.push('node-windows@0.1.14'); modules.push('loadavg-windows@1.1.1'); if (sspi == true) { modules.push('node-sspi@0.2.10'); } } // Add Windows modules
- if (ldap == true) { modules.push('ldapauth-fork@5.0.5'); }
- if (ssh == true) { modules.push('ssh2@1.17.0'); }
- if (passport != null) { modules.push(...passport); }
- if (captcha == true) { modules.push('svg-captcha@1.4.0'); }
-
- if (sessionRecording == true) { modules.push('image-size@2.0.2'); } // Need to get the remote desktop JPEG sizes to index the recording file.
- if (config.letsencrypt != null) { modules.push('acme-client@4.2.5'); } // Add acme-client module. We need to force v4.2.4 or higher since olver versions using SHA-1 which is no longer supported by Let's Encrypt.
- if (config.settings.mqtt != null) { modules.push('aedes@0.51.3'); } // Add MQTT Modules
- if (config.settings.mysql != null) { modules.push('mysql2@3.15.1'); } // Add MySQL.
- //if (config.settings.mysql != null) { modules.push('@mysql/xdevapi@8.0.33'); } // Add MySQL, official driver (https://dev.mysql.com/doc/dev/connector-nodejs/8.0/)
- if (config.settings.mongodb != null) { modules.push('mongodb@4.17.2'); modules.push('@mongodb-js/saslprep@1.3.1')} // Add MongoDB, official driver.
- if (config.settings.postgres != null) { modules.push('pg@8.16.3') } // Add Postgres, official driver.
- if (config.settings.mariadb != null) { modules.push('mariadb@3.4.5'); } // Add MariaDB, official driver.
- if (config.settings.acebase != null) { modules.push('acebase@1.29.5'); } // Add AceBase, official driver.
- if (config.settings.sqlite3 != null) { modules.push('sqlite3@5.1.7'); } // Add sqlite3, official driver.
- if (config.settings.vault != null) { modules.push('node-vault@0.10.5'); } // Add official HashiCorp's Vault module.
- const hasExistingProxy = process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy'];
- 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
- else if (config.settings.xmongodb != null) { modules.push('mongojs@3.1.0'); } // Add MongoJS, old driver.
- if (nodemailer || ((config.smtp != null) && (config.smtp.name != 'console')) || (config.sendmail != null)) { modules.push('nodemailer@6.10.1'); } // Add SMTP support
- if (sendgrid || (config.sendgrid != null)) { modules.push('@sendgrid/mail@8.1.6'); } // Add SendGrid support
- if ((args.translate || args.dev) && (Number(process.version.match(/^v(\d+\.\d+)/)[1]) >= 16)) { modules.push('jsdom@22.1.0'); modules.push('esprima@4.0.1'); modules.push('html-minifier-terser@7.2.0'); } // Translation support
- if (typeof config.settings.crowdsec == 'object') { modules.push('@crowdsec/express-bouncer@0.1.0'); } // Add CrowdSec bounser module (https://www.npmjs.com/package/@crowdsec/express-bouncer)
- if (config.settings.prometheus != null) { modules.push('prom-client@15.1.3'); } // Add Prometheus Metrics support
-
- if (typeof config.settings.autobackup == 'object') {
- // Setup encrypted zip support if needed
- if (config.settings.autobackup.zippassword) { modules.push('archiver-zip-encrypted@2.0.0'); }
- // Enable Google Drive Support
- if (typeof config.settings.autobackup.googledrive == 'object') { modules.push('googleapis@128.0.0'); }
- // Enable WebDAV Support
- if (typeof config.settings.autobackup.webdav == 'object') {
- if ((typeof config.settings.autobackup.webdav.url != 'string') || (typeof config.settings.autobackup.webdav.username != 'string') || (typeof config.settings.autobackup.webdav.password != 'string')) { addServerWarning("Missing WebDAV parameters.", 2, null, !args.launch); } else { modules.push('webdav@5.8.0'); }
- }
- // Enable S3 Support
- if (typeof config.settings.autobackup.s3 == 'object') { modules.push('minio@8.0.6'); }
- }
-
- // Setup common password blocking
- if (wildleek == true) { modules.push('wildleek@2.0.0'); }
-
- // Setup 2nd factor authentication
- if (config.settings.no2factorauth !== true) {
- // Setup YubiKey OTP if configured
- if (yubikey == true) { modules.push('yub@0.11.1'); } // Add YubiKey OTP support (replaced yubikeyotp due to form-data issues)
- if (allsspi == false) { modules.push('otplib@12.0.1'); } // Google Authenticator support (v10 supports older NodeJS versions).
- }
-
- // Desktop multiplexor support
- if (config.settings.desktopmultiplex === true) { modules.push('image-size@2.0.2'); }
-
- // SMS support
- if (config.sms != null) {
- if (config.sms.provider == 'twilio') { modules.push('twilio@4.23.0'); }
- if (config.sms.provider == 'plivo') { modules.push('plivo@4.75.1'); }
- if (config.sms.provider == 'telnyx') { modules.push('telnyx@1.25.5'); }
- }
-
- // Messaging support
- if (config.messaging != null) {
- if (config.messaging.telegram != null) { modules.push('telegram@2.26.22'); modules.push('input@1.0.1'); }
- if (config.messaging.discord != null) { if (nodeVersion >= 17) { modules.push('discord.js@14.6.0'); } else { delete config.messaging.discord; addServerWarning('This NodeJS version does not support Discord.js.', 26); } }
- if (config.messaging.xmpp != null) { modules.push('@xmpp/client@0.13.6'); }
- if (config.messaging.pushover != null) { modules.push('node-pushover@1.0.0'); }
- if (config.messaging.zulip != null) { modules.push('zulip@0.1.0'); }
- }
-
- // Setup web based push notifications
- if ((typeof config.settings.webpush == 'object') && (typeof config.settings.webpush.email == 'string')) { modules.push('web-push@3.6.7'); }
-
- // Firebase Support
- if ((config.firebase != null) && (typeof config.firebase.serviceaccountfile == 'string')) { modules.push('firebase-admin@12.7.0'); }
-
- // Syslog support
- if ((require('os').platform() != 'win32') && (config.settings.syslog || config.settings.syslogjson)) { modules.push('modern-syslog@1.2.0'); }
- if (config.settings.syslogtcp) { modules.push('syslog@0.1.1-1'); }
-
- // Setup heapdump support if needed, useful for memory leak debugging
- // https://www.arbazsiddiqui.me/a-practical-guide-to-memory-leaks-in-nodejs/
- if (config.settings.heapdump === true) { modules.push('heapdump@0.3.15'); }
-
- // Install any missing modules and launch the server
- InstallModules(modules, args, function () {
- if (require('os').platform() == 'win32') { try { require('node-windows'); } catch (ex) { console.log("Module node-windows can't be loaded. Restart MeshCentral."); process.exit(); return; } }
- meshserver = CreateMeshCentralServer(config, args);
- meshserver.Start();
- });
-
- // On exit, also terminate the child process if applicable
- process.on('exit', function () { if (childProcess) { childProcess.kill(); childProcess = null; } });
-
- // If our parent exits, we also exit
- if (args.launch) {
- process.stderr.on('end', function () { process.exit(); });
- process.stdout.on('end', function () { process.exit(); });
- process.stdin.on('end', function () { process.exit(); });
- process.stdin.on('data', function (data) { });
- }
- });
-}
-
-if (require.main === module) {
- mainStart(); // Called directly, launch normally.
-} else {
- module.exports.mainStart = mainStart; // Required as a module, useful for winservice.js
-}
+/**
+* @description MeshCentral main module
+* @author Ylian Saint-Hilaire
+* @copyright Intel Corporation 2018-2022
+* @license Apache-2.0
+* @version v0.0.1
+*/
+
+/*xjslint node: true */
+/*xjslint plusplus: true */
+/*xjslint maxlen: 256 */
+/*jshint node: true */
+/*jshint strict: false */
+/*jshint esversion: 6 */
+"use strict";
+
+const common = require('./common.js');
+
+// If app metrics is available
+if (process.argv[2] == '--launch') { try { require('appmetrics-dash').monitor({ url: '/', title: 'MeshCentral', port: 88, host: '127.0.0.1' }); } catch (ex) { } }
+
+function CreateMeshCentralServer(config, args) {
+ const obj = {};
+ obj.db = null;
+ obj.webserver = null; // HTTPS main web server, typically on port 443
+ obj.redirserver = null; // HTTP relay web server, typically on port 80
+ obj.mpsserver = null; // Intel AMT CIRA server, typically on port 4433
+ obj.mqttbroker = null; // MQTT server, not is not often used
+ obj.swarmserver = null; // Swarm server, this is used only to update older MeshCentral v1 agents
+ obj.smsserver = null; // SMS server, used to send user SMS messages
+ obj.msgserver = null; // Messaging server, used to sent used messages
+ obj.amtEventHandler = null;
+ obj.pluginHandler = null;
+ obj.amtScanner = null;
+ obj.amtManager = null; // Intel AMT manager, used to oversee all Intel AMT devices, activate them and sync policies
+ obj.meshScanner = null;
+ obj.taskManager = null;
+ obj.letsencrypt = null; // Let's encrypt server, used to get and renew TLS certificates
+ obj.eventsDispatch = {};
+ obj.fs = require('fs');
+ obj.path = require('path');
+ obj.crypto = require('crypto');
+ obj.exeHandler = require('./exeHandler.js');
+ obj.platform = require('os').platform();
+ obj.args = args;
+ obj.common = common;
+ obj.configurationFiles = null;
+ obj.certificates = null;
+ obj.connectivityByNode = {}; // This object keeps a list of all connected CIRA and agents, by nodeid->value (value: 1 = Agent, 2 = CIRA, 4 = AmtDirect)
+ obj.peerConnectivityByNode = {}; // This object keeps a list of all connected CIRA and agents of peers, by serverid->nodeid->value (value: 1 = Agent, 2 = CIRA, 4 = AmtDirect)
+ obj.debugSources = [];
+ obj.debugRemoteSources = null;
+ obj.config = config; // Configuration file
+ obj.dbconfig = {}; // Persistance values, loaded from database
+ obj.certificateOperations = null;
+ obj.defaultMeshCmd = null;
+ obj.defaultMeshCores = {};
+ obj.defaultMeshCoresDeflate = {};
+ obj.defaultMeshCoresHash = {};
+ obj.meshToolsBinaries = {}; // Mesh Tools Binaries, ToolName --> { hash:(sha384 hash), size:(binary size), path:(binary path) }
+ obj.meshAgentBinaries = {}; // Mesh Agent Binaries, Architecture type --> { hash:(sha384 hash), size:(binary size), path:(binary path) }
+ obj.meshAgentInstallScripts = {}; // Mesh Install Scripts, Script ID -- { hash:(sha384 hash), size:(binary size), path:(binary path) }
+ obj.multiServer = null;
+ obj.ipKvmManager = null;
+ obj.maintenanceTimer = null;
+ obj.serverId = null;
+ obj.serverKey = Buffer.from(obj.crypto.randomBytes(48), 'binary');
+ obj.loginCookieEncryptionKey = null;
+ obj.invitationLinkEncryptionKey = null;
+ obj.serverSelfWriteAllowed = true;
+ obj.serverStatsCounter = Math.floor(Math.random() * 1000);
+ obj.taskLimiter = obj.common.createTaskLimiterQueue(50, 20, 60); // (maxTasks, maxTaskTime, cleaningInterval) This is a task limiter queue to smooth out server work.
+ obj.agentUpdateBlockSize = 65531; // MeshAgent update block size
+ obj.serverWarnings = []; // List of warnings that should be shown to administrators
+ obj.cookieUseOnceTable = {}; // List of cookies that are already expired
+ obj.cookieUseOnceTableCleanCounter = 0; // Clean the cookieUseOnceTable each 20 additions
+ obj.firstStats = true; // True until this server saves it's not stats to the database
+
+ // Server version
+ obj.currentVer = null;
+ function getCurrentVersion() { try { obj.currentVer = JSON.parse(obj.fs.readFileSync(obj.path.join(__dirname, 'package.json'), 'utf8')).version; } catch (ex) { } return obj.currentVer; } // Fetch server version
+ getCurrentVersion();
+
+ // Setup the default configuration and files paths
+ if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) {
+ obj.parentpath = obj.path.join(__dirname, '../..');
+ obj.datapath = obj.path.join(__dirname, '../../meshcentral-data');
+ obj.filespath = obj.path.join(__dirname, '../../meshcentral-files');
+ obj.backuppath = obj.path.join(__dirname, '../../meshcentral-backups');
+ obj.recordpath = obj.path.join(__dirname, '../../meshcentral-recordings');
+ obj.webViewsPath = obj.path.join(__dirname, 'views');
+ obj.webPublicPath = obj.path.join(__dirname, 'public');
+ obj.webEmailsPath = obj.path.join(__dirname, 'emails');
+ if (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web/views'))) { obj.webViewsOverridePath = obj.path.join(__dirname, '../../meshcentral-web/views'); }
+ if (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web/public'))) { obj.webPublicOverridePath = obj.path.join(__dirname, '../../meshcentral-web/public'); }
+ if (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web/emails'))) { obj.webEmailsOverridePath = obj.path.join(__dirname, '../../meshcentral-web/emails'); }
+ } else {
+ obj.parentpath = __dirname;
+ obj.datapath = obj.path.join(__dirname, '../meshcentral-data');
+ obj.filespath = obj.path.join(__dirname, '../meshcentral-files');
+ obj.backuppath = obj.path.join(__dirname, '../meshcentral-backups');
+ obj.recordpath = obj.path.join(__dirname, '../meshcentral-recordings');
+ obj.webViewsPath = obj.path.join(__dirname, 'views');
+ obj.webPublicPath = obj.path.join(__dirname, 'public');
+ obj.webEmailsPath = obj.path.join(__dirname, 'emails');
+ if (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web/views'))) { obj.webViewsOverridePath = obj.path.join(__dirname, '../meshcentral-web/views'); }
+ if (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web/public'))) { obj.webPublicOverridePath = obj.path.join(__dirname, '../meshcentral-web/public'); }
+ if (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web/emails'))) { obj.webEmailsOverridePath = obj.path.join(__dirname, '../meshcentral-web/emails'); }
+ }
+
+ // Clean up any temporary files
+ const removeTime = new Date(Date.now()).getTime() - (30 * 60 * 1000); // 30 minutes
+ const dir = obj.fs.readdir(obj.path.join(obj.filespath, 'tmp'), function (err, files) {
+ if (err != null) return;
+ for (var i in files) { try { const filepath = obj.path.join(obj.filespath, 'tmp', files[i]); if (obj.fs.statSync(filepath).mtime.getTime() < removeTime) { obj.fs.unlink(filepath, function () { }); } } catch (ex) { } }
+ });
+
+ // Look to see if data and/or file path is specified
+ if (obj.config.settings && (typeof obj.config.settings.datapath == 'string')) { obj.datapath = obj.config.settings.datapath; }
+ if (obj.config.settings && (typeof obj.config.settings.filespath == 'string')) { obj.filespath = obj.config.settings.filespath; }
+
+ // Create data and files folders if needed
+ try { obj.fs.mkdirSync(obj.datapath); } catch (ex) { }
+ try { obj.fs.mkdirSync(obj.filespath); } catch (ex) { }
+
+ // Windows Specific Code, setup service and event log
+ obj.service = null;
+ obj.servicelog = null;
+ if (obj.platform == 'win32') {
+ const nodewindows = require('node-windows');
+ obj.service = nodewindows.Service;
+ const eventlogger = nodewindows.EventLogger;
+ obj.servicelog = new eventlogger('MeshCentral');
+ }
+
+ // Start the Meshcentral server
+ obj.Start = function () {
+ var i;
+ try { require('./pass').hash('test', function () { }, 0); } catch (ex) { console.log('Old version of node, must upgrade.'); return; } // TODO: Not sure if this test works or not.
+
+ // Check for invalid arguments
+ const validArguments = ['_', 'user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'rediraliasport', 'cert', 'mpscert', 'deletedomain', 'deletedefaultdomain', 'showall', 'showusers', 'showitem', 'listuserids', 'showusergroups', 'shownodes', 'showallmeshes', 'showmeshes', 'showevents', 'showsmbios', 'showpower', 'clearpower', 'showiplocations', 'help', 'exactports', 'xinstall', 'xuninstall', 'install', 'uninstall', 'start', 'stop', 'restart', 'debug', 'filespath', 'datapath', 'noagentupdate', 'launch', 'noserverbackup', 'mongodb', 'mongodbcol', 'wanonly', 'lanonly', 'nousers', 'mpspass', 'ciralocalfqdn', 'dbexport', 'dbexportmin', 'dbimport', 'dbmerge', 'dbfix', 'dbencryptkey', 'selfupdate', 'tlsoffload', 'usenodedefaulttlsciphers', 'tlsciphers', 'userallowedip', 'userblockedip', 'swarmallowedip', 'agentallowedip', 'agentblockedip', 'fastcert', 'swarmport', 'logintoken', 'logintokenkey', 'logintokengen', 'mailtokengen', 'admin', 'unadmin', 'sessionkey', 'sessiontime', 'minify', 'minifycore', 'dblistconfigfiles', 'dbshowconfigfile', 'dbpushconfigfiles', 'oldencrypt', 'dbpullconfigfiles', 'dbdeleteconfigfiles', 'vaultpushconfigfiles', 'vaultpullconfigfiles', 'vaultdeleteconfigfiles', 'configkey', 'loadconfigfromdb', 'npmpath', 'serverid', 'recordencryptionrecode', 'vault', 'token', 'unsealkey', 'name', 'log', 'dbstats', 'translate', 'createaccount', 'setuptelegram', 'resetaccount', 'pass', 'removesubdomain', 'adminaccount', 'domain', 'email', 'configfile', 'maintenancemode', 'nedbtodb', 'removetestagents', 'agentupdatetest', 'hashpassword', 'hashpass', 'indexmcrec', 'mpsdebug', 'dumpcores', 'dev', 'mysql', 'mariadb', 'trustedproxy'];
+ for (var arg in obj.args) { obj.args[arg.toLocaleLowerCase()] = obj.args[arg]; if (validArguments.indexOf(arg.toLocaleLowerCase()) == -1) { console.log('Invalid argument "' + arg + '", use --help.'); return; } }
+ const ENVVAR_PREFIX = "meshcentral_"
+ let envArgs = []
+ for (let [envvar, envval] of Object.entries(process.env)) {
+ if (envvar.toLocaleLowerCase().startsWith(ENVVAR_PREFIX)) {
+ let argname = envvar.slice(ENVVAR_PREFIX.length).toLocaleLowerCase()
+ if (!!argname && !(validArguments.indexOf(argname) == -1)) {
+ envArgs = envArgs.concat([`--${argname}`, envval])
+ }
+ }
+ }
+ envArgs = require('minimist')(envArgs)
+ obj.args = Object.assign(envArgs, obj.args)
+ if (obj.args.mongodb == true) { console.log('Must specify: --mongodb [connectionstring] \r\nSee https://docs.mongodb.com/manual/reference/connection-string/ for MongoDB connection string.'); return; }
+ if (obj.args.mysql == true) { console.log('Must specify: --mysql [connectionstring] \r\nExample mysql://user:password@127.0.0.1:3306/database'); return; }
+ if (obj.args.mariadb == true) { console.log('Must specify: --mariadb [connectionstring] \r\nExample mariadb://user:password@127.0.0.1:3306/database'); return; }
+ for (i in obj.config.settings) { obj.args[i] = obj.config.settings[i]; } // Place all settings into arguments, arguments have already been placed into settings so arguments take precedence.
+
+ if ((obj.args.help == true) || (obj.args['?'] == true)) {
+ console.log('MeshCentral v' + getCurrentVersion() + ', remote computer management web portal.');
+ console.log('This software is open source under Apache 2.0 license.');
+ console.log('Details at: https://www.meshcentral.com\r\n');
+ if ((obj.platform == 'win32') || (obj.platform == 'linux')) {
+ console.log('Run as a background service');
+ console.log(' --install/uninstall Install MeshCentral as a background service.');
+ console.log(' --start/stop/restart Control MeshCentral background service.');
+ console.log('');
+ console.log('Run standalone, console application');
+ }
+ console.log(' --user [username] Always login as [username] if account exists.');
+ console.log(' --port [number] Web server port number.');
+ console.log(' --redirport [number] Creates an additional HTTP server to redirect users to the HTTPS server.');
+ console.log(' --exactports Server must run with correct ports or exit.');
+ console.log(' --noagentupdate Server will not update mesh agent native binaries.');
+ console.log(' --nedbtodb Transfer all NeDB records into current database.');
+ console.log(' --listuserids Show a list of a user identifiers in the database.');
+ console.log(' --cert [name], (country), (org) Create a web server certificate with [name] server name.');
+ console.log(' country and organization can optionally be set.');
+ console.log('');
+ console.log('Server recovery commands, use only when MeshCentral is offline.');
+ console.log(' --createaccount [userid] Create a new user account.');
+ console.log(' --resetaccount [userid] Unlock an account, disable 2FA and set a new account password.');
+ console.log(' --adminaccount [userid] Promote account to site administrator.');
+ return;
+ }
+
+ // Fix a NeDB database
+ if (obj.args.dbfix) {
+ var lines = null, badJsonCount = 0, fieldNames = [], fixedDb = [];
+ try { lines = obj.fs.readFileSync(obj.getConfigFilePath(obj.args.dbfix), { encoding: 'utf8' }).split('\n'); } catch (ex) { console.log('Invalid file: ' + obj.args.dbfix + ': ' + ex); process.exit(); }
+ for (var i = 0; i < lines.length; i++) {
+ var x = null;
+ try { x = JSON.parse(lines[i]); } catch (ex) { badJsonCount++; }
+ if (x != null) { fixedDb.push(lines[i]); for (var j in x) { if (fieldNames.indexOf(j) == -1) { fieldNames.push(j); } } }
+ }
+ console.log('Lines: ' + lines.length + ', badJSON: ' + badJsonCount + ', Feilds: ' + fieldNames);
+ obj.fs.writeFileSync(obj.getConfigFilePath(obj.args.dbfix) + '-fixed', fixedDb.join('\n'), { encoding: 'utf8' });
+ return;
+ }
+
+ // Check for invalid cert name
+ if ((obj.args.cert != null) && ((typeof obj.args.cert != "string") || (obj.args.cert.indexOf('@') >= 0) || (obj.args.cert.indexOf('/') >= 0) || (obj.args.cert.indexOf(':') >= 0))) { console.log("Invalid certificate name"); process.exit(); return; }
+
+ // Perform a password hash
+ if (obj.args.hashpassword) { require('./pass').hash(obj.args.hashpassword, function (err, salt, hash, tag) { console.log(salt + ',' + hash); process.exit(); }); return; }
+
+ // Dump to mesh cores
+ if (obj.args.dumpcores) { obj.updateMeshCore(function () { console.log('Done.'); }, true); return; }
+
+ // Setup Telegram
+ if (obj.args.setuptelegram) { require('./meshmessaging.js').SetupTelegram(obj); return; }
+
+ // Perform web site translations into different languages
+ if (obj.args.translate) {
+ // Check NodeJS version
+ const NodeJSVer = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
+ if (NodeJSVer < 8) { console.log("Translation feature requires Node v8 or above, current version is " + process.version + "."); process.exit(); return; }
+
+ // Check if translate.json is in the "meshcentral-data" folder, if so use that and translate default pages.
+ var translationFile = null, customTranslation = false;
+ if (require('fs').existsSync(obj.path.join(obj.datapath, 'translate.json'))) { translationFile = obj.path.join(obj.datapath, 'translate.json'); console.log("Using translate.json in meshcentral-data."); customTranslation = true; }
+ if (translationFile == null) { if (require('fs').existsSync(obj.path.join(__dirname, 'translate', 'translate.json'))) { translationFile = obj.path.join(__dirname, 'translate', 'translate.json'); console.log("Using default translate.json."); } }
+ if (translationFile == null) { console.log("Unable to find translate.json."); process.exit(); return; }
+
+ // Perform translation operations
+ var didSomething = false;
+ process.chdir(obj.path.join(__dirname, 'translate'));
+ const translateEngine = require('./translate/translate.js')
+ if (customTranslation == true) {
+ // Translate all of the default files using custom translation file
+ translateEngine.startEx(['', '', 'minifyall']);
+ translateEngine.startEx(['', '', 'translateall', translationFile]);
+ translateEngine.startEx(['', '', 'extractall', translationFile]);
+ didSomething = true;
+ } else {
+ // Translate all of the default files
+ translateEngine.startEx(['', '', 'minifyall']);
+ translateEngine.startEx(['', '', 'translateall']);
+ translateEngine.startEx(['', '', 'extractall']);
+ didSomething = true;
+ }
+
+ // Check if "meshcentral-web" exists, if so, translate all pages in that folder.
+ if (obj.webViewsOverridePath != null) {
+ didSomething = true;
+ var files = obj.fs.readdirSync(obj.webViewsOverridePath);
+ for (var i in files) {
+ var file = obj.path.join(obj.webViewsOverridePath, files[i]);
+ if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) {
+ translateEngine.startEx(['', '', 'minify', file]);
+ }
+ }
+ files = obj.fs.readdirSync(obj.webViewsOverridePath);
+ for (var i in files) {
+ var file = obj.path.join(obj.webViewsOverridePath, files[i]);
+ if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) {
+ translateEngine.startEx(['', '', 'translate', '*', translationFile, file, '--subdir:translations']);
+ }
+ }
+ }
+
+ // Check domains and see if "meshcentral-web-DOMAIN" exists, if so, translate all pages in that folder
+ for (i in obj.config.domains) {
+ if (i == "") continue;
+ var path = obj.path.join(obj.datapath, '..', 'meshcentral-web-' + i, 'views');
+ if (require('fs').existsSync(path)) {
+ didSomething = true;
+ var files = obj.fs.readdirSync(path);
+ for (var a in files) {
+ var file = obj.path.join(path, files[a]);
+ if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) {
+ translateEngine.startEx(['', '', 'minify', file]);
+ }
+ }
+ files = obj.fs.readdirSync(path);
+ for (var a in files) {
+ var file = obj.path.join(path, files[a]);
+ if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) {
+ translateEngine.startEx(['', '', 'translate', '*', translationFile, file, '--subdir:translations']);
+ }
+ }
+ }
+ }
+ /*
+ if (obj.webPublicOverridePath != null) {
+ didSomething = true;
+ var files = obj.fs.readdirSync(obj.webPublicOverridePath);
+ for (var i in files) {
+ var file = obj.path.join(obj.webPublicOverridePath, files[i]);
+ if (file.endsWith('.htm') && !file.endsWith('-min.htm')) {
+ translateEngine.startEx(['', '', 'translate', '*', translationFile, file, '--subdir:translations']);
+ }
+ }
+ }
+ */
+
+ if (didSomething == false) { console.log("Nothing to do."); }
+ console.log('Finished Translating.')
+ process.exit();
+ return;
+ }
+
+ // Setup the Node+NPM path if possible, this makes it possible to update the server even if NodeJS and NPM are not in default paths.
+ if (obj.args.npmpath == null) {
+ try {
+ var nodepath = process.argv[0];
+ var npmpath = obj.path.join(obj.path.dirname(process.argv[0]), 'npm');
+ if (obj.fs.existsSync(nodepath) && obj.fs.existsSync(npmpath)) {
+ if (nodepath.indexOf(' ') >= 0) { nodepath = '"' + nodepath + '"'; }
+ if (npmpath.indexOf(' ') >= 0) { npmpath = '"' + npmpath + '"'; }
+ if (obj.platform == 'win32') { obj.args.npmpath = npmpath; } else { obj.args.npmpath = (nodepath + ' ' + npmpath); }
+ }
+ } catch (ex) { }
+ }
+
+ // Linux background service systemd handling
+ if (obj.platform == 'linux') {
+ if (obj.args.install == true) {
+ // Install MeshCentral in Systemd
+ console.log('Installing MeshCentral as background Service...');
+ var systemdConf = null;
+ const userinfo = require('os').userInfo();
+ if (require('fs').existsSync('/etc/systemd/system')) { systemdConf = '/etc/systemd/system/meshcentral.service'; }
+ else if (require('fs').existsSync('/lib/systemd/system')) { systemdConf = '/lib/systemd/system/meshcentral.service'; }
+ else if (require('fs').existsSync('/usr/lib/systemd/system')) { systemdConf = '/usr/lib/systemd/system/meshcentral.service'; }
+ else { console.log('Unable to find systemd configuration folder.'); process.exit(); return; }
+ console.log('Writing config file...');
+ require('child_process').exec('which node', {}, function (error, stdout, stderr) {
+ if ((error != null) || (stdout.indexOf('\n') == -1)) { console.log('ERROR: Unable to get node location: ' + error); process.exit(); return; }
+ const nodePath = stdout.substring(0, stdout.indexOf('\n'));
+ const config = '[Unit]\nDescription=MeshCentral Server\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nType=simple\nLimitNOFILE=1000000\nExecStart=' + nodePath + ' ' + __dirname + '/meshcentral\nWorkingDirectory=' + userinfo.homedir + '\nEnvironment=NODE_ENV=production\nUser=' + userinfo.username + '\nGroup=' + userinfo.username + '\nRestart=always\n# Restart service after 10 seconds if node service crashes\nRestartSec=10\n# Set port permissions capability\nAmbientCapabilities=cap_net_bind_service\n\n[Install]\nWantedBy=multi-user.target\n';
+ require('child_process').exec('echo \"' + config + '\" | sudo tee ' + systemdConf, {}, function (error, stdout, stderr) {
+ if ((error != null) && (error != '')) { console.log('ERROR: Unable to write config file: ' + error); process.exit(); return; }
+ console.log('Enabling service...');
+ require('child_process').exec('sudo systemctl enable meshcentral.service', {}, function (error, stdout, stderr) {
+ if ((error != null) && (error != '')) { console.log('ERROR: Unable to enable MeshCentral as a service: ' + error); process.exit(); return; }
+ if (stdout.length > 0) { console.log(stdout); }
+ console.log('Starting service...');
+ require('child_process').exec('sudo systemctl start meshcentral.service', {}, function (error, stdout, stderr) {
+ if ((error != null) && (error != '')) { console.log('ERROR: Unable to start MeshCentral as a service: ' + error); process.exit(); return; }
+ if (stdout.length > 0) { console.log(stdout); }
+ console.log('Done.');
+ });
+ });
+ });
+ });
+ return;
+ } else if (obj.args.uninstall == true) {
+ // Uninstall MeshCentral in Systemd
+ console.log('Uninstalling MeshCentral background service...');
+ var systemdConf = null;
+ if (require('fs').existsSync('/etc/systemd/system')) { systemdConf = '/etc/systemd/system/meshcentral.service'; }
+ else if (require('fs').existsSync('/lib/systemd/system')) { systemdConf = '/lib/systemd/system/meshcentral.service'; }
+ else if (require('fs').existsSync('/usr/lib/systemd/system')) { systemdConf = '/usr/lib/systemd/system/meshcentral.service'; }
+ else { console.log('Unable to find systemd configuration folder.'); process.exit(); return; }
+ console.log('Stopping service...');
+ require('child_process').exec('sudo systemctl stop meshcentral.service', {}, function (err, stdout, stderr) {
+ if ((err != null) && (err != '')) { console.log('ERROR: Unable to stop MeshCentral as a service: ' + err); }
+ if (stdout.length > 0) { console.log(stdout); }
+ console.log('Disabling service...');
+ require('child_process').exec('sudo systemctl disable meshcentral.service', {}, function (err, stdout, stderr) {
+ if ((err != null) && (err != '')) { console.log('ERROR: Unable to disable MeshCentral as a service: ' + err); }
+ if (stdout.length > 0) { console.log(stdout); }
+ console.log('Removing config file...');
+ require('child_process').exec('sudo rm ' + systemdConf, {}, function (err, stdout, stderr) {
+ if ((err != null) && (err != '')) { console.log('ERROR: Unable to delete MeshCentral config file: ' + err); }
+ console.log('Done.');
+ });
+ });
+ });
+ return;
+ } else if (obj.args.start == true) {
+ // Start MeshCentral in Systemd
+ require('child_process').exec('sudo systemctl start meshcentral.service', {}, function (err, stdout, stderr) {
+ if ((err != null) && (err != '')) { console.log('ERROR: Unable to start MeshCentral: ' + err); process.exit(); return; }
+ console.log('Done.');
+ });
+ return;
+ } else if (obj.args.stop == true) {
+ // Stop MeshCentral in Systemd
+ require('child_process').exec('sudo systemctl stop meshcentral.service', {}, function (err, stdout, stderr) {
+ if ((err != null) && (err != '')) { console.log('ERROR: Unable to stop MeshCentral: ' + err); process.exit(); return; }
+ console.log('Done.');
+ });
+ return;
+ } else if (obj.args.restart == true) {
+ // Restart MeshCentral in Systemd
+ require('child_process').exec('sudo systemctl restart meshcentral.service', {}, function (err, stdout, stderr) {
+ if ((err != null) && (err != '')) { console.log('ERROR: Unable to restart MeshCentral: ' + err); process.exit(); return; }
+ console.log('Done.');
+ });
+ return;
+ }
+ }
+
+ // FreeBSD background service handling, MUST USE SPAWN FOR SERVICE COMMANDS!
+ if (obj.platform == 'freebsd') {
+ if (obj.args.install == true) {
+ // Install MeshCentral in rc.d
+ console.log('Installing MeshCentral as background Service...');
+ var systemdConf = "/usr/local/etc/rc.d/meshcentral";
+ const userinfo = require('os').userInfo();
+ console.log('Writing config file...');
+ require('child_process').exec('which node', {}, function (error, stdout, stderr) {
+ if ((error != null) || (stdout.indexOf('\n') == -1)) { console.log('ERROR: Unable to get node location: ' + error); process.exit(); return; }
+ const nodePath = stdout.substring(0, stdout.indexOf('\n'));
+ const config = '#!/bin/sh\n# MeshCentral FreeBSD Service Script\n# PROVIDE: meshcentral\n# REQUIRE: NETWORKING\n# KEYWORD: shutdown\n. /etc/rc.subr\nname=meshcentral\nuser=' + userinfo.username + '\nrcvar=meshcentral_enable\n: \\${meshcentral_enable:=\\"NO\\"}\n: \\${meshcentral_args:=\\"\\"}\npidfile=/var/run/meshcentral/meshcentral.pid\ncommand=\\"/usr/sbin/daemon\\"\nmeshcentral_chdir=\\"' + obj.parentpath + '\\"\ncommand_args=\\"-r -u \\${user} -P \\${pidfile} -S -T meshcentral -m 3 ' + nodePath + ' ' + __dirname + ' \\${meshcentral_args}\\"\nload_rc_config \\$name\nrun_rc_command \\"\\$1\\"\n';
+ require('child_process').exec('echo \"' + config + '\" | tee ' + systemdConf + ' && chmod +x ' + systemdConf, {}, function (error, stdout, stderr) {
+ if ((error != null) && (error != '')) { console.log('ERROR: Unable to write config file: ' + error); process.exit(); return; }
+ console.log('Enabling service...');
+ require('child_process').exec('sysrc meshcentral_enable="YES"', {}, function (error, stdout, stderr) {
+ if ((error != null) && (error != '')) { console.log('ERROR: Unable to enable MeshCentral as a service: ' + error); process.exit(); return; }
+ if (stdout.length > 0) { console.log(stdout); }
+ console.log('Starting service...');
+ const service = require('child_process').spawn('service', ['meshcentral', 'start']);
+ service.stdout.on('data', function (data) { console.log(data.toString()); });
+ service.stderr.on('data', function (data) { console.log(data.toString()); });
+ service.on('exit', function (code) {
+ console.log((code === 0) ? 'Done.' : 'ERROR: Unable to start MeshCentral as a service');
+ process.exit(); // Must exit otherwise we just hang
+ });
+ });
+ });
+ });
+ return;
+ } else if (obj.args.uninstall == true) {
+ // Uninstall MeshCentral in rc.d
+ console.log('Uninstalling MeshCentral background service...');
+ var systemdConf = "/usr/local/etc/rc.d/meshcentral";
+ console.log('Stopping service...');
+ const service = require('child_process').spawn('service', ['meshcentral', 'stop']);
+ service.stdout.on('data', function (data) { console.log(data.toString()); });
+ service.stderr.on('data', function (data) { console.log(data.toString()); });
+ service.on('exit', function (code) {
+ if (code !== 0) { console.log('ERROR: Unable to stop MeshCentral as a service'); }
+ console.log('Disabling service...');
+ require('child_process').exec('sysrc -x meshcentral_enable', {}, function (err, stdout, stderr) {
+ if ((err != null) && (err != '')) { console.log('ERROR: Unable to disable MeshCentral as a service: ' + err); }
+ if (stdout.length > 0) { console.log(stdout); }
+ console.log('Removing config file...');
+ require('child_process').exec('rm ' + systemdConf, {}, function (err, stdout, stderr) {
+ if ((err != null) && (err != '')) { console.log('ERROR: Unable to delete MeshCentral config file: ' + err); }
+ console.log('Done.');
+ process.exit(); // Must exit otherwise we just hang
+ });
+ });
+ });
+ return;
+ } else if (obj.args.start == true) {
+ // Start MeshCentral in rc.d
+ const service = require('child_process').spawn('service', ['meshcentral', 'start']);
+ service.stdout.on('data', function (data) { console.log(data.toString()); });
+ service.stderr.on('data', function (data) { console.log(data.toString()); });
+ service.on('exit', function (code) {
+ console.log((code === 0) ? 'Done.' : 'ERROR: Unable to start MeshCentral as a service: ' + error);
+ process.exit(); // Must exit otherwise we just hang
+ });
+ return;
+ } else if (obj.args.stop == true) {
+ // Stop MeshCentral in rc.d
+ const service = require('child_process').spawn('service', ['meshcentral', 'stop']);
+ service.stdout.on('data', function (data) { console.log(data.toString()); });
+ service.stderr.on('data', function (data) { console.log(data.toString()); });
+ service.on('exit', function (code) {
+ console.log((code === 0) ? 'Done.' : 'ERROR: Unable to stop MeshCentral as a service: ' + error);
+ process.exit(); // Must exit otherwise we just hang
+ });
+ return;
+ } else if (obj.args.restart == true) {
+ // Restart MeshCentral in rc.d
+ const service = require('child_process').spawn('service', ['meshcentral', 'restart']);
+ service.stdout.on('data', function (data) { console.log(data.toString()); });
+ service.stderr.on('data', function (data) { console.log(data.toString()); });
+ service.on('exit', function (code) {
+ console.log((code === 0) ? 'Done.' : 'ERROR: Unable to restart MeshCentral as a service: ' + error);
+ process.exit(); // Must exit otherwise we just hang
+ });
+ return;
+ }
+ }
+
+ // Index a recorded file
+ if (obj.args.indexmcrec != null) {
+ if (typeof obj.args.indexmcrec != 'string') {
+ console.log('Usage: --indexmrec [filename.mcrec]');
+ } else if (obj.fs.existsSync(obj.args.indexmcrec)) {
+ console.log('Indexing file: ' + obj.args.indexmcrec);
+ require(require('path').join(__dirname, 'mcrec.js')).indexFile(obj.args.indexmcrec);
+ } else {
+ console.log('Unable to find file: ' + obj.args.indexmcrec);
+ }
+ return;
+ }
+
+ // Windows background service handling
+ if ((obj.platform == 'win32') && (obj.service != null)) {
+ // Build MeshCentral parent path and Windows Service path
+ var mcpath = __dirname;
+ if (mcpath.endsWith('\\node_modules\\meshcentral') || mcpath.endsWith('/node_modules/meshcentral')) { mcpath = require('path').join(mcpath, '..', '..'); }
+ const servicepath = obj.path.join(mcpath, 'WinService');
+
+ // Check if we need to install, start, stop, remove ourself as a background service
+ if (((obj.args.xinstall == true) || (obj.args.xuninstall == true) || (obj.args.start == true) || (obj.args.stop == true) || (obj.args.restart == true))) {
+ var env = [], xenv = ['user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'exactport', 'rediraliasport', 'debug'];
+ for (i in xenv) { if (obj.args[xenv[i]] != null) { env.push({ name: 'mesh' + xenv[i], value: obj.args[xenv[i]] }); } } // Set some args as service environment variables.
+
+ var serviceFilePath = null;
+ if (obj.fs.existsSync(obj.path.join(servicepath, 'winservice.js'))) { serviceFilePath = obj.path.join(servicepath, 'winservice.js'); }
+ else if (obj.fs.existsSync(obj.path.join(__dirname, '../WinService/winservice.js'))) { serviceFilePath = obj.path.join(__dirname, '../WinService/winservice.js'); }
+ else if (obj.fs.existsSync(obj.path.join(__dirname, 'winservice.js'))) { serviceFilePath = obj.path.join(__dirname, 'winservice.js'); }
+ if (serviceFilePath == null) { console.log('Unable to find winservice.js'); return; }
+
+ const svc = new obj.service({ name: 'MeshCentral', description: 'MeshCentral Remote Management Server', script: servicepath, env: env, wait: 2, grow: 0.5 });
+ svc.on('install', function () { console.log('MeshCentral service installed.'); svc.start(); });
+ svc.on('uninstall', function () { console.log('MeshCentral service uninstalled.'); process.exit(); });
+ svc.on('start', function () { console.log('MeshCentral service started.'); process.exit(); });
+ svc.on('stop', function () { console.log('MeshCentral service stopped.'); if (obj.args.stop) { process.exit(); } if (obj.args.restart) { console.log('Holding 5 seconds...'); setTimeout(function () { svc.start(); }, 5000); } });
+ svc.on('alreadyinstalled', function () { console.log('MeshCentral service already installed.'); process.exit(); });
+ svc.on('invalidinstallation', function () { console.log('Invalid MeshCentral service installation.'); process.exit(); });
+
+ if (obj.args.xinstall == true) { try { svc.install(); } catch (ex) { logException(ex); } }
+ if (obj.args.stop == true || obj.args.restart == true) { try { svc.stop(); } catch (ex) { logException(ex); } }
+ if (obj.args.start == true) { try { svc.start(); } catch (ex) { logException(ex); } }
+ if (obj.args.xuninstall == true) { try { svc.uninstall(); } catch (ex) { logException(ex); } }
+ return;
+ }
+
+ // Windows service install using the external winservice.js
+ if (obj.args.install == true) {
+ console.log('Installing MeshCentral as Windows Service...');
+ if (obj.fs.existsSync(servicepath) == false) { try { obj.fs.mkdirSync(servicepath); } catch (ex) { console.log('ERROR: Unable to create WinService folder: ' + ex); process.exit(); return; } }
+ try { obj.fs.createReadStream(obj.path.join(__dirname, 'winservice.js')).pipe(obj.fs.createWriteStream(obj.path.join(servicepath, 'winservice.js'))); } catch (ex) { console.log('ERROR: Unable to copy winservice.js: ' + ex); process.exit(); return; }
+ require('child_process').exec('node winservice.js --install', { maxBuffer: 512000, timeout: 120000, cwd: servicepath }, function (error, stdout, stderr) {
+ if ((error != null) && (error != '')) { console.log('ERROR: Unable to install MeshCentral as a service: ' + error); process.exit(); return; }
+ console.log(stdout);
+ });
+ return;
+ } else if (obj.args.uninstall == true) {
+ console.log('Uninstalling MeshCentral Windows Service...');
+ if (obj.fs.existsSync(servicepath) == true) {
+ require('child_process').exec('node winservice.js --uninstall', { maxBuffer: 512000, timeout: 120000, cwd: servicepath }, function (error, stdout, stderr) {
+ if ((error != null) && (error != '')) { console.log('ERROR: Unable to uninstall MeshCentral service: ' + error); process.exit(); return; }
+ console.log(stdout);
+ try { obj.fs.unlinkSync(obj.path.join(servicepath, 'winservice.js')); } catch (ex) { }
+ try { obj.fs.rmdirSync(servicepath); } catch (ex) { }
+ });
+ } else if (obj.fs.existsSync(obj.path.join(__dirname, '../WinService')) == true) {
+ require('child_process').exec('node winservice.js --uninstall', { maxBuffer: 512000, timeout: 120000, cwd: obj.path.join(__dirname, '../WinService') }, function (error, stdout, stderr) {
+ if ((error != null) && (error != '')) { console.log('ERROR: Unable to uninstall MeshCentral service: ' + error); process.exit(); return; }
+ console.log(stdout);
+ try { obj.fs.unlinkSync(obj.path.join(__dirname, '../WinService/winservice.js')); } catch (ex) { }
+ try { obj.fs.rmdirSync(obj.path.join(__dirname, '../WinService')); } catch (ex) { }
+ });
+ } else {
+ require('child_process').exec('node winservice.js --uninstall', { maxBuffer: 512000, timeout: 120000, cwd: __dirname }, function (error, stdout, stderr) {
+ if ((error != null) && (error != '')) { console.log('ERROR: Unable to uninstall MeshCentral service: ' + error); process.exit(); return; }
+ console.log(stdout);
+ });
+ }
+ return;
+ }
+ }
+
+ // If "--launch" is in the arguments, launch now
+ if (obj.args.launch) {
+ if (obj.args.vault) { obj.StartVault(); } else { obj.StartEx(); }
+ } else {
+ // if "--launch" is not specified, launch the server as a child process.
+ const startArgs = [];
+ for (i in process.argv) {
+ if (i > 0) {
+ const arg = process.argv[i];
+ if ((arg.length > 0) && ((arg.indexOf(' ') >= 0) || (arg.indexOf('&') >= 0))) { startArgs.push(arg); } else { startArgs.push(arg); }
+ }
+ }
+ startArgs.push('--launch', process.pid);
+ obj.launchChildServer(startArgs);
+ }
+ };
+
+ // Launch MeshCentral as a child server and monitor it.
+ obj.launchChildServer = function (startArgs) {
+ const child_process = require('child_process');
+ const isInspectorAttached = (()=> { try { return require('node:inspector').url() !== undefined; } catch (_) { return false; } }).call();
+ const logFromChildProcess = isInspectorAttached ? () => {} : console.log.bind(console);
+ try { if (process.traceDeprecation === true) { startArgs.unshift('--trace-deprecation'); } } catch (ex) { }
+ try { if (process.traceProcessWarnings === true) { startArgs.unshift('--trace-warnings'); } } catch (ex) { }
+ if (startArgs[0] != "--disable-proto=delete") startArgs.unshift("--disable-proto=delete")
+ childProcess = child_process.execFile(process.argv[0], startArgs, { maxBuffer: Infinity, cwd: obj.parentpath }, function (error, stdout, stderr) {
+ if (childProcess.xrestart == 1) {
+ setTimeout(function () { obj.launchChildServer(startArgs); }, 500); // This is an expected restart.
+ } else if (childProcess.xrestart == 2) {
+ console.log('Expected exit...');
+ process.exit(); // User CTRL-C exit.
+ } else if (childProcess.xrestart == 3) {
+ // Server self-update exit
+ var version = '';
+ if (typeof obj.args.selfupdate == 'string') { version = '@' + obj.args.selfupdate; }
+ else if (typeof obj.args.specificupdate == 'string') { version = '@' + obj.args.specificupdate; delete obj.args.specificupdate; }
+ const child_process = require('child_process');
+ const npmpath = ((typeof obj.args.npmpath == 'string') ? obj.args.npmpath : 'npm');
+ const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
+ const env = Object.assign({}, process.env); // Shallow clone
+ if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
+ // always use --save-exact - https://stackoverflow.com/a/64507176/1210734
+ const xxprocess = child_process.exec(npmpath + ' install --save-exact --no-audit meshcentral' + version + npmproxy, { maxBuffer: Infinity, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) {
+ if ((error != null) && (error != '')) { console.log('Update failed: ' + error); }
+ });
+ xxprocess.data = '';
+ xxprocess.stdout.on('data', function (data) { xxprocess.data += data; });
+ xxprocess.stderr.on('data', function (data) { xxprocess.data += data; });
+ xxprocess.on('close', function (code) {
+ if (code == 0) { console.log('Update completed...'); }
+
+ // Run the server updated script if present
+ if (typeof obj.config.settings.runonserverupdated == 'string') {
+ const child_process = require('child_process');
+ var parentpath = __dirname;
+ if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) { parentpath = require('path').join(__dirname, '../..'); }
+ child_process.exec(obj.config.settings.runonserverupdated + ' ' + getCurrentVersion(), { maxBuffer: 512000, timeout: 120000, cwd: parentpath }, function (error, stdout, stderr) { });
+ }
+
+ if (obj.args.cleannpmcacheonupdate === true) {
+ // Perform NPM cache clean
+ console.log('Cleaning NPM cache...');
+ const xxxprocess = child_process.exec(npmpath + ' cache clean --force', { maxBuffer: Infinity, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { });
+ xxxprocess.on('close', function (code) { setTimeout(function () { obj.launchChildServer(startArgs); }, 1000); });
+ } else {
+ // Run the updated server
+ setTimeout(function () { obj.launchChildServer(startArgs); }, 1000);
+ }
+ });
+ } else {
+ if (error != null) {
+ // This is an un-expected restart
+ console.log(error);
+ console.log('ERROR: MeshCentral failed with critical error, check mesherrors.txt. Restarting in 5 seconds...');
+ setTimeout(function () { obj.launchChildServer(startArgs); }, 5000);
+
+ // Run the server error script if present
+ if (typeof obj.config.settings.runonservererror == 'string') {
+ const child_process = require('child_process');
+ var parentpath = __dirname;
+ if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) { parentpath = require('path').join(__dirname, '../..'); }
+ child_process.exec(obj.config.settings.runonservererror + ' ' + getCurrentVersion(), { maxBuffer: 512000, timeout: 120000, cwd: parentpath }, function (error, stdout, stderr) { });
+ }
+ }
+ }
+ });
+ childProcess.stdout.on('data', function (data) {
+ if (data[data.length - 1] == '\n') { data = data.substring(0, data.length - 1); }
+ if (data.indexOf('Updating settings folder...') >= 0) { childProcess.xrestart = 1; }
+ else if (data.indexOf('Updating server certificates...') >= 0) { childProcess.xrestart = 1; }
+ else if (data.indexOf('Server Ctrl-C exit...') >= 0) { childProcess.xrestart = 2; }
+ else if (data.indexOf('Starting self upgrade...') >= 0) { childProcess.xrestart = 3; }
+ else if (data.indexOf('Server restart...') >= 0) { childProcess.xrestart = 1; }
+ else if (data.indexOf('Starting self upgrade to: ') >= 0) { obj.args.specificupdate = data.substring(26).split('\r')[0].split('\n')[0]; childProcess.xrestart = 3; }
+ var datastr = data;
+ while (datastr.endsWith('\r') || datastr.endsWith('\n')) { datastr = datastr.substring(0, datastr.length - 1); }
+ logFromChildProcess(datastr);
+ });
+ childProcess.stderr.on('data', function (data) {
+ var datastr = data;
+ while (datastr.endsWith('\r') || datastr.endsWith('\n')) { datastr = datastr.substring(0, datastr.length - 1); }
+ logFromChildProcess('ERR: ' + datastr);
+ if (data.startsWith('le.challenges[tls-sni-01].loopback')) { return; } // Ignore this error output from GreenLock
+ if (data[data.length - 1] == '\n') { data = data.substring(0, data.length - 1); }
+ obj.logError(data);
+ });
+ childProcess.on('close', function (code) { if ((code != 0) && (code != 123)) { /* console.log("Exited with code " + code); */ } });
+ };
+
+ obj.logError = function (err) {
+ try {
+ var errlogpath = null;
+ if (typeof obj.args.mesherrorlogpath == 'string') { errlogpath = obj.path.join(obj.args.mesherrorlogpath, 'mesherrors.txt'); } else { errlogpath = obj.getConfigFilePath('mesherrors.txt'); }
+ obj.fs.appendFileSync(errlogpath, '-------- ' + new Date().toLocaleString() + ' ---- ' + getCurrentVersion() + ' --------\r\n\r\n' + err + '\r\n\r\n\r\n');
+ } catch (ex) { console.log('ERROR: Unable to write to mesherrors.txt.'); }
+ };
+
+ // Get current and latest MeshCentral server versions using NPM
+ obj.getLatestServerVersion = function (callback) {
+ if (callback == null) return;
+ try {
+ if (typeof obj.args.selfupdate == 'string') { callback(getCurrentVersion(), obj.args.selfupdate); return; } // If we are targetting a specific version, return that one as current.
+ const child_process = require('child_process');
+ const npmpath = ((typeof obj.args.npmpath == 'string') ? obj.args.npmpath : 'npm');
+ const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
+ const env = Object.assign({}, process.env); // Shallow clone
+ if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
+ const xxprocess = child_process.exec(npmpath + npmproxy + ' view meshcentral dist-tags.latest', { maxBuffer: 512000, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { });
+ xxprocess.data = '';
+ xxprocess.stdout.on('data', function (data) { xxprocess.data += data; });
+ xxprocess.stderr.on('data', function (data) { });
+ xxprocess.on('close', function (code) {
+ var latestVer = null;
+ if (code == 0) { try { latestVer = xxprocess.data.split(' ').join('').split('\r').join('').split('\n').join(''); } catch (ex) { } }
+ callback(getCurrentVersion(), latestVer);
+ });
+ } catch (ex) { callback(getCurrentVersion(), null, ex); } // If the system is running out of memory, an exception here can easily happen.
+ };
+
+ // Get current version and all MeshCentral server tags using NPM
+ obj.getServerTags = function (callback) {
+ if (callback == null) return;
+ try {
+ if (typeof obj.args.selfupdate == 'string') { callback({ current: getCurrentVersion(), latest: obj.args.selfupdate }); return; } // If we are targetting a specific version, return that one as current.
+ const child_process = require('child_process');
+ const npmpath = ((typeof obj.args.npmpath == 'string') ? obj.args.npmpath : 'npm');
+ const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
+ const env = Object.assign({}, process.env); // Shallow clone
+ if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
+ const xxprocess = child_process.exec(npmpath + npmproxy + ' dist-tag ls meshcentral', { maxBuffer: 512000, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { });
+ xxprocess.data = '';
+ xxprocess.stdout.on('data', function (data) { xxprocess.data += data; });
+ xxprocess.stderr.on('data', function (data) { });
+ xxprocess.on('close', function (code) {
+ var tags = { current: getCurrentVersion() };
+ if (code == 0) {
+ try {
+ var lines = xxprocess.data.split('\r\n').join('\n').split('\n');
+ for (var i in lines) { var s = lines[i].split(': '); if ((s.length == 2) && (obj.args.npmtag == null) || (obj.args.npmtag == s[0])) { tags[s[0]] = s[1]; } }
+ } catch (ex) { }
+ }
+ callback(tags);
+ });
+ } catch (ex) { callback({ current: getCurrentVersion() }, ex); } // If the system is running out of memory, an exception here can easily happen.
+ };
+
+ // Use NPM to get list of versions
+ obj.getServerVersions = function (callback) {
+ try {
+ const child_process = require('child_process');
+ const npmpath = ((typeof obj.args.npmpath == 'string') ? obj.args.npmpath : 'npm');
+ const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
+ const env = Object.assign({}, process.env); // Shallow clone
+ if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
+ const xxprocess = child_process.exec(npmpath + npmproxy + ' view meshcentral versions --json', { maxBuffer: 512000, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { });
+ xxprocess.data = '';
+ xxprocess.stdout.on('data', function (data) { xxprocess.data += data; });
+ xxprocess.stderr.on('data', function (data) { });
+ xxprocess.on('close', function (code) {
+ (code == 0) ? callback(xxprocess.data) : callback('{}');
+ });
+ } catch (ex) { callback('{}'); }
+ };
+
+ // Initiate server self-update
+ obj.performServerUpdate = function (version) {
+ if (obj.serverSelfWriteAllowed != true) return false;
+ if ((version == null) || (version == '') || (typeof version != 'string')) { console.log('Starting self upgrade...'); } else { console.log('Starting self upgrade to: ' + version); }
+ process.exit(200);
+ return true;
+ };
+
+ // Initiate server self-update
+ obj.performServerCertUpdate = function () { console.log('Updating server certificates...'); process.exit(200); };
+
+ // Start by loading configuration from Vault
+ obj.StartVault = function () {
+ // Check that the configuration can only be loaded from one place
+ if ((obj.args.vault != null) && (obj.args.loadconfigfromdb != null)) { console.log("Can't load configuration from both database and Vault."); process.exit(); return; }
+
+ // Fix arguments if needed
+ if (typeof obj.args.vault == 'string') {
+ obj.args.vault = { endpoint: obj.args.vault };
+ if (typeof obj.args.token == 'string') { obj.args.vault.token = obj.args.token; }
+ if (typeof obj.args.unsealkey == 'string') { obj.args.vault.unsealkey = obj.args.unsealkey; }
+ if (typeof obj.args.name == 'string') { obj.args.vault.name = obj.args.name; }
+ }
+
+ // Load configuration for HashiCorp's Vault if needed
+ if (obj.args.vault) {
+ if (obj.args.vault.endpoint == null) { console.log('Missing Vault endpoint.'); process.exit(); return; }
+ if (obj.args.vault.token == null) { console.log('Missing Vault token.'); process.exit(); return; }
+ if (obj.args.vault.unsealkey == null) { console.log('Missing Vault unsealkey.'); process.exit(); return; }
+ if (obj.args.vault.name == null) { obj.args.vault.name = 'meshcentral'; }
+
+ // Get new instance of the client
+ const vault = require("node-vault")({ endpoint: obj.args.vault.endpoint, token: obj.args.vault.token });
+ vault.unseal({ key: obj.args.vault.unsealkey })
+ .then(function () {
+ if (obj.args.vaultdeleteconfigfiles) {
+ vault.delete('secret/data/' + obj.args.vault.name)
+ .then(function (r) { console.log('Done.'); process.exit(); })
+ .catch(function (x) { console.log(x); process.exit(); });
+ } else if (obj.args.vaultpushconfigfiles) {
+ // Push configuration files into Vault
+ if ((obj.args.vaultpushconfigfiles == '*') || (obj.args.vaultpushconfigfiles === true)) { obj.args.vaultpushconfigfiles = obj.datapath; }
+ obj.fs.readdir(obj.args.vaultpushconfigfiles, function (err, files) {
+ if (err != null) { console.log('ERROR: Unable to read from folder ' + obj.args.vaultpushconfigfiles); process.exit(); return; }
+ var configFound = false;
+ for (var i in files) { if (files[i] == 'config.json') { configFound = true; } }
+ if (configFound == false) { console.log('ERROR: No config.json in folder ' + obj.args.vaultpushconfigfiles); process.exit(); return; }
+ var configFiles = {};
+ for (var i in files) {
+ const file = files[i];
+ if ((file == 'config.json') || file.endsWith('.key') || file.endsWith('.crt') || (file == 'terms.txt') || file.endsWith('.jpg') || file.endsWith('.png')) {
+ const path = obj.path.join(obj.args.vaultpushconfigfiles, files[i]), binary = Buffer.from(obj.fs.readFileSync(path, { encoding: 'binary' }), 'binary');
+ console.log('Pushing ' + file + ', ' + binary.length + ' bytes.');
+ if (file.endsWith('.json') || file.endsWith('.key') || file.endsWith('.crt')) { configFiles[file] = binary.toString(); } else { configFiles[file] = binary.toString('base64'); }
+ }
+ }
+ vault.write('secret/data/' + obj.args.vault.name, { "data": configFiles })
+ .then(function (r) { console.log('Done.'); process.exit(); })
+ .catch(function (x) { console.log(x); process.exit(); });
+ });
+ } else {
+ // Read configuration files from Vault
+ vault.read('secret/data/' + obj.args.vault.name)
+ .then(function (r) {
+ if ((r == null) || (r.data == null) || (r.data.data == null)) { console.log('Unable to read configuration from Vault.'); process.exit(); return; }
+ var configFiles = obj.configurationFiles = r.data.data;
+
+ // Decode Base64 when needed
+ for (var file in configFiles) { if (!file.endsWith('.json') && !file.endsWith('.key') && !file.endsWith('.crt')) { configFiles[file] = Buffer.from(configFiles[file], 'base64'); } }
+
+ // Save all of the files
+ if (obj.args.vaultpullconfigfiles) {
+ for (var i in configFiles) {
+ var fullFileName = obj.path.join(obj.args.vaultpullconfigfiles, i);
+ try { obj.fs.writeFileSync(fullFileName, configFiles[i]); } catch (ex) { console.log('Unable to write to ' + fullFileName); process.exit(); return; }
+ console.log('Pulling ' + i + ', ' + configFiles[i].length + ' bytes.');
+ }
+ console.log('Done.');
+ process.exit();
+ }
+
+ // Parse the new configuration file
+ var config2 = null;
+ try { config2 = JSON.parse(configFiles['config.json']); } catch (ex) { console.log('Error, unable to parse config.json from Vault.'); process.exit(); return; }
+
+ // Set the command line arguments to the config file if they are not present
+ if (!config2.settings) { config2.settings = {}; }
+ for (var i in args) { config2.settings[i] = args[i]; }
+ obj.args = args = config2.settings;
+
+ // Lower case all keys in the config file
+ obj.common.objKeysToLower(config2, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'telegram/proxy']);
+
+ // Grad some of the values from the original config.json file if present.
+ if ((config.settings.vault != null) && (config2.settings != null)) { config2.settings.vault = config.settings.vault; }
+
+ // We got a new config.json from the database, let's use it.
+ config = obj.config = config2;
+ obj.StartEx();
+ })
+ .catch(function (x) { console.log(x); process.exit(); });
+ }
+ }).catch(function (x) { console.log(x); process.exit(); });
+ return;
+ }
+ }
+
+ // Look for easy command line instructions and do them here.
+ obj.StartEx = async function () {
+ var i;
+ //var wincmd = require('node-windows');
+ //wincmd.list(function (svc) { console.log(svc); }, true);
+
+ // Setup syslog support. Not supported on Windows.
+ if ((require('os').platform() != 'win32') && ((config.settings.syslog != null) || (config.settings.syslogjson != null) || (config.settings.syslogauth != null))) {
+ if (config.settings.syslog === true) { config.settings.syslog = 'meshcentral'; }
+ if (config.settings.syslogjson === true) { config.settings.syslogjson = 'meshcentral-json'; }
+ if (config.settings.syslogauth === true) { config.settings.syslogauth = 'meshcentral-auth'; }
+ if (typeof config.settings.syslog == 'string') {
+ obj.syslog = require('modern-syslog');
+ console.log('Starting ' + config.settings.syslog + ' syslog.');
+ obj.syslog.init(config.settings.syslog, obj.syslog.LOG_PID | obj.syslog.LOG_ODELAY, obj.syslog.LOG_LOCAL0);
+ obj.syslog.log(obj.syslog.LOG_INFO, "MeshCentral v" + getCurrentVersion() + " Server Start");
+ }
+ if (typeof config.settings.syslogjson == 'string') {
+ obj.syslogjson = require('modern-syslog');
+ console.log('Starting ' + config.settings.syslogjson + ' JSON syslog.');
+ obj.syslogjson.init(config.settings.syslogjson, obj.syslogjson.LOG_PID | obj.syslogjson.LOG_ODELAY, obj.syslogjson.LOG_LOCAL0);
+ obj.syslogjson.log(obj.syslogjson.LOG_INFO, "MeshCentral v" + getCurrentVersion() + " Server Start");
+ }
+ if (typeof config.settings.syslogauth == 'string') {
+ obj.syslogauth = require('modern-syslog');
+ console.log('Starting ' + config.settings.syslogauth + ' auth syslog.');
+ obj.syslogauth.init(config.settings.syslogauth, obj.syslogauth.LOG_PID | obj.syslogauth.LOG_ODELAY, obj.syslogauth.LOG_LOCAL0);
+ obj.syslogauth.log(obj.syslogauth.LOG_INFO, "MeshCentral v" + getCurrentVersion() + " Server Start");
+ }
+ }
+ // Setup TCP syslog support, this works on all OS's.
+ if (config.settings.syslogtcp != null) {
+ const syslog = require('syslog');
+ if (config.settings.syslogtcp === true) {
+ obj.syslogtcp = syslog.createClient(514, 'localhost');
+ } else {
+ const sp = config.settings.syslogtcp.split(':');
+ obj.syslogtcp = syslog.createClient(parseInt(sp[1]), sp[0]);
+ }
+ obj.syslogtcp.log("MeshCentral v" + getCurrentVersion() + " Server Start", obj.syslogtcp.LOG_INFO);
+ }
+
+ // Check top level configuration for any unrecognized values
+ if (config) { for (var i in config) { if ((typeof i == 'string') && (i.length > 0) && (i[0] != '_') && (['settings', 'domaindefaults', 'domains', 'configfiles', 'smtp', 'letsencrypt', 'peers', 'sms', 'messaging', 'sendgrid', 'sendmail', 'firebase', 'firebaserelay', '$schema'].indexOf(i) == -1)) { addServerWarning('Unrecognized configuration option \"' + i + '\".', 3, [i]); } } }
+
+ // Read IP lists from files if applicable
+ config.settings.userallowedip = obj.args.userallowedip = readIpListFromFile(obj.args.userallowedip);
+ config.settings.userblockedip = obj.args.userblockedip = readIpListFromFile(obj.args.userblockedip);
+ config.settings.agentallowedip = obj.args.agentallowedip = readIpListFromFile(obj.args.agentallowedip);
+ config.settings.agentblockedip = obj.args.agentblockedip = readIpListFromFile(obj.args.agentblockedip);
+ config.settings.swarmallowedip = obj.args.swarmallowedip = readIpListFromFile(obj.args.swarmallowedip);
+
+ // Check IP lists and ranges
+ if (typeof obj.args.userallowedip == 'string') { if (obj.args.userallowedip == '') { config.settings.userallowedip = obj.args.userallowedip = null; } else { config.settings.userallowedip = obj.args.userallowedip = obj.args.userallowedip.split(' ').join('').split(','); } }
+ if (typeof obj.args.userblockedip == 'string') { if (obj.args.userblockedip == '') { config.settings.userblockedip = obj.args.userblockedip = null; } else { config.settings.userblockedip = obj.args.userblockedip = obj.args.userblockedip.split(' ').join('').split(','); } }
+ if (typeof obj.args.agentallowedip == 'string') { if (obj.args.agentallowedip == '') { config.settings.agentallowedip = obj.args.agentallowedip = null; } else { config.settings.agentallowedip = obj.args.agentallowedip = obj.args.agentallowedip.split(' ').join('').split(','); } }
+ if (typeof obj.args.agentblockedip == 'string') { if (obj.args.agentblockedip == '') { config.settings.agentblockedip = obj.args.agentblockedip = null; } else { config.settings.agentblockedip = obj.args.agentblockedip = obj.args.agentblockedip.split(' ').join('').split(','); } }
+ if (typeof obj.args.swarmallowedip == 'string') { if (obj.args.swarmallowedip == '') { obj.args.swarmallowedip = null; } else { obj.args.swarmallowedip = obj.args.swarmallowedip.split(' ').join('').split(','); } }
+ if ((typeof obj.args.agentupdateblocksize == 'number') && (obj.args.agentupdateblocksize >= 1024) && (obj.args.agentupdateblocksize <= 65531)) { obj.agentUpdateBlockSize = obj.args.agentupdateblocksize; }
+ if (typeof obj.args.trustedproxy == 'string') { obj.args.trustedproxy = obj.args.trustedproxy.split(' ').join('').split(','); }
+ if (typeof obj.args.tlsoffload == 'string') { obj.args.tlsoffload = obj.args.tlsoffload.split(' ').join('').split(','); }
+
+ // Check IP lists and ranges and if DNS return IP addresses
+ config.settings.userallowedip = await resolveDomainsToIps(config.settings.userallowedip);
+ config.settings.userblockedip = await resolveDomainsToIps(config.settings.userblockedip);
+ config.settings.agentallowedip = await resolveDomainsToIps(config.settings.agentallowedip);
+ config.settings.agentblockedip = await resolveDomainsToIps(config.settings.agentblockedip);
+ config.settings.swarmallowedip = await resolveDomainsToIps(config.settings.swarmallowedip);
+
+ // Check the "cookieIpCheck" value
+ if ((obj.args.cookieipcheck === false) || (obj.args.cookieipcheck == 'none')) { obj.args.cookieipcheck = 'none'; }
+ else if ((typeof obj.args.cookieipcheck != 'string') || (obj.args.cookieipcheck.toLowerCase() != 'strict')) { obj.args.cookieipcheck = 'lax'; }
+ else { obj.args.cookieipcheck = 'strict'; }
+
+ // Check the "cookieSameSite" value
+ if (typeof obj.args.cookiesamesite != 'string') { delete obj.args.cookiesamesite; }
+ else if (['none', 'lax', 'strict'].indexOf(obj.args.cookiesamesite.toLowerCase()) == -1) { delete obj.args.cookiesamesite; } else { obj.args.cookiesamesite = obj.args.cookiesamesite.toLowerCase(); }
+
+ // Check if WebSocket compression is supported. It's known to be broken in NodeJS v11.11 to v12.15, and v13.2
+ const verSplit = process.version.substring(1).split('.');
+ const ver = parseInt(verSplit[0]) + (parseInt(verSplit[1]) / 100);
+ if (((ver >= 11.11) && (ver <= 12.15)) || (ver == 13.2)) {
+ if ((obj.args.wscompression === true) || (obj.args.agentwscompression === true)) { addServerWarning('WebSocket compression is disabled, this feature is broken in NodeJS v11.11 to v12.15 and v13.2', 4); }
+ obj.args.wscompression = obj.args.agentwscompression = false;
+ obj.config.settings.wscompression = obj.config.settings.agentwscompression = false;
+ }
+
+ // Local console tracing
+ if (typeof obj.args.debug == 'string') { obj.debugSources = obj.args.debug.toLowerCase().split(','); }
+ else if (typeof obj.args.debug == 'object') { obj.debugSources = obj.args.debug; }
+ else if (obj.args.debug === true) { obj.debugSources = '*'; }
+
+ require('./db.js').CreateDB(obj,
+ function (db) {
+ obj.db = db;
+ obj.db.SetupDatabase(function (dbversion) {
+ // See if any database operations needs to be completed
+ if (obj.args.deletedomain) { obj.db.DeleteDomain(obj.args.deletedomain, function () { console.log('Deleted domain ' + obj.args.deletedomain + '.'); process.exit(); }); return; }
+ if (obj.args.deletedefaultdomain) { obj.db.DeleteDomain('', function () { console.log('Deleted default domain.'); process.exit(); }); return; }
+ if (obj.args.showall) { obj.db.GetAll(function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
+ if (obj.args.showusers) { obj.db.GetAllType('user', function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
+ if (obj.args.showitem) { obj.db.Get(obj.args.showitem, function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
+ if (obj.args.listuserids) { obj.db.GetAllType('user', function (err, docs) { for (var i in docs) { console.log(docs[i]._id); } process.exit(); }); return; }
+ if (obj.args.showusergroups) { obj.db.GetAllType('ugrp', function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
+ if (obj.args.shownodes) { obj.db.GetAllType('node', function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
+ if (obj.args.showallmeshes) { obj.db.GetAllType('mesh', function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
+ if (obj.args.showmeshes) { obj.db.GetAllType('mesh', function (err, docs) { var x = []; for (var i in docs) { if (docs[i].deleted == null) { x.push(docs[i]); } } console.log(JSON.stringify(x, null, 2)); process.exit(); }); return; }
+ if (obj.args.showevents) { obj.db.GetAllEvents(function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
+ if (obj.args.showsmbios) { obj.db.GetAllSMBIOS(function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
+ if (obj.args.showpower) { obj.db.getAllPower(function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
+ if (obj.args.clearpower) { obj.db.removeAllPowerEvents(function () { process.exit(); }); return; }
+ if (obj.args.showiplocations) { obj.db.GetAllType('iploc', function (err, docs) { console.log(docs); process.exit(); }); return; }
+ if (obj.args.logintoken) { obj.getLoginToken(obj.args.logintoken, function (r) { console.log(r); process.exit(); }); return; }
+ if (obj.args.logintokenkey) { obj.showLoginTokenKey(function (r) { console.log(r); process.exit(); }); return; }
+ if (obj.args.recordencryptionrecode) { obj.db.performRecordEncryptionRecode(function (count) { console.log('Re-encoded ' + count + ' record(s).'); process.exit(); }); return; }
+ if (obj.args.dbstats) { obj.db.getDbStats(function (stats) { console.log(stats); process.exit(); }); return; }
+ if (obj.args.createaccount) { // Create a new user account
+ if ((typeof obj.args.createaccount != 'string') || ((obj.args.pass == null) && (obj.args.hashpass == null)) || (obj.args.pass == '') || (obj.args.hashpass == '') || (obj.args.createaccount.indexOf(' ') >= 0)) { console.log("Usage: --createaccount [userid] --pass [password] --domain (domain) --email (email) --name (name)."); process.exit(); return; }
+ var userid = 'user/' + (obj.args.domain ? obj.args.domain : '') + '/' + obj.args.createaccount.toLowerCase(), domainid = obj.args.domain ? obj.args.domain : '';
+ if (obj.args.createaccount.startsWith('user/')) { userid = obj.args.createaccount; domainid = obj.args.createaccount.split('/')[1]; }
+ if (userid.split('/').length != 3) { console.log("Invalid userid."); process.exit(); return; }
+ obj.db.Get(userid, function (err, docs) {
+ if (err != null) { console.log("Database error: " + err); process.exit(); return; }
+ if ((docs != null) && (docs.length != 0)) { console.log('User already exists.'); process.exit(); return; }
+ if ((domainid != '') && ((config.domains == null) || (config.domains[domainid] == null))) { console.log("Invalid domain."); process.exit(); return; }
+ const user = { _id: userid, type: 'user', name: (typeof obj.args.name == 'string') ? obj.args.name : (userid.split('/')[2]), domain: domainid, creation: Math.floor(Date.now() / 1000), links: {} };
+ if (typeof obj.args.email == 'string') { user.email = obj.args.email; user.emailVerified = true; }
+ if (obj.args.hashpass) {
+ // Create an account using a pre-hashed password. Use --hashpassword to pre-hash a password.
+ var hashpasssplit = obj.args.hashpass.split(',');
+ if (hashpasssplit.length != 2) { console.log("Invalid hashed password."); process.exit(); return; }
+ user.salt = hashpasssplit[0];
+ user.hash = hashpasssplit[1];
+ obj.db.Set(user, function () { console.log("Done. This command will only work if MeshCentral is stopped."); process.exit(); return; });
+ } else {
+ // Hash the password and create the account.
+ require('./pass').hash(obj.args.pass, function (err, salt, hash, tag) { if (err) { console.log("Unable create account password: " + err); process.exit(); return; } user.salt = salt; user.hash = hash; obj.db.Set(user, function () { console.log("Done."); process.exit(); return; }); }, 0);
+ }
+ });
+ return;
+ }
+ if (obj.args.resetaccount) { // Unlock a user account, set a new password and remove 2FA
+ if ((typeof obj.args.resetaccount != 'string') || (obj.args.resetaccount.indexOf(' ') >= 0)) { console.log("Usage: --resetaccount [userid] --domain (domain) --pass [password]."); process.exit(); return; }
+ var userid = 'user/' + (obj.args.domain ? obj.args.domain : '') + '/' + obj.args.resetaccount.toLowerCase();
+ if (obj.args.resetaccount.startsWith('user/')) { userid = obj.args.resetaccount; }
+ if (userid.split('/').length != 3) { console.log("Invalid userid."); process.exit(); return; }
+ obj.db.Get(userid, function (err, docs) {
+ if (err != null) { console.log("Database error: " + err); process.exit(); return; }
+ if ((docs == null) || (docs.length == 0)) { console.log("Unknown userid, usage: --resetaccount [userid] --domain (domain) --pass [password]."); process.exit(); return; }
+ const user = docs[0]; if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { user.siteadmin -= 32; } // Unlock the account.
+ delete user.phone; delete user.otpekey; delete user.otpsecret; delete user.otpkeys; delete user.otphkeys; delete user.otpdev; delete user.otpsms; delete user.otpmsg; user.otpduo; // Disable 2FA
+ delete user.msghandle; // Disable users 2fa messaging too
+ var config = getConfig(false);
+ if (config.domains[user.domain].auth || config.domains[user.domain].authstrategies) {
+ console.log('This users domain has external authentication methods enabled so the password will not be changed if you set one')
+ obj.db.Set(user, function () { console.log("Done."); process.exit(); return; });
+ } else {
+ if (obj.args.hashpass && (typeof obj.args.hashpass == 'string')) {
+ // Reset an account using a pre-hashed password. Use --hashpassword to pre-hash a password.
+ var hashpasssplit = obj.args.hashpass.split(',');
+ if (hashpasssplit.length != 2) { console.log("Invalid hashed password."); process.exit(); return; }
+ user.salt = hashpasssplit[0];
+ user.hash = hashpasssplit[1];
+ obj.db.Set(user, function () { console.log("Done. This command will only work if MeshCentral is stopped."); process.exit(); return; });
+ } else if (obj.args.pass && (typeof obj.args.pass == 'string')) {
+ // Hash the password and reset the account.
+ require('./pass').hash(String(obj.args.pass), user.salt, function (err, hash, tag) {
+ if (err) { console.log("Unable to reset password: " + err); process.exit(); return; }
+ user.hash = hash;
+ obj.db.Set(user, function () { console.log("Done."); process.exit(); return; });
+ }, 0);
+ } else {
+ console.log('Not setting a users password');
+ obj.db.Set(user, function () { console.log("Done."); process.exit(); return; });
+ }
+ }
+ });
+ return;
+ }
+ if (obj.args.adminaccount) { // Set a user account to server administrator
+ if ((typeof obj.args.adminaccount != 'string') || (obj.args.adminaccount.indexOf(' ') >= 0)) { console.log("Invalid userid, usage: --adminaccount [username] --domain (domain)"); process.exit(); return; }
+ var userid = 'user/' + (obj.args.domain ? obj.args.domain : '') + '/' + obj.args.adminaccount.toLowerCase();
+ if (obj.args.adminaccount.startsWith('user/')) { userid = obj.args.adminaccount; }
+ if (userid.split('/').length != 3) { console.log("Invalid userid."); process.exit(); return; }
+ obj.db.Get(userid, function (err, docs) {
+ if (err != null) { console.log("Database error: " + err); process.exit(); return; }
+ if ((docs == null) || (docs.length == 0)) { console.log("Unknown userid, usage: --adminaccount [userid] --domain (domain)."); process.exit(); return; }
+ docs[0].siteadmin = 0xFFFFFFFF; // Set user as site administrator
+ obj.db.Set(docs[0], function () { console.log("Done. This command will only work if MeshCentral is stopped."); process.exit(); return; });
+ });
+ return;
+ }
+ if (obj.args.removesubdomain) { // Remove all references to a sub domain from the database
+ if ((typeof obj.args.removesubdomain != 'string') || (obj.args.removesubdomain.indexOf(' ') >= 0)) { console.log("Invalid sub domain, usage: --removesubdomain [domain]"); process.exit(); return; }
+ obj.db.removeDomain(obj.args.removesubdomain, function () { console.log("Done."); process.exit(); return; });
+ return;
+ }
+ if (obj.args.removetestagents) { // Remove all test agents from the database
+ db.GetAllType('node', function (err, docs) {
+ if ((err != null) || (docs.length == 0)) {
+ console.log('Unable to get any nodes from the database');
+ process.exit(0);
+ } else {
+ // Load all users
+ const allusers = {}, removeCount = 0;
+ obj.db.GetAllType('user', function (err, docs) {
+ obj.common.unEscapeAllLinksFieldName(docs);
+ for (i in docs) { allusers[docs[i]._id] = docs[i]; }
+ });
+
+ // Look at all devices
+ for (var i in docs) {
+ if ((docs[i] != null) && (docs[i].agent != null) && (docs[i].agent.id == 23)) {
+ // Remove this test node
+ const node = docs[i];
+
+ // Delete this node including network interface information, events and timeline
+ removeCount++;
+ db.Remove(node._id); // Remove node with that id
+ db.Remove('if' + node._id); // Remove interface information
+ db.Remove('nt' + node._id); // Remove notes
+ db.Remove('lc' + node._id); // Remove last connect time
+ db.Remove('si' + node._id); // Remove system information
+ if (db.RemoveSMBIOS) { db.RemoveSMBIOS(node._id); } // Remove SMBios data
+ db.RemoveAllNodeEvents(node._id); // Remove all events for this node
+ db.removeAllPowerEventsForNode(node._id); // Remove all power events for this node
+ if (typeof node.pmt == 'string') { db.Remove('pmt_' + node.pmt); } // Remove Push Messaging Token
+ db.Get('ra' + node._id, function (err, nodes) {
+ if ((nodes != null) && (nodes.length == 1)) { db.Remove('da' + nodes[0].daid); } // Remove diagnostic agent to real agent link
+ db.Remove('ra' + node._id); // Remove real agent to diagnostic agent link
+ });
+
+ // Remove any user node links
+ if (node.links != null) {
+ for (var i in node.links) {
+ if (i.startsWith('user/')) {
+ var cuser = allusers[i];
+ if ((cuser != null) && (cuser.links != null) && (cuser.links[node._id] != null)) {
+ // Remove the user link & save the user
+ delete cuser.links[node._id];
+ if (Object.keys(cuser.links).length == 0) { delete cuser.links; }
+ db.SetUser(cuser);
+ }
+ }
+ }
+ }
+
+ }
+ }
+ if (removeCount == 0) {
+ console.log("Done, no devices removed.");
+ process.exit(0);
+ } else {
+ console.log("Removed " + removeCount + " device(s), holding 10 seconds...");
+ setTimeout(function () { console.log("Done."); process.exit(0); }, 10000)
+ }
+ }
+ });
+ return;
+ }
+
+ // Import NeDB data into database
+ if (obj.args.nedbtodb) {
+ if (db.databaseType == 1) { console.log("NeDB is current database, can't perform transfer."); process.exit(); return; }
+ console.log("Transfering NeDB data into database...");
+ db.nedbtodb(function (msg) { console.log(msg); process.exit(); })
+ return;
+ }
+
+ // Show a list of all configuration files in the database
+ if (obj.args.dblistconfigfiles) {
+ obj.db.GetAllType('cfile', function (err, docs) {
+ if (err == null) {
+ if (docs.length == 0) {
+ console.log("No files found.");
+ } else {
+ for (var i in docs) {
+ if (typeof obj.args.dblistconfigfiles == 'string') {
+ const data = obj.db.decryptData(obj.args.dblistconfigfiles, docs[i].data);
+ if (data == null) {
+ console.log(docs[i]._id.split('/')[1] + ', ' + Buffer.from(docs[i].data, 'base64').length + ' encrypted bytes - Unable to decrypt.');
+ } else {
+ console.log(docs[i]._id.split('/')[1] + ', ' + data.length + ' bytes, decoded correctly.');
+ }
+ } else {
+ console.log(docs[i]._id.split('/')[1] + ', ' + Buffer.from(docs[i].data, 'base64').length + ' encrypted bytes.');
+ }
+ }
+ }
+ } else { console.log('Unable to read from database.'); } process.exit();
+ });
+ return;
+ }
+
+ // Display the content of a configuration file in the database
+ if (obj.args.dbshowconfigfile) {
+ if (typeof obj.args.configkey != 'string') { console.log("Error, --configkey is required."); process.exit(); return; }
+ obj.db.getConfigFile(obj.args.dbshowconfigfile, function (err, docs) {
+ if (err == null) {
+ if (docs.length == 0) { console.log("File not found."); } else {
+ const data = obj.db.decryptData(obj.args.configkey, docs[0].data);
+ if (data == null) { console.log("Invalid config key."); } else { console.log(data); }
+ }
+ } else { console.log("Unable to read from database."); }
+ process.exit();
+ }); return;
+ }
+
+ // Delete all configuration files from database
+ if (obj.args.dbdeleteconfigfiles) {
+ console.log("Deleting all configuration files from the database..."); obj.db.RemoveAllOfType('cfile', function () { console.log('Done.'); process.exit(); });
+ }
+
+ // Push all relevent files from meshcentral-data into the database
+ if (obj.args.dbpushconfigfiles) {
+ if (typeof obj.args.configkey != 'string') { console.log("Error, --configkey is required."); process.exit(); return; }
+ if ((obj.args.dbpushconfigfiles !== true) && (typeof obj.args.dbpushconfigfiles != 'string')) {
+ console.log("Usage: --dbpulldatafiles (path) This will import files from folder into the database");
+ console.log(" --dbpulldatafiles This will import files from meshcentral-data into the db.");
+ process.exit();
+ } else {
+ if ((obj.args.dbpushconfigfiles == '*') || (obj.args.dbpushconfigfiles === true)) { obj.args.dbpushconfigfiles = obj.datapath; }
+ obj.fs.readdir(obj.args.dbpushconfigfiles, function (err, files) {
+ if (err != null) { console.log('ERROR: Unable to read from folder ' + obj.args.dbpushconfigfiles); process.exit(); return; }
+ var configFound = false;
+ for (var i in files) { if (files[i] == 'config.json') { configFound = true; } }
+ if (configFound == false) { console.log('ERROR: No config.json in folder ' + obj.args.dbpushconfigfiles); process.exit(); return; }
+ obj.db.RemoveAllOfType('cfile', function () {
+ obj.fs.readdir(obj.args.dbpushconfigfiles, function (err, files) {
+ var lockCount = 1
+ for (var i in files) {
+ const file = files[i];
+ if ((file == 'config.json') || file.endsWith('.key') || file.endsWith('.crt') || (file == 'terms.txt') || file.endsWith('.jpg') || file.endsWith('.png')) {
+ const path = obj.path.join(obj.args.dbpushconfigfiles, files[i]), binary = Buffer.from(obj.fs.readFileSync(path, { encoding: 'binary' }), 'binary');
+ console.log('Pushing ' + file + ', ' + binary.length + ' bytes.');
+ lockCount++;
+ if (obj.args.oldencrypt) {
+ obj.db.setConfigFile(file, obj.db.oldEncryptData(obj.args.configkey, binary), function () { if ((--lockCount) == 0) { console.log('Done.'); process.exit(); } });
+ } else {
+ obj.db.setConfigFile(file, obj.db.encryptData(obj.args.configkey, binary), function () { if ((--lockCount) == 0) { console.log('Done.'); process.exit(); } });
+ }
+ }
+ }
+ if (--lockCount == 0) { process.exit(); }
+ });
+ });
+ });
+ }
+ return;
+ }
+
+ // Pull all database files into meshcentral-data
+ if (obj.args.dbpullconfigfiles) {
+ if (typeof obj.args.configkey != 'string') { console.log("Error, --configkey is required."); process.exit(); return; }
+ if (typeof obj.args.dbpullconfigfiles != 'string') {
+ console.log("Usage: --dbpulldatafiles (path)");
+ process.exit();
+ } else {
+ obj.db.GetAllType('cfile', function (err, docs) {
+ if (err == null) {
+ if (docs.length == 0) {
+ console.log("File not found.");
+ } else {
+ for (var i in docs) {
+ const file = docs[i]._id.split('/')[1], binary = obj.db.decryptData(obj.args.configkey, docs[i].data);
+ if (binary == null) {
+ console.log("Invalid config key.");
+ } else {
+ const fullFileName = obj.path.join(obj.args.dbpullconfigfiles, file);
+ try { obj.fs.writeFileSync(fullFileName, binary); } catch (ex) { console.log('Unable to write to ' + fullFileName); process.exit(); return; }
+ console.log('Pulling ' + file + ', ' + binary.length + ' bytes.');
+ }
+ }
+ }
+ } else {
+ console.log("Unable to read from database.");
+ }
+ process.exit();
+ });
+ }
+ return;
+ }
+
+ if (obj.args.dbexport) {
+ // Export the entire database to a JSON file
+ if (obj.args.dbexport == true) { obj.args.dbexport = obj.getConfigFilePath('meshcentral.db.json'); }
+ obj.db.GetAll(function (err, docs) {
+ obj.fs.writeFileSync(obj.args.dbexport, JSON.stringify(docs));
+ console.log('Exported ' + docs.length + ' objects(s) to ' + obj.args.dbexport + '.'); process.exit();
+ });
+ return;
+ }
+ if (obj.args.dbexportmin) {
+ // Export a minimal database to a JSON file. Export only users, meshes and nodes.
+ // This is a useful command to look at the database.
+ if (obj.args.dbexportmin == true) { obj.args.dbexportmin = obj.getConfigFilePath('meshcentral.db.json'); }
+ obj.db.GetAllType({ $in: ['user', 'node', 'mesh'] }, function (err, docs) {
+ obj.fs.writeFileSync(obj.args.dbexportmin, JSON.stringify(docs));
+ console.log('Exported ' + docs.length + ' objects(s) to ' + obj.args.dbexportmin + '.'); process.exit();
+ });
+ return;
+ }
+ if (obj.args.dbimport) {
+ // Import the entire database from a JSON file
+ if (obj.args.dbimport == true) { obj.args.dbimport = obj.getConfigFilePath('meshcentral.db.json'); }
+ var json = null, json2 = '', badCharCount = 0;
+ try { json = obj.fs.readFileSync(obj.args.dbimport, { encoding: 'utf8' }); } catch (ex) { console.log('Invalid JSON file: ' + obj.args.dbimport + ': ' + ex); process.exit(); }
+ for (i = 0; i < json.length; i++) { if (json.charCodeAt(i) >= 32) { json2 += json[i]; } else { var tt = json.charCodeAt(i); if (tt != 10 && tt != 13) { badCharCount++; } } } // Remove all bad chars
+ if (badCharCount > 0) { console.log(badCharCount + ' invalid character(s) where removed.'); }
+ try { json = JSON.parse(json2); } catch (ex) { console.log('Invalid JSON format: ' + obj.args.dbimport + ': ' + e); process.exit(); }
+ if ((json == null) || (typeof json.length != 'number') || (json.length < 1)) { console.log('Invalid JSON format: ' + obj.args.dbimport + '.'); }
+ // Escape MongoDB invalid field chars
+ for (i in json) {
+ const doc = json[i];
+ for (var j in doc) { if (j.indexOf('.') >= 0) { console.log("Invalid field name (" + j + ") in document: " + json[i]); return; } }
+ //if ((json[i].type == 'ifinfo') && (json[i].netif2 != null)) { for (var j in json[i].netif2) { var esc = obj.common.escapeFieldName(j); if (esc !== j) { json[i].netif2[esc] = json[i].netif2[j]; delete json[i].netif2[j]; } } }
+ //if ((json[i].type == 'mesh') && (json[i].links != null)) { for (var j in json[i].links) { var esc = obj.common.escapeFieldName(j); if (esc !== j) { json[i].links[esc] = json[i].links[j]; delete json[i].links[j]; } } }
+ }
+ //for (i in json) { if ((json[i].type == "node") && (json[i].host != null)) { json[i].rname = json[i].host; delete json[i].host; } } // DEBUG: Change host to rname
+ setTimeout(function () { // If the Mongo database is being created for the first time, there is a race condition here. This will get around it.
+ obj.db.RemoveAll(function () {
+ obj.db.InsertMany(json, function (err) {
+ if (err != null) { console.log(err); } else { console.log('Imported ' + json.length + ' objects(s) from ' + obj.args.dbimport + '.'); } process.exit();
+ });
+ });
+ }, 100);
+ return;
+ }
+ /*
+ if (obj.args.dbimport) {
+ // Import the entire database from a very large JSON file
+ obj.db.RemoveAll(function () {
+ if (obj.args.dbimport == true) { obj.args.dbimport = obj.getConfigFilePath('meshcentral.db.json'); }
+ var json = null, json2 = "", badCharCount = 0;
+ const StreamArray = require('stream-json/streamers/StreamArray');
+ const jsonStream = StreamArray.withParser();
+ jsonStream.on('data', function (data) { obj.db.Set(data.value); });
+ jsonStream.on('end', () => { console.log('Done.'); process.exit(); });
+ obj.fs.createReadStream(obj.args.dbimport).pipe(jsonStream.input);
+ });
+ return;
+ }
+ */
+ if (obj.args.dbmerge) {
+ // Import the entire database from a JSON file
+ if (obj.args.dbmerge == true) { obj.args.dbmerge = obj.getConfigFilePath('meshcentral.db.json'); }
+ var json = null, json2 = "", badCharCount = 0;
+ try { json = obj.fs.readFileSync(obj.args.dbmerge, { encoding: 'utf8' }); } catch (ex) { console.log('Invalid JSON file: ' + obj.args.dbmerge + ': ' + ex); process.exit(); }
+ for (i = 0; i < json.length; i++) { if (json.charCodeAt(i) >= 32) { json2 += json[i]; } else { var tt = json.charCodeAt(i); if (tt != 10 && tt != 13) { badCharCount++; } } } // Remove all bad chars
+ if (badCharCount > 0) { console.log(badCharCount + ' invalid character(s) where removed.'); }
+ try { json = JSON.parse(json2); } catch (ex) { console.log('Invalid JSON format: ' + obj.args.dbmerge + ': ' + ex); process.exit(); }
+ if ((json == null) || (typeof json.length != 'number') || (json.length < 1)) { console.log('Invalid JSON format: ' + obj.args.dbimport + '.'); }
+
+ // Get all users from current database
+ obj.db.GetAllType('user', function (err, docs) {
+ const users = {}, usersCount = 0;
+ for (var i in docs) { users[docs[i]._id] = docs[i]; usersCount++; }
+
+ // Fetch all meshes from the database
+ obj.db.GetAllType('mesh', function (err, docs) {
+ obj.common.unEscapeAllLinksFieldName(docs);
+ const meshes = {}, meshesCount = 0;
+ for (var i in docs) { meshes[docs[i]._id] = docs[i]; meshesCount++; }
+ console.log('Loaded ' + usersCount + ' users and ' + meshesCount + ' meshes.');
+ // Look at each object in the import file
+ const objectToAdd = [];
+ for (var i in json) {
+ const newobj = json[i];
+ if (newobj.type == 'user') {
+ // Check if the user already exists
+ var existingUser = users[newobj._id];
+ if (existingUser) {
+ // Merge the links
+ if (typeof newobj.links == 'object') {
+ for (var j in newobj.links) {
+ if ((existingUser.links == null) || (existingUser.links[j] == null)) {
+ if (existingUser.links == null) { existingUser.links = {}; }
+ existingUser.links[j] = newobj.links[j];
+ }
+ }
+ }
+ if (existingUser.name == 'admin') { existingUser.links = {}; }
+ objectToAdd.push(existingUser); // Add this user
+ } else {
+ objectToAdd.push(newobj); // Add this user
+ }
+ } else if (newobj.type == 'mesh') {
+ // Add this object
+ objectToAdd.push(newobj);
+ } // Don't add nodes.
+ }
+ console.log('Importing ' + objectToAdd.length + ' object(s)...');
+ var pendingCalls = 1;
+ for (var i in objectToAdd) {
+ pendingCalls++;
+ obj.db.Set(objectToAdd[i], function (err) { if (err != null) { console.log(err); } else { if (--pendingCalls == 0) { process.exit(); } } });
+ }
+ if (--pendingCalls == 0) { process.exit(); }
+ });
+ });
+ return;
+ }
+
+ // Check if the database is capable of performing a backup
+ // Moved behind autobackup config init in startex4: obj.db.checkBackupCapability(function (err, msg) { if (msg != null) { obj.addServerWarning(msg, true) } });
+
+ // Load configuration for database if needed
+ if (obj.args.loadconfigfromdb) {
+ var key = null;
+ if (typeof obj.args.configkey == 'string') { key = obj.args.configkey; }
+ else if (typeof obj.args.loadconfigfromdb == 'string') { key = obj.args.loadconfigfromdb; }
+ if (key == null) { console.log("Error, --configkey is required."); process.exit(); return; }
+ obj.db.getAllConfigFiles(key, function (configFiles) {
+ if (configFiles == null) { console.log("Error, no configuration files found or invalid configkey."); process.exit(); return; }
+ if (!configFiles['config.json']) { console.log("Error, could not file config.json from database."); process.exit(); return; }
+ if (typeof configFiles['config.json'] == 'object') { configFiles['config.json'] = configFiles['config.json'].toString(); }
+ if (configFiles['config.json'].charCodeAt(0) == 65279) { configFiles['config.json'] = configFiles['config.json'].substring(1); }
+ obj.configurationFiles = configFiles;
+
+ // Parse the new configuration file
+ var config2 = null;
+ try { config2 = JSON.parse(configFiles['config.json']); } catch (ex) { console.log('Error, unable to parse config.json from database.', ex); process.exit(); return; }
+
+ // Set the command line arguments to the config file if they are not present
+ if (!config2.settings) { config2.settings = {}; }
+ for (i in args) { config2.settings[i] = args[i]; }
+
+ // Lower case all keys in the config file
+ common.objKeysToLower(config2, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'telegram/proxy']);
+
+ // Grab some of the values from the original config.json file if present.
+ config2['mysql'] = config['mysql'];
+ config2['mariadb'] = config['mariadb'];
+ config2['mongodb'] = config['mongodb'];
+ config2['mongodbcol'] = config['mongodbcol'];
+ config2['dbencryptkey'] = config['dbencryptkey'];
+ config2['acebase'] = config['acebase'];
+ config2['sqlite3'] = config['sqlite3'];
+
+ // We got a new config.json from the database, let's use it.
+ config = obj.config = config2;
+ obj.StartEx1b();
+ });
+ } else {
+ config = obj.config = getConfig(obj.args.vault == null);
+ obj.StartEx1b();
+ }
+ });
+ }
+ );
+ };
+
+ // Time to start the server of real.
+ obj.StartEx1b = async function () {
+ var i;
+
+ // Add NodeJS version warning if needed
+ if (Number(process.version.match(/^v(\d+\.\d+)/)[1]) < 16) { addServerWarning("MeshCentral will require Node v16 or above in the future, your current version is " + process.version + "."); }
+
+ // Setup certificate operations
+ obj.certificateOperations = require('./certoperations.js').CertificateOperations(obj);
+
+ // Linux format /var/log/auth.log
+ if (obj.config.settings.authlog != null) {
+ obj.fs.open(obj.config.settings.authlog, 'a', function (err, fd) {
+ if (err == null) { obj.authlogfile = fd; } else { console.log('ERROR: Unable to open: ' + obj.config.settings.authlog); }
+ })
+ }
+
+ // Start CrowdSec bouncer if needed: https://www.crowdsec.net/
+ if (typeof obj.args.crowdsec == 'object') { obj.crowdSecBounser = require('./crowdsec.js').CreateCrowdSecBouncer(obj, obj.args.crowdsec); }
+
+ // Check if self update is allowed. If running as a Windows service, self-update is not possible.
+ if (obj.fs.existsSync(obj.path.join(__dirname, 'daemon'))) { obj.serverSelfWriteAllowed = false; }
+
+ // If we are targetting a specific version, update now.
+ if ((obj.serverSelfWriteAllowed == true) && (typeof obj.args.selfupdate == 'string')) {
+ obj.args.selfupdate = obj.args.selfupdate.toLowerCase();
+ if (getCurrentVersion() !== obj.args.selfupdate) { obj.performServerUpdate(); return; } // We are targetting a specific version, run self update now.
+ }
+
+ // Write the server state
+ obj.updateServerState('state', 'starting');
+ if (process.pid) { obj.updateServerState('server-pid', process.pid); }
+ if (process.ppid) { obj.updateServerState('server-parent-pid', process.ppid); }
+
+ // Read environment variables. For a subset of arguments, we allow them to be read from environment variables.
+ const xenv = ['user', 'port', 'mpsport', 'mpsaliasport', 'redirport', 'rediraliasport', 'exactport', 'debug'];
+ for (i in xenv) { if ((obj.args[xenv[i]] == null) && (process.env['mesh' + xenv[i]])) { obj.args[xenv[i]] = obj.common.toNumber(process.env['mesh' + xenv[i]]); } }
+
+ // Validate the domains, this is used for multi-hosting
+ if (obj.config.domains == null) { obj.config.domains = {}; }
+ if (obj.config.domains[''] == null) { obj.config.domains[''] = {}; }
+ if (obj.config.domains[''].dns != null) { console.log("ERROR: Default domain can't have a DNS name."); return; }
+ var xdomains = {}; for (i in obj.config.domains) { xdomains[i.toLowerCase()] = obj.config.domains[i]; } obj.config.domains = xdomains;
+ var bannedDomains = ['public', 'private', 'images', 'scripts', 'styles', 'views']; // List of banned domains
+ for (i in obj.config.domains) { for (var j in bannedDomains) { if (i == bannedDomains[j]) { console.log("ERROR: Domain '" + i + "' is not allowed domain name in config.json."); delete obj.config.domains[i]; } } }
+ for (i in obj.config.domains) { if ((i.length > 64) || (Buffer.from(i).length > 64)) { console.log("ERROR: Domain '" + i + "' is longer that 64 bytes, this is not allowed."); delete obj.config.domains[i]; } }
+ for (i in obj.config.domains) {
+ // Remove any domains that start with underscore
+ if (i.startsWith('_')) { delete obj.config.domains[i]; continue; }
+
+ // Apply default domain settings if present
+ if (typeof obj.config.domaindefaults == 'object') { for (var j in obj.config.domaindefaults) { if (obj.config.domains[i][j] == null) { obj.config.domains[i][j] = obj.config.domaindefaults[j]; } } }
+
+ // Perform domain setup
+ if (typeof obj.config.domains[i] != 'object') { console.log("ERROR: Invalid domain configuration in config.json."); process.exit(); return; }
+ if ((i.length > 0) && (i[0] == '_')) { delete obj.config.domains[i]; continue; } // Remove any domains with names that start with _
+ if (typeof config.domains[i].auth == 'string') { config.domains[i].auth = config.domains[i].auth.toLowerCase(); }
+ if (obj.config.domains[i].limits == null) { obj.config.domains[i].limits = {}; }
+ if (obj.config.domains[i].preventduplicatedevices == null) { obj.config.domains[i].preventduplicatedevices = false; }
+ if (obj.config.domains[i].dns == null) { obj.config.domains[i].url = (i == '') ? '/' : ('/' + i + '/'); } else { obj.config.domains[i].url = '/'; }
+ obj.config.domains[i].id = i;
+ if ((typeof obj.config.domains[i].maxdeviceview != 'number') || (obj.config.domains[i].maxdeviceview < 1)) { delete obj.config.domains[i].maxdeviceview; }
+ if (typeof obj.config.domains[i].loginkey == 'string') { obj.config.domains[i].loginkey = [obj.config.domains[i].loginkey]; }
+ if ((obj.config.domains[i].loginkey != null) && (obj.common.validateAlphaNumericArray(obj.config.domains[i].loginkey, 1, 128) == false)) { console.log("ERROR: Invalid login key, must be alpha-numeric string with no spaces."); process.exit(); return; }
+ if (typeof obj.config.domains[i].agentkey == 'string') { obj.config.domains[i].agentkey = [obj.config.domains[i].agentkey]; }
+ if ((obj.config.domains[i].agentkey != null) && (obj.common.validateAlphaNumericArray(obj.config.domains[i].agentkey, 1, 128) == false)) { console.log("ERROR: Invalid agent key, must be alpha-numeric string with no spaces."); process.exit(); return; }
+ obj.config.domains[i].userallowedip = obj.config.domains[i].userallowedip = readIpListFromFile(obj.config.domains[i].userallowedip);
+ obj.config.domains[i].userblockedip = obj.config.domains[i].userblockedip = readIpListFromFile(obj.config.domains[i].userblockedip);
+ obj.config.domains[i].agentallowedip = obj.config.domains[i].agentallowedip = readIpListFromFile(obj.config.domains[i].agentallowedip);
+ obj.config.domains[i].agentblockedip = obj.config.domains[i].agentblockedip = readIpListFromFile(obj.config.domains[i].agentblockedip);
+ if (typeof obj.config.domains[i].userallowedip == 'string') { if (obj.config.domains[i].userallowedip == '') { delete obj.config.domains[i].userallowedip; } else { obj.config.domains[i].userallowedip = obj.config.domains[i].userallowedip.split(' ').join('').split(','); } }
+ if (typeof obj.config.domains[i].userblockedip == 'string') { if (obj.config.domains[i].userblockedip == '') { delete obj.config.domains[i].userblockedip; } else { obj.config.domains[i].userblockedip = obj.config.domains[i].userblockedip.split(' ').join('').split(','); } }
+ if (typeof obj.config.domains[i].agentallowedip == 'string') { if (obj.config.domains[i].agentallowedip == '') { delete obj.config.domains[i].agentallowedip; } else { obj.config.domains[i].agentallowedip = obj.config.domains[i].agentallowedip.split(' ').join('').split(','); } }
+ if (typeof obj.config.domains[i].agentblockedip == 'string') { if (obj.config.domains[i].agentblockedip == '') { delete obj.config.domains[i].agentblockedip; } else { obj.config.domains[i].agentblockedip = obj.config.domains[i].agentblockedip.split(' ').join('').split(','); } }
+ // Check IP lists and ranges and if DNS return IP addresses
+ obj.config.domains[i].userallowedip = await resolveDomainsToIps(obj.config.domains[i].userallowedip);
+ obj.config.domains[i].userblockedip = await resolveDomainsToIps(obj.config.domains[i].userblockedip);
+ obj.config.domains[i].agentallowedip = await resolveDomainsToIps(obj.config.domains[i].agentallowedip);
+ obj.config.domains[i].agentblockedip = await resolveDomainsToIps(obj.config.domains[i].agentblockedip);
+ if (typeof obj.config.domains[i].ignoreagenthashcheck == 'string') { if (obj.config.domains[i].ignoreagenthashcheck == '') { delete obj.config.domains[i].ignoreagenthashcheck; } else { obj.config.domains[i].ignoreagenthashcheck = obj.config.domains[i].ignoreagenthashcheck.split(','); } }
+ if (typeof obj.config.domains[i].allowedorigin == 'string') { if (obj.config.domains[i].allowedorigin == '') { delete obj.config.domains[i].allowedorigin; } else { obj.config.domains[i].allowedorigin = obj.config.domains[i].allowedorigin.split(','); } }
+ if ((obj.config.domains[i].passwordrequirements != null) && (typeof obj.config.domains[i].passwordrequirements == 'object')) {
+ if (typeof obj.config.domains[i].passwordrequirements.skip2factor == 'string') {
+ obj.config.domains[i].passwordrequirements.skip2factor = obj.config.domains[i].passwordrequirements.skip2factor.split(',');
+ } else {
+ delete obj.config.domains[i].passwordrequirements.skip2factor;
+ }
+ // Fix the list of users to add "user/domain/" if needed
+ if (Array.isArray(obj.config.domains[i].passwordrequirements.logintokens)) {
+ var newValues = [];
+ for (var j in obj.config.domains[i].passwordrequirements.logintokens) {
+ var splitVal = obj.config.domains[i].passwordrequirements.logintokens[j].split('/');;
+ if (splitVal.length == 1) { newValues.push('user/' + i + '/' + splitVal[0]); }
+ if (splitVal.length == 2) { newValues.push('user/' + splitVal[0] + '/' + splitVal[1]); }
+ if (splitVal.length == 3) { newValues.push(splitVal[0] + '/' + splitVal[1] + '/' + splitVal[2]); }
+ }
+ obj.config.domains[i].passwordrequirements.logintokens = newValues;
+ }
+ }
+ if ((obj.config.domains[i].auth == 'ldap') && (typeof obj.config.domains[i].ldapoptions != 'object')) {
+ if (i == '') { console.log("ERROR: Default domain is LDAP, but is missing LDAPOptions."); } else { console.log("ERROR: Domain '" + i + "' is LDAP, but is missing LDAPOptions."); }
+ process.exit();
+ return;
+ }
+ if ((obj.config.domains[i].auth == 'ldap') || (obj.config.domains[i].auth == 'sspi')) { obj.config.domains[i].newaccounts = 0; } // No new accounts allowed in SSPI/LDAP authentication modes.
+ if (obj.config.domains[i].sitestyle == null) { obj.config.domains[i].sitestyle = 2; } // Default to site style #2
+
+ // Convert newAccountsRights from a array of strings to flags number.
+ obj.config.domains[i].newaccountsrights = obj.common.meshServerRightsArrayToNumber(obj.config.domains[i].newaccountsrights);
+ if (typeof (obj.config.domains[i].newaccountsrights) != 'number') { delete obj.config.domains[i].newaccountsrights; }
+
+ // Check if there is a web views path and/or web public path for this domain
+ if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) {
+ if ((obj.config.domains[i].webviewspath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web-' + i + '/views')))) { obj.config.domains[i].webviewspath = obj.path.join(__dirname, '../../meshcentral-web-' + i + '/views'); }
+ if ((obj.config.domains[i].webpublicpath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web-' + i + '/public')))) { obj.config.domains[i].webpublicpath = obj.path.join(__dirname, '../../meshcentral-web-' + i + '/public'); }
+ if ((obj.config.domains[i].webemailspath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web-' + i + '/emails')))) { obj.config.domains[i].webemailspath = obj.path.join(__dirname, '../../meshcentral-web-' + i + '/emails'); }
+ } else {
+ if ((obj.config.domains[i].webviewspath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web-' + i + '/views')))) { obj.config.domains[i].webviewspath = obj.path.join(__dirname, '../meshcentral-web-' + i + '/views'); }
+ if ((obj.config.domains[i].webpublicpath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web-' + i + '/public')))) { obj.config.domains[i].webpublicpath = obj.path.join(__dirname, '../meshcentral-web-' + i + '/public'); }
+ if ((obj.config.domains[i].webemailspath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web-' + i + '/emails')))) { obj.config.domains[i].webemailspath = obj.path.join(__dirname, '../meshcentral-web-' + i + '/emails'); }
+ }
+
+ // Check agent customization if any
+ if (typeof obj.config.domains[i].agentcustomization == 'object') {
+ if (typeof obj.config.domains[i].agentcustomization.displayname != 'string') { delete obj.config.domains[i].agentcustomization.displayname; } else { obj.config.domains[i].agentcustomization.displayname = obj.config.domains[i].agentcustomization.displayname.split('\r').join('').split('\n').join(''); }
+ if (typeof obj.config.domains[i].agentcustomization.description != 'string') { delete obj.config.domains[i].agentcustomization.description; } else { obj.config.domains[i].agentcustomization.description = obj.config.domains[i].agentcustomization.description.split('\r').join('').split('\n').join(''); }
+ if (typeof obj.config.domains[i].agentcustomization.companyname != 'string') { delete obj.config.domains[i].agentcustomization.companyname; } else { obj.config.domains[i].agentcustomization.companyname = obj.config.domains[i].agentcustomization.companyname.split('\r').join('').split('\n').join(''); }
+ if (typeof obj.config.domains[i].agentcustomization.servicename != 'string') { delete obj.config.domains[i].agentcustomization.servicename; } else { obj.config.domains[i].agentcustomization.servicename = obj.config.domains[i].agentcustomization.servicename.split('\r').join('').split('\n').join('').split(' ').join('').split('"').join('').split('\'').join('').split('>').join('').split('<').join('').split('/').join('').split('\\').join(''); }
+ if (typeof obj.config.domains[i].agentcustomization.image != 'string') { delete obj.config.domains[i].agentcustomization.image; } else { try { obj.config.domains[i].agentcustomization.image = 'data:image/png;base64,' + Buffer.from(obj.fs.readFileSync(obj.getConfigFilePath(obj.config.domains[i].agentcustomization.image)), 'binary').toString('base64'); } catch (ex) { console.log(ex); delete obj.config.domains[i].agentcustomization.image; } }
+ } else {
+ delete obj.config.domains[i].agentcustomization;
+ }
+
+ // Convert user consent flags
+ if (typeof obj.config.domains[i].userconsentflags == 'object') {
+ var flags = 0;
+ if (obj.config.domains[i].userconsentflags.desktopnotify == true) { flags |= 1; }
+ if (obj.config.domains[i].userconsentflags.terminalnotify == true) { flags |= 2; }
+ if (obj.config.domains[i].userconsentflags.filenotify == true) { flags |= 4; }
+ if (obj.config.domains[i].userconsentflags.desktopprompt == true) { flags |= 8; }
+ if (obj.config.domains[i].userconsentflags.terminalprompt == true) { flags |= 16; }
+ if (obj.config.domains[i].userconsentflags.fileprompt == true) { flags |= 32; }
+ if (obj.config.domains[i].userconsentflags.desktopprivacybar == true) { flags |= 64; }
+ obj.config.domains[i].userconsentflags = flags;
+ }
+
+ // If we have Intel AMT manager settings, take a look at them here.
+ if (typeof obj.config.domains[i].amtmanager == 'object') {
+ if (typeof obj.config.domains[i].amtmanager.tlsrootcert == 'object') {
+ obj.config.domains[i].amtmanager.tlsrootcert2 = obj.certificateOperations.loadGenericCertAndKey(obj.config.domains[i].amtmanager.tlsrootcert);
+ if (obj.config.domains[i].amtmanager.tlsrootcert2 == null) { // Show an error message if needed
+ if (i == '') {
+ addServerWarning("Unable to load Intel AMT TLS root certificate for default domain.", 5);
+ } else {
+ addServerWarning("Unable to load Intel AMT TLS root certificate for domain " + i + ".", 6, [i]);
+ }
+ }
+ }
+ }
+
+ // Check agentfileinfo
+ if (typeof obj.config.domains[i].agentfileinfo == 'object') {
+ if ((obj.config.domains[i].agentfileinfo.fileversionnumber != null) && (obj.common.parseVersion(obj.config.domains[i].agentfileinfo.fileversionnumber) == null)) { delete obj.config.domains[i].agentfileinfo.fileversionnumber; }
+ if ((obj.config.domains[i].agentfileinfo.productversionnumber != null) && (obj.common.parseVersion(obj.config.domains[i].agentfileinfo.productversionnumber) == null)) { delete obj.config.domains[i].agentfileinfo.productversionnumber; }
+ if ((obj.config.domains[i].agentfileinfo.fileversionnumber == null) && (typeof obj.config.domains[i].agentfileinfo.fileversion == 'string') && (obj.common.parseVersion(obj.config.domains[i].agentfileinfo.fileversion) != null)) { obj.config.domains[i].agentfileinfo.fileversionnumber = obj.config.domains[i].agentfileinfo.fileversion; }
+ if (typeof obj.config.domains[i].agentfileinfo.icon == 'string') {
+ // Load the agent .ico file
+ var icon = null;
+ try { icon = require('./authenticode.js').loadIcon(obj.path.join(obj.datapath, obj.config.domains[i].agentfileinfo.icon)); } catch (ex) { }
+ if (icon != null) {
+ // The icon file was correctly loaded
+ obj.config.domains[i].agentfileinfo.icon = icon;
+ } else {
+ // Failed to load the icon file, display a server warning
+ addServerWarning("Unable to load agent icon file: " + obj.config.domains[i].agentfileinfo.icon + ".", 23, [obj.config.domains[i].agentfileinfo.icon]);
+ delete obj.config.domains[i].agentfileinfo.icon;
+ }
+ } else {
+ // Invalid icon file path
+ delete obj.config.domains[i].agentfileinfo.icon;
+ }
+ if (typeof obj.config.domains[i].agentfileinfo.logo == 'string') {
+ // Load the agent .bmp file
+ var logo = null;
+ try { logo = require('./authenticode.js').loadBitmap(obj.path.join(obj.datapath, obj.config.domains[i].agentfileinfo.logo)); } catch (ex) { }
+ if (logo != null) {
+ // The logo file was correctly loaded
+ obj.config.domains[i].agentfileinfo.logo = logo;
+ } else {
+ // Failed to load the icon file, display a server warning
+ addServerWarning("Unable to load agent logo file: " + obj.config.domains[i].agentfileinfo.logo + ".", 24, [obj.config.domains[i].agentfileinfo.logo]);
+ delete obj.config.domains[i].agentfileinfo.logo;
+ }
+ } else {
+ // Invalid icon file path
+ delete obj.config.domains[i].agentfileinfo.logo;
+ }
+ }
+ }
+
+ // Log passed arguments into Windows Service Log
+ //if (obj.servicelog != null) { var s = ''; for (i in obj.args) { if (i != '_') { if (s.length > 0) { s += ', '; } s += i + "=" + obj.args[i]; } } logInfoEvent('MeshServer started with arguments: ' + s); }
+
+ // Look at passed in arguments
+ if ((obj.args.user != null) && (typeof obj.args.user != 'string')) { delete obj.args.user; }
+ if ((obj.args.ciralocalfqdn != null) && ((obj.args.lanonly == true) || (obj.args.wanonly == true))) { addServerWarning("CIRA local FQDN's ignored when server in LAN-only or WAN-only mode.", 7); }
+ if ((obj.args.ciralocalfqdn != null) && (obj.args.ciralocalfqdn.split(',').length > 4)) { addServerWarning("Can't have more than 4 CIRA local FQDN's. Ignoring value.", 8); obj.args.ciralocalfqdn = null; }
+ if (obj.args.ignoreagenthashcheck === true) { addServerWarning("Agent hash checking is being skipped, this is unsafe.", 9); }
+ if (obj.args.port == null || typeof obj.args.port != 'number') { obj.args.port = 443; }
+ if (obj.args.aliasport != null && (typeof obj.args.aliasport != 'number')) obj.args.aliasport = null;
+ if (obj.args.mpsport == null || typeof obj.args.mpsport != 'number') obj.args.mpsport = 4433;
+ if (obj.args.mpsaliasport != null && (typeof obj.args.mpsaliasport != 'number')) obj.args.mpsaliasport = null;
+ if (obj.args.rediraliasport != null && (typeof obj.args.rediraliasport != 'number')) obj.args.rediraliasport = null;
+ if (obj.args.redirport == null) obj.args.redirport = 80;
+ if (obj.args.minifycore == null) obj.args.minifycore = false;
+ if (typeof obj.args.agentidletimeout != 'number') { obj.args.agentidletimeout = 150000; } else { obj.args.agentidletimeout *= 1000 } // Default agent idle timeout is 2m, 30sec.
+ if ((obj.args.lanonly != true) && (typeof obj.args.webrtconfig == 'object')) { // fix incase you are using an old mis-spelt webrtconfig
+ obj.args.webrtcconfig = obj.args.webrtconfig;
+ delete obj.args.webrtconfig;
+ }
+ if ((obj.args.lanonly != true) && (obj.args.webrtcconfig == null)) { obj.args.webrtcconfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun.cloudflare.com:3478' }] }; } // Setup default WebRTC STUN servers
+ else if ((obj.args.lanonly != true) && (typeof obj.args.webrtcconfig == 'object')) {
+ if (obj.args.webrtcconfig.iceservers) { // webrtc is case-sensitive, so must rename iceservers to iceServers!
+ obj.args.webrtcconfig.iceServers = obj.args.webrtcconfig.iceservers;
+ delete obj.args.webrtcconfig.iceservers;
+ }
+ }
+ if (typeof obj.args.ignoreagenthashcheck == 'string') { if (obj.args.ignoreagenthashcheck == '') { delete obj.args.ignoreagenthashcheck; } else { obj.args.ignoreagenthashcheck = obj.args.ignoreagenthashcheck.split(','); } }
+
+ // Setup a site administrator
+ if ((obj.args.admin) && (typeof obj.args.admin == 'string')) {
+ var adminname = obj.args.admin.split('/');
+ if (adminname.length == 1) { adminname = 'user//' + adminname[0]; }
+ else if (adminname.length == 2) { adminname = 'user/' + adminname[0] + '/' + adminname[1]; }
+ else { console.log("Invalid administrator name."); process.exit(); return; }
+ obj.db.Get(adminname, function (err, user) {
+ if (user.length != 1) { console.log("Invalid user name."); process.exit(); return; }
+ user[0].siteadmin = 4294967295; // 0xFFFFFFFF
+ obj.db.Set(user[0], function () {
+ if (user[0].domain == '') { console.log('User ' + user[0].name + ' set to site administrator.'); } else { console.log("User " + user[0].name + " of domain " + user[0].domain + " set to site administrator."); }
+ process.exit();
+ return;
+ });
+ });
+ return;
+ }
+
+ // Remove a site administrator
+ if ((obj.args.unadmin) && (typeof obj.args.unadmin == 'string')) {
+ var adminname = obj.args.unadmin.split('/');
+ if (adminname.length == 1) { adminname = 'user//' + adminname[0]; }
+ else if (adminname.length == 2) { adminname = 'user/' + adminname[0] + '/' + adminname[1]; }
+ else { console.log("Invalid administrator name."); process.exit(); return; }
+ obj.db.Get(adminname, function (err, user) {
+ if (user.length != 1) { console.log("Invalid user name."); process.exit(); return; }
+ if (user[0].siteadmin) { delete user[0].siteadmin; }
+ obj.db.Set(user[0], function () {
+ if (user[0].domain == '') { console.log("User " + user[0].name + " is not a site administrator."); } else { console.log("User " + user[0].name + " of domain " + user[0].domain + " is not a site administrator."); }
+ process.exit();
+ return;
+ });
+ });
+ return;
+ }
+
+ // Setup agent error log
+ if ((obj.config) && (obj.config.settings) && (obj.config.settings.agentlogdump)) {
+ obj.fs.open(obj.path.join(obj.datapath, 'agenterrorlogs.txt'), 'a', function (err, fd) { obj.agentErrorLog = fd; })
+ }
+
+ // Perform other database cleanup
+ obj.db.cleanup();
+
+ // Set all nodes to power state of unknown (0)
+ obj.db.storePowerEvent({ time: new Date(), nodeid: '*', power: 0, s: 1 }, obj.multiServer); // s:1 indicates that the server is starting up.
+
+ // Read or setup database configuration values
+ obj.db.Get('dbconfig', function (err, dbconfig) {
+ if ((dbconfig != null) && (dbconfig.length == 1)) { obj.dbconfig = dbconfig[0]; } else { obj.dbconfig = { _id: 'dbconfig', version: 1 }; }
+ if (obj.dbconfig.amtWsEventSecret == null) { obj.crypto.randomBytes(32, function (err, buf) { obj.dbconfig.amtWsEventSecret = buf.toString('hex'); obj.db.Set(obj.dbconfig); }); }
+
+ // This is used by the user to create a username/password for a Intel AMT WSMAN event subscription
+ if (obj.args.getwspass) {
+ if (obj.args.getwspass.length == 64) {
+ obj.crypto.randomBytes(6, function (err, buf) {
+ while (obj.dbconfig.amtWsEventSecret == null) { process.nextTick(); }
+ const username = buf.toString('hex');
+ const nodeid = obj.args.getwspass;
+ const pass = obj.crypto.createHash('sha384').update(username.toLowerCase() + ':' + nodeid + ':' + obj.dbconfig.amtWsEventSecret).digest('base64').substring(0, 12).split('/').join('x').split('\\').join('x');
+ console.log("--- Intel(r) AMT WSMAN eventing credentials ---");
+ console.log("Username: " + username);
+ console.log("Password: " + pass);
+ console.log("Argument: " + nodeid);
+ process.exit();
+ });
+ } else {
+ console.log("Invalid NodeID.");
+ process.exit();
+ }
+ return;
+ }
+
+ // Setup the task manager
+ if ((obj.config) && (obj.config.settings) && (obj.config.settings.taskmanager == true)) {
+ obj.taskManager = require('./taskmanager').createTaskManager(obj);
+ }
+
+ // Start plugin manager if configuration allows this.
+ if ((obj.config) && (obj.config.settings) && (obj.config.settings.plugins != null) && (obj.config.settings.plugins != false) && ((typeof obj.config.settings.plugins != 'object') || (obj.config.settings.plugins.enabled != false))) {
+ obj.pluginHandler = require('./pluginHandler.js').pluginHandler(obj);
+ }
+
+ // Load the default meshcore and meshcmd
+ obj.updateMeshCore();
+ obj.updateMeshCmd();
+
+ // Setup and start the redirection server if needed. We must start the redirection server before Let's Encrypt.
+ if ((obj.args.redirport != null) && (typeof obj.args.redirport == 'number') && (obj.args.redirport != 0)) {
+ obj.redirserver = require('./redirserver.js').CreateRedirServer(obj, obj.db, obj.args, obj.StartEx2);
+ } else {
+ obj.StartEx2(); // If not needed, move on.
+ }
+ });
+ }
+
+ // Done starting the redirection server, go on to load the server certificates
+ obj.StartEx2 = function () {
+ // Load server certificates
+ obj.certificateOperations.GetMeshServerCertificate(obj.args, obj.config, function (certs) {
+ // Get the current node version
+ if ((obj.config.letsencrypt == null) || (obj.redirserver == null)) {
+ obj.StartEx3(certs); // Just use the configured certificates
+ } else if ((obj.config.letsencrypt != null) && (obj.config.letsencrypt.nochecks == true)) {
+ // Use Let's Encrypt with no checking
+ obj.letsencrypt = require('./letsencrypt.js').CreateLetsEncrypt(obj);
+ obj.letsencrypt.getCertificate(certs, obj.StartEx3); // Use Let's Encrypt with no checking, use at your own risk.
+ } else {
+ // Check Let's Encrypt settings
+ var leok = true;
+ if ((typeof obj.config.letsencrypt.names != 'string') && (typeof obj.config.settings.cert == 'string')) { obj.config.letsencrypt.names = obj.config.settings.cert; }
+ if (typeof obj.config.letsencrypt.email != 'string') { leok = false; addServerWarning("Missing Let's Encrypt email address.", 10); }
+ else if (typeof obj.config.letsencrypt.names != 'string') { leok = false; addServerWarning("Invalid Let's Encrypt host names.", 11); }
+ else if (obj.config.letsencrypt.names.indexOf('*') >= 0) { leok = false; addServerWarning("Invalid Let's Encrypt names, can't contain a *.", 12); }
+ else if (obj.config.letsencrypt.email.split('@').length != 2) { leok = false; addServerWarning("Invalid Let's Encrypt email address.", 10); }
+ else if (obj.config.letsencrypt.email.trim() !== obj.config.letsencrypt.email) { leok = false; addServerWarning("Invalid Let's Encrypt email address.", 10); }
+ else {
+ const le = require('./letsencrypt.js');
+ try { obj.letsencrypt = le.CreateLetsEncrypt(obj); } catch (ex) { console.log(ex); }
+ if (obj.letsencrypt == null) { addServerWarning("Unable to setup Let's Encrypt module.", 13); leok = false; }
+ }
+ if (leok == true) {
+ // Check that the email address domain MX resolves.
+ require('dns').resolveMx(obj.config.letsencrypt.email.split('@')[1], function (err, addresses) {
+ if (err == null) {
+ // Check that all names resolve
+ checkResolveAll(obj.config.letsencrypt.names.split(','), function (err) {
+ if (err == null) {
+ obj.letsencrypt.getCertificate(certs, obj.StartEx3); // Use Let's Encrypt
+ } else {
+ for (var i in err) { addServerWarning("Invalid Let's Encrypt names, unable to resolve: " + err[i], 14, [err[i]]); }
+ obj.StartEx3(certs); // Let's Encrypt did not load, just use the configured certificates
+ }
+ });
+ } else {
+ addServerWarning("Invalid Let's Encrypt email address, unable to resolve: " + obj.config.letsencrypt.email.split('@')[1], 15, [obj.config.letsencrypt.email.split('@')[1]]);
+ obj.StartEx3(certs); // Let's Encrypt did not load, just use the configured certificates
+ }
+ });
+ } else {
+ obj.StartEx3(certs); // Let's Encrypt did not load, just use the configured certificates
+ }
+ }
+ });
+ };
+
+ // Start the server with the given certificates, but check if we have web certificates to load
+ obj.StartEx3 = function (certs) {
+ obj.certificates = certs;
+ obj.certificateOperations.acceleratorStart(certs); // Set the state of the accelerators
+
+ // Load any domain web certificates
+ for (var i in obj.config.domains) {
+ // Load any Intel AMT ACM activation certificates
+ if (obj.config.domains[i].amtacmactivation == null) { obj.config.domains[i].amtacmactivation = {}; }
+ obj.certificateOperations.loadIntelAmtAcmCerts(obj.config.domains[i].amtacmactivation);
+ if (obj.config.domains[i].amtacmactivation.acmCertErrors != null) { for (var j in obj.config.domains[i].amtacmactivation.acmCertErrors) { obj.addServerWarning(obj.config.domains[i].amtacmactivation.acmCertErrors[j]); } }
+ if (typeof obj.config.domains[i].certurl == 'string') {
+ obj.supportsProxyCertificatesRequest = true; // If a certurl is set, enable proxy cert requests
+ // Then, fix the URL and add 'https://' if needed
+ if (obj.config.domains[i].certurl.indexOf('://') < 0) { obj.config.domains[i].certurl = 'https://' + obj.config.domains[i].certurl; }
+ }
+ }
+
+ // Load CloudFlare trusted proxies list if needed
+ if ((obj.config.settings.trustedproxy != null) && (typeof obj.config.settings.trustedproxy == 'string') && (obj.config.settings.trustedproxy.toLowerCase() == 'cloudflare')) {
+ obj.config.settings.extrascriptsrc = 'ajax.cloudflare.com'; // Add CloudFlare as a trusted script source. This allows for CloudFlare's RocketLoader feature.
+ delete obj.args.trustedproxy;
+ delete obj.config.settings.trustedproxy;
+ obj.certificateOperations.loadTextFile('https://www.cloudflare.com/ips-v4', null, function (url, data, tag) {
+ if (data != null) {
+ if (Array.isArray(obj.args.trustedproxy) == false) { obj.args.trustedproxy = []; }
+ const ipranges = data.split('\n');
+ for (var i in ipranges) { if (ipranges[i] != '') { obj.args.trustedproxy.push(ipranges[i]); } }
+ obj.certificateOperations.loadTextFile('https://www.cloudflare.com/ips-v6', null, function (url, data, tag) {
+ if (data != null) {
+ var ipranges = data.split('\n');
+ for (var i in ipranges) { if (ipranges[i] != '') { obj.args.trustedproxy.push(ipranges[i]); } }
+ obj.config.settings.trustedproxy = obj.args.trustedproxy;
+ } else {
+ addServerWarning("Unable to load CloudFlare trusted proxy IPv6 address list.", 16);
+ }
+ obj.StartEx4(); // Keep going
+ });
+ } else {
+ addServerWarning("Unable to load CloudFlare trusted proxy IPv4 address list.", 16);
+ obj.StartEx4(); // Keep going
+ }
+ });
+ } else {
+ obj.StartEx4(); // Keep going
+ }
+ }
+
+ // Start the server with the given certificates
+ obj.StartEx4 = function () {
+ var i;
+
+ // If the certificate is un-configured, force LAN-only mode
+ if (obj.certificates.CommonName.indexOf('.') == -1) { /*console.log('Server name not configured, running in LAN-only mode.');*/ obj.args.lanonly = true; }
+
+ // Write server version and run mode
+ const productionMode = (process.env.NODE_ENV && (process.env.NODE_ENV == 'production'));
+ const runmode = (obj.args.lanonly ? 2 : (obj.args.wanonly ? 1 : 0));
+ console.log("MeshCentral v" + getCurrentVersion() + ', ' + (["Hybrid (LAN + WAN) mode", "WAN mode", "LAN mode"][runmode]) + (productionMode ? ", Production mode." : '.'));
+
+ // Check that no sub-domains have the same DNS as the parent
+ for (i in obj.config.domains) {
+ if ((obj.config.domains[i].dns != null) && (obj.certificates.CommonName.toLowerCase() === obj.config.domains[i].dns.toLowerCase())) {
+ console.log("ERROR: Server sub-domain can't have same DNS name as the parent."); process.exit(0); return;
+ }
+ }
+
+ // Load the list of MeshCentral tools
+ obj.updateMeshTools();
+
+ // Load MeshAgent translation strings
+ try {
+ var translationpath = obj.path.join(__dirname, 'agents', 'agent-translations.json');
+ const translationpath2 = obj.path.join(obj.datapath, 'agents', 'agent-translations.json');
+ if (obj.fs.existsSync(translationpath2)) { translationpath = translationpath2; } // If the agent is present in "meshcentral-data/agents", use that one instead.
+ var translations = JSON.parse(obj.fs.readFileSync(translationpath).toString());
+ if (translations['zh-chs']) { translations['zh-hans'] = translations['zh-chs']; delete translations['zh-chs']; }
+ if (translations['zh-cht']) { translations['zh-hant'] = translations['zh-cht']; delete translations['zh-cht']; }
+
+ // If there is domain customizations to the agent strings, do this here.
+ for (var i in obj.config.domains) {
+ var domainTranslations = translations;
+ if ((typeof obj.config.domains[i].agentcustomization == 'object') && (typeof obj.config.domains[i].agentcustomization.installtext == 'string')) {
+ domainTranslations = Object.assign({}, domainTranslations); // Shallow clone
+ for (var j in domainTranslations) { delete domainTranslations[j].description; }
+ domainTranslations.en.description = obj.config.domains[i].agentcustomization.installtext;
+ }
+ obj.config.domains[i].agentTranslations = JSON.stringify(domainTranslations);
+ }
+ } catch (ex) { }
+
+ // Load any domain specific agents
+ for (var i in obj.config.domains) { if ((i != '') && (obj.config.domains[i].share == null)) { obj.updateMeshAgentsTable(obj.config.domains[i], function () { }); } }
+
+ // Load the list of mesh agents and install scripts
+ if ((obj.args.noagentupdate == 1) || (obj.args.noagentupdate == true)) { for (i in obj.meshAgentsArchitectureNumbers) { obj.meshAgentsArchitectureNumbers[i].update = false; } }
+ obj.signMeshAgents(obj.config.domains[''], function () {
+ obj.updateMeshAgentsTable(obj.config.domains[''], function () {
+ obj.updateMeshAgentInstallScripts();
+
+ // Setup and start the web server
+ obj.crypto.randomBytes(48, function (err, buf) {
+ // Setup Mesh Multi-Server if needed
+ obj.multiServer = require('./multiserver.js').CreateMultiServer(obj, obj.args);
+ if (obj.multiServer != null) {
+ if ((obj.db.databaseType != 3) || (obj.db.changeStream != true)) { console.log("ERROR: Multi-server support requires use of MongoDB with ReplicaSet and ChangeStream enabled."); process.exit(0); return; }
+ if (typeof obj.args.sessionkey != 'string') { console.log("ERROR: Multi-server support requires \"SessionKey\" be set in the settings section of config.json, same key for all servers."); process.exit(0); return; }
+ obj.serverId = obj.multiServer.serverid;
+ for (var serverid in obj.config.peers.servers) { obj.peerConnectivityByNode[serverid] = {}; }
+ }
+
+ // If the server is set to "nousers", allow only loopback unless IP filter is set
+ if ((obj.args.nousers == true) && (obj.args.userallowedip == null)) { obj.args.userallowedip = "::1,127.0.0.1"; }
+
+ // Set the session length to 60 minutes if not set and set a random key if needed
+ if ((obj.args.sessiontime != null) && ((typeof obj.args.sessiontime != 'number') || (obj.args.sessiontime < 1))) { delete obj.args.sessiontime; }
+ if (typeof obj.args.sessionkey != 'string') { obj.args.sessionkey = buf.toString('hex').toUpperCase(); }
+
+ // Create MQTT Broker to hook into webserver and mpsserver
+ if ((typeof obj.config.settings.mqtt == 'object') && (typeof obj.config.settings.mqtt.auth == 'object') && (typeof obj.config.settings.mqtt.auth.keyid == 'string') && (typeof obj.config.settings.mqtt.auth.key == 'string')) { obj.mqttbroker = require("./mqttbroker.js").CreateMQTTBroker(obj, obj.db, obj.args); }
+
+ // Start the web server and if needed, the redirection web server.
+ obj.webserver = require('./webserver.js').CreateWebServer(obj, obj.db, obj.args, obj.certificates, obj.StartEx5);
+ if (obj.redirserver != null) { obj.redirserver.hookMainWebServer(obj.certificates); }
+
+ // Change RelayDNS to a array of strings
+ if (typeof obj.args.relaydns == 'string') { obj.args.relaydns = [obj.args.relaydns]; }
+ if (obj.common.validateStrArray(obj.args.relaydns, 1) == false) { delete obj.args.relaydns; }
+
+ // Start the HTTP relay web server if needed
+ if ((obj.args.relaydns == null) && (typeof obj.args.relayport == 'number') && (obj.args.relayport != 0)) {
+ obj.webrelayserver = require('./webrelayserver.js').CreateWebRelayServer(obj, obj.db, obj.args, obj.certificates, function () { });
+ }
+
+ // Update proxy certificates
+ if (obj.supportsProxyCertificatesRequest == true) { obj.updateProxyCertificates(true); }
+
+ // Setup the Intel AMT event handler
+ obj.amtEventHandler = require('./amtevents.js').CreateAmtEventsHandler(obj);
+
+ // Setup the Intel AMT local network scanner
+ if (obj.args.wanonly != true) {
+ if (obj.args.amtscanner != false) { obj.amtScanner = require('./amtscanner.js').CreateAmtScanner(obj).start(); }
+ if (obj.args.meshscanner != false) { obj.meshScanner = require('./meshscanner.js').CreateMeshScanner(obj).start(); }
+ }
+
+ // Setup and start the MPS server
+ obj.mpsserver = require('./mpsserver.js').CreateMpsServer(obj, obj.db, obj.args, obj.certificates);
+
+ // Setup the Intel AMT manager
+ if (obj.args.amtmanager !== false) {
+ obj.amtManager = require('./amtmanager.js').CreateAmtManager(obj);
+ }
+
+ // Setup and start the legacy swarm server
+ if ((obj.certificates.swarmserver != null) && (obj.args.swarmport != null) && (obj.args.swarmport !== 0)) {
+ obj.swarmserver = require('./swarmserver.js').CreateSwarmServer(obj, obj.db, obj.args, obj.certificates);
+ }
+
+ // Setup the main email server
+ if (obj.config.sendgrid != null) {
+ // Sendgrid server
+ obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
+ obj.mailserver.verify();
+ if (obj.args.lanonly == true) { addServerWarning("SendGrid server has limited use in LAN mode.", 17); }
+ } else if (obj.config.smtp != null) {
+ // SMTP server
+ obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
+ obj.mailserver.verify();
+ if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
+ } else if (obj.config.sendmail != null) {
+ // Sendmail server
+ obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
+ obj.mailserver.verify();
+ if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
+ }
+
+ // Setup the email server for each domain
+ for (i in obj.config.domains) {
+ if (obj.config.domains[i].sendgrid != null) {
+ // Sendgrid server
+ obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
+ obj.config.domains[i].mailserver.verify();
+ if (obj.args.lanonly == true) { addServerWarning("SendGrid server has limited use in LAN mode.", 17); }
+ } else if ((obj.config.domains[i].smtp != null) && (obj.config.domains[i].smtp.host != null) && (obj.config.domains[i].smtp.from != null)) {
+ // SMTP server
+ obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
+ obj.config.domains[i].mailserver.verify();
+ if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
+ } else if (obj.config.domains[i].sendmail != null) {
+ // Sendmail server
+ obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
+ obj.config.domains[i].mailserver.verify();
+ if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
+ } else {
+ // Setup the parent mail server for this domain
+ if (obj.mailserver != null) { obj.config.domains[i].mailserver = obj.mailserver; }
+ }
+ }
+
+ // Setup SMS gateway
+ if (config.sms != null) {
+ obj.smsserver = require('./meshsms.js').CreateMeshSMS(obj);
+ if ((obj.smsserver != null) && (obj.args.lanonly == true)) { addServerWarning("SMS gateway has limited use in LAN mode.", 19); }
+ }
+
+ // Setup user messaging
+ if (config.messaging != null) {
+ obj.msgserver = require('./meshmessaging.js').CreateServer(obj);
+ }
+
+ // Setup web based push notifications
+ if ((typeof config.settings.webpush == 'object') && (typeof config.settings.webpush.email == 'string')) {
+ obj.webpush = require('web-push');
+ var vapidKeys = null;
+ try { vapidKeys = JSON.parse(obj.fs.readFileSync(obj.path.join(obj.datapath, 'vapid.json')).toString()); } catch (ex) { }
+ if ((vapidKeys == null) || (typeof vapidKeys.publicKey != 'string') || (typeof vapidKeys.privateKey != 'string')) {
+ console.log("Generating web push VAPID keys...");
+ vapidKeys = obj.webpush.generateVAPIDKeys();
+ obj.common.moveOldFiles([obj.path.join(obj.datapath, 'vapid.json')]);
+ obj.fs.writeFileSync(obj.path.join(obj.datapath, 'vapid.json'), JSON.stringify(vapidKeys));
+ }
+ obj.webpush.vapidPublicKey = vapidKeys.publicKey;
+ obj.webpush.setVapidDetails('mailto:' + config.settings.webpush.email, vapidKeys.publicKey, vapidKeys.privateKey);
+ if (typeof config.settings.webpush.gcmapi == 'string') { webpush.setGCMAPIKey(config.settings.webpush.gcmapi); }
+ }
+
+ // Get the current node version
+ const verSplit = process.version.substring(1).split('.');
+ var nodeVersion = parseInt(verSplit[0]) + (parseInt(verSplit[1]) / 100);
+
+ // Setup Firebase
+ if ((config.firebase != null) && (typeof config.firebase.senderid == 'string') && (typeof config.firebase.serverkey == 'string')) {
+ addServerWarning('Firebase now requires a service account JSON file, Firebase disabled.', 27);
+ } else if ((config.firebase != null) && (typeof config.firebase.serviceaccountfile == 'string')) {
+ var serviceAccount;
+ try { serviceAccount = JSON.parse(obj.fs.readFileSync(obj.path.join(obj.datapath, config.firebase.serviceaccountfile)).toString()); } catch (ex) { console.log(ex); }
+ if (serviceAccount != null) { obj.firebase = require('./firebase').CreateFirebase(obj, serviceAccount); }
+ } else if ((typeof config.firebaserelay == 'object') && (typeof config.firebaserelay.url == 'string')) {
+ // Setup the push messaging relay
+ obj.firebase = require('./firebase').CreateFirebaseRelay(obj, config.firebaserelay.url, config.firebaserelay.key);
+ } else if (obj.config.settings.publicpushnotifications === true) {
+ // Setup the Firebase push messaging relay using https://alt.meshcentral.com, this is the public push notification server.
+ obj.firebase = require('./firebase').CreateFirebaseRelay(obj, 'https://alt.meshcentral.com/firebaserelay.aspx');
+ }
+
+ // Setup monitoring
+ obj.monitoring = require('./monitoring.js').CreateMonitoring(obj, obj.args);
+
+ // Start periodic maintenance
+ obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 60 * 60); // Run this every hour
+ //obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 10 * 1); // DEBUG: Run this more often
+
+ // Dispatch an event that the server is now running
+ obj.DispatchEvent(['*'], obj, { etype: 'server', action: 'started', msg: 'Server started' });
+
+ // Plugin hook. Need to run something at server startup? This is the place.
+ if (obj.pluginHandler) { obj.pluginHandler.callHook('server_startup'); }
+
+ // Setup the login cookie encryption key
+ if ((obj.config) && (obj.config.settings) && (typeof obj.config.settings.logincookieencryptionkey == 'string')) {
+ // We have a string, hash it and use that as a key
+ try { obj.loginCookieEncryptionKey = Buffer.from(obj.config.settings.logincookieencryptionkey, 'hex'); } catch (ex) { }
+ if ((obj.loginCookieEncryptionKey == null) || (obj.loginCookieEncryptionKey.length != 80)) { addServerWarning("Invalid \"LoginCookieEncryptionKey\" in config.json.", 20); obj.loginCookieEncryptionKey = null; }
+ }
+
+ // Login cookie encryption key not set, use one from the database
+ if (obj.loginCookieEncryptionKey == null) {
+ obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
+ if ((docs != null) && (docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 160)) {
+ obj.loginCookieEncryptionKey = Buffer.from(docs[0].key, 'hex');
+ } else {
+ obj.loginCookieEncryptionKey = obj.generateCookieKey(); obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() });
+ }
+ });
+ }
+
+ // Load the invitation link encryption key from the database
+ obj.db.Get('InvitationLinkEncryptionKey', function (err, docs) {
+ if ((docs != null) && (docs.length > 0) && (docs[0].key != null) && (docs[0].key.length >= 160)) {
+ obj.invitationLinkEncryptionKey = Buffer.from(docs[0].key, 'hex');
+ } else {
+ obj.invitationLinkEncryptionKey = obj.generateCookieKey(); obj.db.Set({ _id: 'InvitationLinkEncryptionKey', key: obj.invitationLinkEncryptionKey.toString('hex'), time: Date.now() });
+ }
+ });
+
+ // Setup Intel AMT hello server
+ if ((typeof config.settings.amtprovisioningserver == 'object') && (typeof config.settings.amtprovisioningserver.devicegroup == 'string') && (typeof config.settings.amtprovisioningserver.newmebxpassword == 'string') && (typeof config.settings.amtprovisioningserver.trustedfqdn == 'string') && (typeof config.settings.amtprovisioningserver.ip == 'string')) {
+ obj.amtProvisioningServer = require('./amtprovisioningserver').CreateAmtProvisioningServer(obj, config.settings.amtprovisioningserver);
+ }
+
+ // Start collecting server stats every 5 minutes
+ obj.trafficStats = obj.webserver.getTrafficStats();
+ setInterval(function () {
+ obj.serverStatsCounter++;
+ var hours = 720; // Start with all events lasting 30 days.
+ if (((obj.serverStatsCounter) % 2) == 1) { hours = 3; } // Half of the event get removed after 3 hours.
+ else if ((Math.floor(obj.serverStatsCounter / 2) % 2) == 1) { hours = 8; } // Another half of the event get removed after 8 hours.
+ else if ((Math.floor(obj.serverStatsCounter / 4) % 2) == 1) { hours = 24; } // Another half of the event get removed after 24 hours.
+ else if ((Math.floor(obj.serverStatsCounter / 8) % 2) == 1) { hours = 48; } // Another half of the event get removed after 48 hours.
+ else if ((Math.floor(obj.serverStatsCounter / 16) % 2) == 1) { hours = 72; } // Another half of the event get removed after 72 hours.
+ const expire = new Date();
+ expire.setTime(expire.getTime() + (60 * 60 * 1000 * hours));
+
+ // Get traffic data
+ var trafficStats = obj.webserver.getTrafficDelta(obj.trafficStats);
+ obj.trafficStats = trafficStats.current;
+
+ var data = {
+ time: new Date(),
+ expire: expire,
+ mem: process.memoryUsage(),
+ conn: {
+ ca: Object.keys(obj.webserver.wsagents).length,
+ cu: Object.keys(obj.webserver.wssessions).length,
+ us: Object.keys(obj.webserver.wssessions2).length,
+ rs: obj.webserver.relaySessionCount,
+ am: 0
+ },
+ traffic: trafficStats.delta
+ };
+ try { data.cpu = require('os').loadavg(); } catch (ex) { }
+ if (obj.mpsserver != null) {
+ data.conn.amc = 0;
+ for (var i in obj.mpsserver.ciraConnections) { data.conn.amc += obj.mpsserver.ciraConnections[i].length; }
+ }
+ for (var i in obj.connectivityByNode) {
+ const node = obj.connectivityByNode[i];
+ if (node && typeof node.connectivity !== 'undefined' && node.connectivity === 4) { data.conn.am++; }
+ }
+ if (obj.firstStats === true) { delete obj.firstStats; data.first = true; }
+ if (obj.multiServer != null) { data.s = obj.multiServer.serverid; }
+ obj.db.SetServerStats(data); // Save the stats to the database
+ obj.DispatchEvent(['*'], obj, { action: 'servertimelinestats', data: data }); // Event the server stats
+ }, 300000);
+
+ obj.debug('main', "Server started");
+ if (obj.args.nousers == true) { obj.updateServerState('nousers', '1'); }
+ obj.updateServerState('state', "running");
+
+ // Setup auto-backup defaults. Unless autobackup is set to false try to make a backup.
+ if (obj.config.settings.autobackup == false || obj.config.settings.autobackup == 'false') { obj.config.settings.autobackup = {backupintervalhours: -1}; } //block all autobackup functions
+ else {
+ if (typeof obj.config.settings.autobackup != 'object') { obj.config.settings.autobackup = {}; };
+ if (typeof obj.config.settings.autobackup.backupintervalhours != 'number') { obj.config.settings.autobackup.backupintervalhours = 24; };
+ if (typeof obj.config.settings.autobackup.keeplastdaysbackup != 'number') { obj.config.settings.autobackup.keeplastdaysbackup = 10; };
+ if (obj.config.settings.autobackup.backuphour != null ) { obj.config.settings.autobackup.backupintervalhours = 24; if ((typeof obj.config.settings.autobackup.backuphour != 'number') || (obj.config.settings.autobackup.backuphour > 23 || obj.config.settings.autobackup.backuphour < 0 )) { obj.config.settings.autobackup.backuphour = 0; }}
+ else {obj.config.settings.autobackup.backuphour = -1 };
+ //arrayfi in case of string and remove possible ', ' space. !! If a string instead of an array is passed, it will be split by ',' so *{.txt,.log} won't work in that case !!
+ if (!obj.config.settings.autobackup.backupignorefilesglob) {obj.config.settings.autobackup.backupignorefilesglob = []}
+ else if (typeof obj.config.settings.autobackup.backupignorefilesglob == 'string') { obj.config.settings.autobackup.backupignorefilesglob = obj.config.settings.autobackup.backupignorefilesglob.replaceAll(', ', ',').split(','); };
+ if (!obj.config.settings.autobackup.backupskipfoldersglob) {obj.config.settings.autobackup.backupskipfoldersglob = []}
+ else if (typeof obj.config.settings.autobackup.backupskipfoldersglob == 'string') { obj.config.settings.autobackup.backupskipfoldersglob = obj.config.settings.autobackup.backupskipfoldersglob.replaceAll(', ', ',').split(','); };
+ if (typeof obj.config.settings.autobackup.backuppath == 'string') { obj.backuppath = (obj.config.settings.autobackup.backuppath = (obj.path.resolve(obj.config.settings.autobackup.backuppath))) } else { obj.config.settings.autobackup.backuppath = obj.backuppath };
+ if (typeof obj.config.settings.autobackup.backupname != 'string') { obj.config.settings.autobackup.backupname = 'meshcentral-autobackup-'};
+ if (typeof obj.config.settings.autobackup.webdav == 'object') {
+ //make webdav compliant: http://www.webdav.org/specs/rfc4918.html#rfc.section.5.2, http://www.webdav.org/specs/rfc2518.html#METHOD_MKCOL
+ // So with leading and trailing slash in the foldername, and no double and backslashes
+ if (typeof obj.config.settings.autobackup.webdav.foldername != 'string') {obj.config.settings.autobackup.webdav.foldername = '/MeshCentral-Backups/'}
+ else {obj.config.settings.autobackup.webdav.foldername = ('/' + obj.config.settings.autobackup.webdav.foldername + '/').replaceAll("\\", "/").replaceAll("//", "/").replaceAll("//", "/")};
+ }
+ }
+
+ // Check if the database is capable of performing a backup
+ obj.db.checkBackupCapability(function (err, msg) { if (msg != null) { obj.addServerWarning(msg, true) } });
+
+ // Load Intel AMT passwords from the "amtactivation.log" file
+ obj.loadAmtActivationLogPasswords(function (amtPasswords) {
+ obj.amtPasswords = amtPasswords;
+ });
+
+ // Setup users that can see all device groups
+ if (typeof obj.config.settings.managealldevicegroups == 'string') { obj.config.settings.managealldevicegroups = obj.config.settings.managealldevicegroups.split(','); }
+ else if (Array.isArray(obj.config.settings.managealldevicegroups) == false) { obj.config.settings.managealldevicegroups = []; }
+ for (i in obj.config.domains) {
+ if (Array.isArray(obj.config.domains[i].managealldevicegroups)) {
+ for (var j in obj.config.domains[i].managealldevicegroups) {
+ if (typeof obj.config.domains[i].managealldevicegroups[j] == 'string') {
+ const u = 'user/' + i + '/' + obj.config.domains[i].managealldevicegroups[j];
+ if (obj.config.settings.managealldevicegroups.indexOf(u) == -1) { obj.config.settings.managealldevicegroups.push(u); }
+ }
+ }
+ }
+ }
+ obj.config.settings.managealldevicegroups.sort();
+
+ // Start watchdog timer if needed
+ // This is used to monitor if NodeJS is servicing IO correctly or getting held up a lot. Add this line to the settings section of config.json
+ // "watchDog": { "interval": 100, "timeout": 150 }
+ // This will check every 100ms, if the timer is more than 150ms late, it will warn.
+ if ((typeof config.settings.watchdog == 'object') && (typeof config.settings.watchdog.interval == 'number') && (typeof config.settings.watchdog.timeout == 'number') && (config.settings.watchdog.interval >= 50) && (config.settings.watchdog.timeout >= 50)) {
+ obj.watchdogtime = Date.now();
+ obj.watchdogmax = 0;
+ obj.watchdogmaxtime = null;
+ obj.watchdogtable = [];
+ obj.watchdog = setInterval(function () {
+ const now = Date.now(), delta = now - obj.watchdogtime - config.settings.watchdog.interval;
+ if (delta > obj.watchdogmax) { obj.watchdogmax = delta; obj.watchdogmaxtime = new Date().toLocaleString(); }
+ if (delta > config.settings.watchdog.timeout) {
+ const msg = obj.common.format("Watchdog timer timeout, {0}ms.", delta);
+ obj.watchdogtable.push(new Date().toLocaleString() + ', ' + delta + 'ms');
+ while (obj.watchdogtable.length > 10) { obj.watchdogtable.shift(); }
+ obj.debug('main', msg);
+ try {
+ var errlogpath = null;
+ if (typeof obj.args.mesherrorlogpath == 'string') { errlogpath = obj.path.join(obj.args.mesherrorlogpath, 'mesherrors.txt'); } else { errlogpath = obj.getConfigFilePath('mesherrors.txt'); }
+ obj.fs.appendFileSync(errlogpath, new Date().toLocaleString() + ': ' + msg + '\r\n');
+ } catch (ex) { console.log('ERROR: Unable to write to mesherrors.txt.'); }
+ }
+ obj.watchdogtime = now;
+ }, config.settings.watchdog.interval);
+ obj.debug('main', "Started watchdog timer.");
+ }
+
+ });
+ });
+ });
+ };
+
+ // Called when the web server finished loading
+ obj.StartEx5 = function () {
+ // Setup the email server for each domain
+ var ipKvmSupport = false;
+ for (var i in obj.config.domains) { if (obj.config.domains[i].ipkvm == true) { ipKvmSupport = true; } }
+ if (ipKvmSupport) { obj.ipKvmManager = require('./meshipkvm').CreateIPKVMManager(obj); }
+
+ // Run the server start script if present
+ if (typeof obj.config.settings.runonserverstarted == 'string') {
+ const child_process = require('child_process');
+ var parentpath = __dirname;
+ if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) { parentpath = require('path').join(__dirname, '../..'); }
+ child_process.exec(obj.config.settings.runonserverstarted + ' ' + getCurrentVersion(), { maxBuffer: 512000, timeout: 120000, cwd: parentpath }, function (error, stdout, stderr) { });
+ }
+ }
+
+ // Refresh any certificate hashs from the reverse proxy
+ obj.pendingProxyCertificatesRequests = 0;
+ obj.lastProxyCertificatesRequest = null;
+ obj.supportsProxyCertificatesRequest = false;
+ obj.updateProxyCertificates = function (force) {
+ if (force !== true) {
+ if ((obj.pendingProxyCertificatesRequests > 0) || (obj.supportsProxyCertificatesRequest == false)) return;
+ if ((obj.lastProxyCertificatesRequest != null) && ((Date.now() - obj.lastProxyCertificatesRequest) < 120000)) return; // Don't allow this call more than every 2 minutes.
+ obj.lastProxyCertificatesRequest = Date.now();
+ }
+
+ // Load any domain web certificates
+ for (var i in obj.config.domains) {
+ if (obj.config.domains[i].certurl != null) {
+ // Load web certs
+ obj.pendingProxyCertificatesRequests++;
+ var dnsname = obj.config.domains[i].dns;
+ if ((dnsname == null) && (obj.config.settings.cert != null)) { dnsname = obj.config.settings.cert; }
+ obj.certificateOperations.loadCertificate(obj.config.domains[i].certurl, dnsname, obj.config.domains[i], function (url, cert, xhostname, xdomain) {
+ obj.pendingProxyCertificatesRequests--;
+ if (cert != null) {
+ // Hash the entire cert
+ const hash = obj.crypto.createHash('sha384').update(Buffer.from(cert, 'binary')).digest('hex');
+ if (xdomain.certhash != hash) { // The certificate has changed.
+ xdomain.certkeyhash = hash;
+ xdomain.certhash = hash;
+
+ try {
+ // Decode a RSA certificate and hash the public key, if this is not RSA, skip this.
+ const forgeCert = obj.certificateOperations.forge.pki.certificateFromAsn1(obj.certificateOperations.forge.asn1.fromDer(cert));
+ xdomain.certkeyhash = obj.certificateOperations.forge.pki.getPublicKeyFingerprint(forgeCert.publicKey, { md: obj.certificateOperations.forge.md.sha384.create(), encoding: 'hex' });
+ obj.webserver.webCertificateExpire[xdomain.id] = Date.parse(forgeCert.validity.notAfter); // Update certificate expire time
+ //console.log('V1: ' + xdomain.certkeyhash);
+ } catch (ex) {
+ delete obj.webserver.webCertificateExpire[xdomain.id]; // Remove certificate expire time
+ delete xdomain.certkeyhash;
+ }
+
+ if (obj.webserver) {
+ obj.webserver.webCertificateHashs[xdomain.id] = obj.webserver.webCertificateFullHashs[xdomain.id] = Buffer.from(hash, 'hex').toString('binary');
+ if (xdomain.certkeyhash != null) { obj.webserver.webCertificateHashs[xdomain.id] = Buffer.from(xdomain.certkeyhash, 'hex').toString('binary'); }
+
+ // Disconnect all agents with bad web certificates
+ for (var i in obj.webserver.wsagentsWithBadWebCerts) { obj.webserver.wsagentsWithBadWebCerts[i].close(1); }
+ }
+
+ console.log(obj.common.format("Loaded web certificate from \"{0}\", host: \"{1}\"", url, xhostname));
+ console.log(obj.common.format(" SHA384 cert hash: {0}", xdomain.certhash));
+ if ((xdomain.certkeyhash != null) && (xdomain.certhash != xdomain.certkeyhash)) { console.log(obj.common.format(" SHA384 key hash: {0}", xdomain.certkeyhash)); }
+ }
+ } else {
+ console.log(obj.common.format("Failed to load web certificate at: \"{0}\", host: \"{1}\"", url, xhostname));
+ }
+ });
+ }
+ }
+ }
+
+ // Perform maintenance operations (called every hour)
+ obj.maintenanceActions = function () {
+ // Perform database maintenance
+ obj.db.maintenance();
+
+ // Clean up any temporary files
+ const removeTime = new Date(Date.now()).getTime() - (30 * 60 * 1000); // 30 minutes
+ const dir = obj.fs.readdir(obj.path.join(obj.filespath, 'tmp'), function (err, files) {
+ if (err != null) return;
+ for (var i in files) { try { const filepath = obj.path.join(obj.filespath, 'tmp', files[i]); if (obj.fs.statSync(filepath).mtime.getTime() < removeTime) { obj.fs.unlink(filepath, function () { }); } } catch (ex) { } }
+ });
+
+ // Check for self-update that targets a specific version
+ if ((typeof obj.args.selfupdate == 'string') && (getCurrentVersion() === obj.args.selfupdate)) { obj.args.selfupdate = false; }
+
+ // Check if we need to perform server self-update
+ if ((obj.args.selfupdate) && (obj.serverSelfWriteAllowed == true)) {
+ obj.db.getValueOfTheDay('performSelfUpdate', 1, function (performSelfUpdate) {
+ if (performSelfUpdate.value > 0) {
+ performSelfUpdate.value--;
+ obj.db.Set(performSelfUpdate);
+ obj.getLatestServerVersion(function (currentVer, latestVer) { if (currentVer != latestVer) { obj.performServerUpdate(); return; } });
+ } else {
+ checkAutobackup();
+ }
+ });
+ } else {
+ checkAutobackup();
+ }
+ };
+
+ // Check if we need to perform an automatic backup
+ function checkAutobackup() {
+ if (obj.config.settings.autobackup.backupintervalhours >= 1 ) {
+ obj.db.Get('LastAutoBackupTime', function (err, docs) {
+ if (err != null) { console.error("checkAutobackup: Error getting LastBackupTime from DB"); return}
+ var lastBackup = 0;
+ const currentdate = new Date();
+ let currentHour = currentdate.getHours();
+ let now = currentdate.getTime();
+ if (docs.length == 1) { lastBackup = docs[0].value; }
+ const delta = now - lastBackup;
+ //const delta = 9999999999; // DEBUG: backup always
+ obj.debug ('backup', 'Entering checkAutobackup, lastAutoBackupTime: ' + new Date(lastBackup).toLocaleString('default', { dateStyle: 'medium', timeStyle: 'short' }) + ', delta: ' + (delta/(1000*60*60)).toFixed(2) + ' hours');
+ //start autobackup if interval has passed or at configured hour, whichever comes first. When an hour schedule is missed, it will make a backup immediately.
+ if ((delta > (obj.config.settings.autobackup.backupintervalhours * 60 * 60 * 1000)) || ((currentHour == obj.config.settings.autobackup.backuphour) && (delta >= 2 * 60 * 60 * 1000))) {
+ // A new auto-backup is required.
+ obj.db.Set({ _id: 'LastAutoBackupTime', value: now }); // Save the current time in the database
+ obj.db.performBackup(); // Perform the backup
+ }
+ });
+ }
+ }
+
+ // Stop the Meshcentral server
+ obj.Stop = function (restoreFile) {
+ // If the database is not setup, exit now.
+ if (!obj.db) return;
+
+ // Dispatch an event saying the server is now stopping
+ obj.DispatchEvent(['*'], obj, { etype: 'server', action: 'stopped', msg: "Server stopped" });
+
+ // Set all nodes to power state of unknown (0)
+ obj.db.storePowerEvent({ time: new Date(), nodeid: '*', power: 0, s: 2 }, obj.multiServer, function () { // s:2 indicates that the server is shutting down.
+ if (restoreFile) {
+ obj.debug('main', obj.common.format("Server stopped, updating settings: {0}", restoreFile));
+ console.log("Updating settings folder...");
+
+ const yauzl = require('yauzl');
+ yauzl.open(restoreFile, { lazyEntries: true }, function (err, zipfile) {
+ if (err) throw err;
+ zipfile.readEntry();
+ zipfile.on('entry', function (entry) {
+ if (/\/$/.test(entry.fileName)) {
+ // Directory file names end with '/'.
+ // Note that entires for directories themselves are optional.
+ // An entry's fileName implicitly requires its parent directories to exist.
+ zipfile.readEntry();
+ } else {
+ // File entry
+ zipfile.openReadStream(entry, function (err, readStream) {
+ if (err) throw err;
+ readStream.on('end', function () { zipfile.readEntry(); });
+ var directory = obj.path.dirname(entry.fileName);
+ if (directory != '.') {
+ directory = obj.getConfigFilePath(directory)
+ if (obj.fs.existsSync(directory) == false) { obj.fs.mkdirSync(directory); }
+ }
+ //console.log('Extracting:', obj.getConfigFilePath(entry.fileName));
+ readStream.pipe(obj.fs.createWriteStream(obj.getConfigFilePath(entry.fileName)));
+ });
+ }
+ });
+ zipfile.on('end', function () { setTimeout(function () { obj.fs.unlinkSync(restoreFile); process.exit(123); }); });
+ });
+ } else {
+ obj.debug('main', "Server stopped");
+ process.exit(0);
+ }
+ });
+
+ // Update the server state
+ obj.updateServerState('state', "stopped");
+ };
+
+ // Event Dispatch
+ obj.AddEventDispatch = function (ids, target) {
+ obj.debug('dispatch', 'AddEventDispatch', ids);
+ for (var i in ids) { var id = ids[i]; if (!obj.eventsDispatch[id]) { obj.eventsDispatch[id] = [target]; } else { obj.eventsDispatch[id].push(target); } }
+ };
+ obj.RemoveEventDispatch = function (ids, target) {
+ obj.debug('dispatch', 'RemoveEventDispatch', ids);
+ for (var i in ids) {
+ const id = ids[i];
+ if (obj.eventsDispatch[id]) {
+ var j = obj.eventsDispatch[id].indexOf(target);
+ if (j >= 0) {
+ if (obj.eventsDispatch[id].length == 1) {
+ delete obj.eventsDispatch[id];
+ } else {
+ const newList = []; // We create a new list so not to modify the original list. Allows this function to be called during an event dispatch.
+ for (var k in obj.eventsDispatch[i]) { if (obj.eventsDispatch[i][k] != target) { newList.push(obj.eventsDispatch[i][k]); } }
+ obj.eventsDispatch[i] = newList;
+ }
+ }
+ }
+ }
+ };
+ obj.RemoveEventDispatchId = function (id) {
+ obj.debug('dispatch', 'RemoveEventDispatchId', id);
+ if (obj.eventsDispatch[id] != null) { delete obj.eventsDispatch[id]; }
+ };
+ obj.RemoveAllEventDispatch = function (target) {
+ obj.debug('dispatch', 'RemoveAllEventDispatch');
+ for (var i in obj.eventsDispatch) {
+ const j = obj.eventsDispatch[i].indexOf(target);
+ if (j >= 0) {
+ if (obj.eventsDispatch[i].length == 1) {
+ delete obj.eventsDispatch[i];
+ } else {
+ const newList = []; // We create a new list so not to modify the original list. Allows this function to be called during an event dispatch.
+ for (var k in obj.eventsDispatch[i]) { if (obj.eventsDispatch[i][k] != target) { newList.push(obj.eventsDispatch[i][k]); } }
+ obj.eventsDispatch[i] = newList;
+ }
+ }
+ }
+ };
+ obj.DispatchEvent = function (ids, source, event, fromPeerServer) {
+ // If the database is not setup, exit now.
+ if (!obj.db) return;
+
+ // Send event to syslog if needed
+ if (obj.syslog && event.msg) { obj.syslog.log(obj.syslog.LOG_INFO, event.msg); }
+ if (obj.syslogjson) { obj.syslogjson.log(obj.syslogjson.LOG_INFO, JSON.stringify(event)); }
+ if (obj.syslogtcp && event.msg) { obj.syslogtcp.log(event.msg, obj.syslogtcp.LOG_INFO); }
+
+ obj.debug('dispatch', 'DispatchEvent', ids);
+ if ((typeof event == 'object') && (!event.nolog)) {
+ event.time = new Date();
+ // The event we store is going to skip some of the fields so we don't store too much stuff in the database.
+ const storeEvent = Object.assign({}, event);
+ if (storeEvent.node) { delete storeEvent.node; } // Skip the "node" field. May skip more in the future.
+ if (storeEvent.links) {
+ // Escape "links" names that may have "." and/or "$"
+ storeEvent.links = Object.assign({}, storeEvent.links);
+ for (var i in storeEvent.links) { var ue = obj.common.escapeFieldName(i); if (ue !== i) { storeEvent.links[ue] = storeEvent.links[i]; delete storeEvent.links[i]; } }
+ }
+ if (storeEvent.mesh) {
+ // Escape "mesh" names that may have "." and/or "$"
+ storeEvent.mesh = obj.common.escapeLinksFieldNameEx(storeEvent.mesh);
+ }
+ storeEvent.ids = ids;
+ obj.db.StoreEvent(storeEvent);
+ }
+ const targets = []; // List of targets we dispatched the event to, we don't want to dispatch to the same target twice.
+ for (var j in ids) {
+ const id = ids[j];
+ const eventsDispatch = obj.eventsDispatch[id];
+ if (eventsDispatch) {
+ for (var i in eventsDispatch) {
+ if (targets.indexOf(eventsDispatch[i]) == -1) { // Check if we already displatched to this target
+ targets.push(eventsDispatch[i]);
+ try { eventsDispatch[i].HandleEvent(source, event, ids, id); } catch (ex) { console.log(ex, eventsDispatch[i]); }
+ }
+ }
+ }
+ }
+ if ((fromPeerServer == null) && (obj.multiServer != null) && ((typeof event != 'object') || (event.nopeers != 1))) { obj.multiServer.DispatchEvent(ids, source, event); }
+ };
+
+ // Get the connection state of a node
+ obj.GetConnectivityState = function (nodeid) { return obj.connectivityByNode[nodeid]; };
+
+ // Get the routing server id for a given node and connection type, can never be self.
+ obj.GetRoutingServerIdNotSelf = function (nodeid, connectType) {
+ if (obj.multiServer == null) return null;
+ for (var serverid in obj.peerConnectivityByNode) {
+ if (serverid == obj.serverId) continue;
+ var state = obj.peerConnectivityByNode[serverid][nodeid];
+ if ((state != null) && ((state.connectivity & connectType) != 0)) { return { serverid: serverid, meshid: state.meshid }; }
+ }
+ return null;
+ };
+
+ // Get the routing server id for a given node and connection type, self first
+ obj.GetRoutingServerId = function (nodeid, connectType) {
+ if (obj.multiServer == null) return null;
+
+ // Look at our own server first
+ var connections = obj.peerConnectivityByNode[obj.serverId];
+ if (connections != null) {
+ var state = connections[nodeid];
+ if ((state != null) && ((state.connectivity & connectType) != 0)) { return { serverid: obj.serverId, meshid: state.meshid }; }
+ }
+
+ // Look at other servers
+ for (var serverid in obj.peerConnectivityByNode) {
+ if (serverid == obj.serverId) continue;
+ var state = obj.peerConnectivityByNode[serverid][nodeid];
+ if ((state != null) && ((state.connectivity & connectType) != 0)) { return { serverid: serverid, meshid: state.meshid }; }
+ }
+ return null;
+ };
+
+ // Update the connection state of a node when in multi-server mode
+ // Update obj.connectivityByNode using obj.peerConnectivityByNode for the list of nodes in argument
+ obj.UpdateConnectivityState = function (nodeids) {
+ for (var nodeid in nodeids) {
+ var meshid = null, state = null, oldConnectivity = 0, oldPowerState = 0, newConnectivity = 0, newPowerState = 0;
+ var oldState = obj.connectivityByNode[nodeid];
+ if (oldState != null) { meshid = oldState.meshid; oldConnectivity = oldState.connectivity; oldPowerState = oldState.powerState; }
+ for (var serverid in obj.peerConnectivityByNode) {
+ var peerState = obj.peerConnectivityByNode[serverid][nodeid];
+ if (peerState != null) {
+ if (state == null) {
+ // Copy the state
+ state = {};
+ newConnectivity = state.connectivity = peerState.connectivity;
+ newPowerState = state.powerState = peerState.powerState;
+ meshid = state.meshid = peerState.meshid;
+ //if (peerState.agentPower) { state.agentPower = peerState.agentPower; }
+ //if (peerState.ciraPower) { state.ciraPower = peerState.ciraPower; }
+ //if (peerState.amtPower) { state.amtPower = peerState.amtPower; }
+ } else {
+ // Merge the state
+ state.connectivity |= peerState.connectivity;
+ newConnectivity = state.connectivity;
+ if ((peerState.powerState != 0) && ((state.powerState == 0) || (peerState.powerState < state.powerState))) { newPowerState = state.powerState = peerState.powerState; }
+ meshid = state.meshid = peerState.meshid;
+ //if (peerState.agentPower) { state.agentPower = peerState.agentPower; }
+ //if (peerState.ciraPower) { state.ciraPower = peerState.ciraPower; }
+ //if (peerState.amtPower) { state.amtPower = peerState.amtPower; }
+ }
+ }
+ }
+ obj.connectivityByNode[nodeid] = state;
+
+ //console.log('xx', nodeid, meshid, newConnectivity, oldPowerState, newPowerState, oldPowerState);
+
+ // Event any changes on this server only
+ if ((newConnectivity != oldPowerState) || (newPowerState != oldPowerState)) {
+ obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: newConnectivity, pwr: newPowerState, nolog: 1, nopeers: 1, id: Math.random() });
+ }
+ }
+ };
+
+ // See if we need to notifiy any user of device state change
+ obj.NotifyUserOfDeviceStateChange = function (meshid, nodeid, connectTime, connectType, powerState, serverid, stateSet, extraInfo) {
+ // Check if there is a email server for this domain
+ const meshSplit = meshid.split('/');
+ if (meshSplit.length != 3) return;
+ const domainId = meshSplit[1];
+ if (obj.config.domains[domainId] == null) return;
+ const mailserver = obj.config.domains[domainId].mailserver;
+ if ((mailserver == null) && (obj.msgserver == null)) return;
+
+ // Get the device group for this device
+ const mesh = obj.webserver.meshes[meshid];
+ if ((mesh == null) || (mesh.links == null)) return;
+
+ // Get the list of users that have visibility to this device
+ // This includes users that are part of user groups
+ const users = [];
+ for (var i in mesh.links) {
+ if (i.startsWith('user/') && (users.indexOf(i) < 0)) { users.push(i); }
+ if (i.startsWith('ugrp/')) {
+ var usergrp = obj.webserver.userGroups[i];
+ if (usergrp.links != null) { for (var j in usergrp.links) { if (j.startsWith('user/') && (users.indexOf(j) < 0)) { users.push(j); } } }
+ }
+ }
+
+ // Check if any user needs email notification
+ for (var i in users) {
+ const user = obj.webserver.users[users[i]];
+ if (user != null) {
+ var notify = 0;
+
+ // Device group notifications
+ const meshLinks = user.links[meshid];
+ if ((meshLinks != null) && (meshLinks.notify != null)) { notify |= meshLinks.notify; }
+
+ // User notifications
+ if (user.notify != null) {
+ if (user.notify[meshid] != null) { notify |= user.notify[meshid]; }
+ if (user.notify[nodeid] != null) { notify |= user.notify[nodeid]; }
+ }
+
+ // Email notifications
+ if ((user.email != null) && (user.emailVerified == true) && (mailserver != null) && ((notify & 48) != 0)) {
+ if (stateSet == true) {
+ if ((notify & 16) != 0) {
+ mailserver.notifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
+ } else {
+ mailserver.cancelNotifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
+ }
+ }
+ else if (stateSet == false) {
+ if ((notify & 32) != 0) {
+ mailserver.notifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
+ } else {
+ mailserver.cancelNotifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
+ }
+ }
+ }
+
+ // Messaging notifications
+ if ((obj.msgserver != null) && ((notify & 384) != 0)) {
+ if (stateSet == true) {
+ if ((notify & 128) != 0) {
+ obj.msgserver.notifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
+ } else {
+ obj.msgserver.cancelNotifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
+ }
+ }
+ else if (stateSet == false) {
+ if ((notify & 256) != 0) {
+ obj.msgserver.notifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
+ } else {
+ obj.msgserver.cancelNotifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // See if we need to notifiy any user of device requested help
+ //if (typeof device.name == 'string') { parent.parent.NotifyUserOfDeviceHelpRequest(domain, device._id, device.meshid, device.name, command.msgArgs[0], command.msgArgs[1]); }
+
+ obj.NotifyUserOfDeviceHelpRequest = function (domain, meshid, nodeid, devicename, helpusername, helprequest) {
+ // Check if there is a email server for this domain
+ const meshSplit = meshid.split('/');
+ if (meshSplit.length != 3) return;
+ const domainId = meshSplit[1];
+ if (obj.config.domains[domainId] == null) return;
+ const mailserver = obj.config.domains[domainId].mailserver;
+ if ((mailserver == null) && (obj.msgserver == null)) return;
+
+ // Get the device group for this device
+ const mesh = obj.webserver.meshes[meshid];
+ if ((mesh == null) || (mesh.links == null)) return;
+
+ // Get the list of users that have visibility to this device
+ // This includes users that are part of user groups
+ const users = [];
+ for (var i in mesh.links) {
+ if (i.startsWith('user/') && (users.indexOf(i) < 0)) { users.push(i); }
+ if (i.startsWith('ugrp/')) {
+ var usergrp = obj.webserver.userGroups[i];
+ if (usergrp.links != null) { for (var j in usergrp.links) { if (j.startsWith('user/') && (users.indexOf(j) < 0)) { users.push(j); } } }
+ }
+ }
+
+ // Check if any user needs email notification
+ for (var i in users) {
+ const user = obj.webserver.users[users[i]];
+ if (user != null) {
+ var notify = 0;
+
+ // Device group notifications
+ const meshLinks = user.links[meshid];
+ if ((meshLinks != null) && (meshLinks.notify != null)) { notify |= meshLinks.notify; }
+
+ // User notifications
+ if (user.notify != null) {
+ if (user.notify[meshid] != null) { notify |= user.notify[meshid]; }
+ if (user.notify[nodeid] != null) { notify |= user.notify[nodeid]; }
+ }
+
+ // Mail help request
+ if ((user.email != null) && (user.emailVerified == true) && ((notify & 64) != 0)) { mailserver.sendDeviceHelpMail(domain, user.name, user.email, devicename, nodeid, helpusername, helprequest, user.llang); }
+
+ // Message help request
+ if ((user.msghandle != null) && ((notify & 512) != 0)) { obj.msgserver.sendDeviceHelpRequest(domain, user.name, user.msghandle, devicename, nodeid, helpusername, helprequest, user.llang); }
+ }
+ }
+ }
+
+ // Set the connectivity state of a node and setup the server so that messages can be routed correctly.
+ // meshId: mesh identifier of format mesh/domain/meshidhex
+ // nodeId: node identifier of format node/domain/nodeidhex
+ // connectTime: time of connection, milliseconds elapsed since the UNIX epoch.
+ // connectType: Bitmask, 1 = MeshAgent, 2 = Intel AMT CIRA, 4 = Intel AMT local, 8 = Intel AMT Relay, 16 = MQTT
+ // powerState: Value, 0 = Unknown, 1 = S0 power on, 2 = S1 Sleep, 3 = S2 Sleep, 4 = S3 Sleep, 5 = S4 Hibernate, 6 = S5 Soft-Off, 7 = Present, 8 = Off
+ //var connectTypeStrings = ['', 'MeshAgent', 'Intel AMT CIRA', '', 'Intel AMT local', '', '', '', 'Intel AMT Relay', '', '', '', '', '', '', '', 'MQTT'];
+ //var powerStateStrings = ['Unknown', 'Powered', 'Sleep', 'Sleep', 'Deep Sleep', 'Hibernating', 'Soft-Off', 'Present', 'Off'];
+ obj.SetConnectivityState = function (meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
+ //console.log('SetConnectivity for ' + nodeid.substring(0, 16) + ', Type: ' + connectTypeStrings[connectType] + ', Power: ' + powerStateStrings[powerState] + (serverid == null ? ('') : (', ServerId: ' + serverid)));
+ if ((serverid == null) && (obj.multiServer != null)) { obj.multiServer.DispatchMessage({ action: 'SetConnectivityState', meshid: meshid, nodeid: nodeid, connectTime: connectTime, connectType: connectType, powerState: powerState, extraInfo: extraInfo }); }
+
+ if (obj.multiServer == null) {
+ // Single server mode
+
+ // Change the node connection state
+ var eventConnectChange = 0;
+ var state = obj.connectivityByNode[nodeid];
+ if (state) {
+ // Change the connection in the node and mesh state lists
+ if ((state.connectivity & connectType) == 0) { state.connectivity |= connectType; eventConnectChange = 1; }
+ state.meshid = meshid;
+ } else {
+ // Add the connection to the node and mesh state list
+ obj.connectivityByNode[nodeid] = state = { connectivity: connectType, meshid: meshid };
+ eventConnectChange = 1;
+ }
+
+ // Set node power state
+ if (connectType == 1) { state.agentPower = powerState; } else if (connectType == 2) { state.ciraPower = powerState; } else if (connectType == 4) { state.amtPower = powerState; }
+ var powerState = 0, oldPowerState = state.powerState;
+ if ((state.connectivity & 1) != 0) { powerState = state.agentPower; } else if ((state.connectivity & 2) != 0) { powerState = state.ciraPower; } else if ((state.connectivity & 4) != 0) { powerState = state.amtPower; }
+ if ((state.powerState == null) || (state.powerState == undefined) || (state.powerState != powerState)) {
+ state.powerState = powerState;
+ eventConnectChange = 1;
+
+ // Set new power state in database
+ const record = { time: new Date(connectTime), nodeid: nodeid, power: powerState };
+ if (oldPowerState != null) { record.oldPower = oldPowerState; }
+ obj.db.storePowerEvent(record, obj.multiServer);
+ }
+
+ // Event the node connection change
+ if (eventConnectChange == 1) {
+ obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: state.connectivity, pwr: state.powerState, ct: connectTime, nolog: 1, nopeers: 1, id: Math.random() });
+
+ // Save indication of node connection change
+ const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 1, connectType: connectType };
+ if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
+ obj.db.Set(lc);
+
+ // Notify any users of device connection
+ obj.NotifyUserOfDeviceStateChange(meshid, nodeid, connectTime, connectType, powerState, serverid, true, extraInfo);
+ }
+ } else {
+ // Multi server mode
+
+ // Change the node connection state
+ if (serverid == null) { serverid = obj.serverId; }
+ if (obj.peerConnectivityByNode[serverid] == null) return; // Guard against unknown serverid's
+ var eventConnectChange = 0;
+ var state = obj.peerConnectivityByNode[serverid][nodeid];
+ if (state) {
+ // Change the connection in the node and mesh state lists
+ if ((state.connectivity & connectType) == 0) { state.connectivity |= connectType; eventConnectChange = 1; }
+ state.meshid = meshid;
+ } else {
+ // Add the connection to the node and mesh state list
+ obj.peerConnectivityByNode[serverid][nodeid] = state = { connectivity: connectType, meshid: meshid };
+ eventConnectChange = 1;
+ }
+
+ // Set node power state
+ if (connectType == 1) { state.agentPower = powerState; } else if (connectType == 2) { state.ciraPower = powerState; } else if (connectType == 4) { state.amtPower = powerState; }
+ var powerState = 0, oldPowerState = state.powerState;
+ if ((state.connectivity & 1) != 0) { powerState = state.agentPower; } else if ((state.connectivity & 2) != 0) { powerState = state.ciraPower; } else if ((state.connectivity & 4) != 0) { powerState = state.amtPower; }
+ if ((state.powerState == null) || (state.powerState == undefined) || (state.powerState != powerState)) {
+ state.powerState = powerState;
+ eventConnectChange = 1;
+
+ // Set new power state in database
+ var record = { time: new Date(connectTime), nodeid: nodeid, power: powerState, server: obj.multiServer.serverid };
+ if (oldPowerState != null) { record.oldPower = oldPowerState; }
+ obj.db.storePowerEvent(record, obj.multiServer);
+ }
+
+ if (eventConnectChange == 1) {
+ // Update the combined node state
+ var x = {}; x[nodeid] = 1;
+ obj.UpdateConnectivityState(x);
+
+ // Save indication of node connection change
+ if (serverid == obj.serverId) {
+ const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 1, connectType: connectType, serverid: obj.serverId };
+ if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
+ obj.db.Set(lc);
+ }
+
+ // Notify any users of device connection
+ obj.NotifyUserOfDeviceStateChange(meshid, nodeid, connectTime, connectType, powerState, serverid, true, extraInfo);
+ }
+ }
+ };
+
+ // Clear the connectivity state of a node and setup the server so that messages can be routed correctly.
+ // meshId: mesh identifier of format mesh/domain/meshidhex
+ // nodeId: node identifier of format node/domain/nodeidhex
+ // connectType: Bitmask, 1 = MeshAgent, 2 = Intel AMT CIRA, 4 = Intel AMT local.
+ obj.ClearConnectivityState = function (meshid, nodeid, connectType, serverid, extraInfo) {
+ //console.log('ClearConnectivity for ' + nodeid.substring(0, 16) + ', Type: ' + connectTypeStrings[connectType] + (serverid == null?(''):(', ServerId: ' + serverid)));
+ if ((serverid == null) && (obj.multiServer != null)) { obj.multiServer.DispatchMessage({ action: 'ClearConnectivityState', meshid: meshid, nodeid: nodeid, connectType: connectType, extraInfo: extraInfo }); }
+
+ if (obj.multiServer == null) {
+ // Single server mode
+ var eventConnectChange = 0;
+
+ // Remove the agent connection from the nodes connection list
+ const state = obj.connectivityByNode[nodeid];
+ if (state == null) return;
+
+ if ((state.connectivity & connectType) != 0) {
+ state.connectivity -= connectType;
+
+ // Save indication of node connection change
+ const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 0, connectType: connectType };
+ if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
+ obj.db.Set(lc);
+
+ // If the node is completely disconnected, clean it up completely
+ if (state.connectivity == 0) { delete obj.connectivityByNode[nodeid]; }
+ eventConnectChange = 1;
+ }
+
+ // Clear node power state
+ var powerState = 0;
+ const oldPowerState = state.powerState;
+ if (connectType == 1) { state.agentPower = 0; } else if (connectType == 2) { state.ciraPower = 0; } else if (connectType == 4) { state.amtPower = 0; }
+ if ((state.connectivity & 1) != 0) { powerState = state.agentPower; } else if ((state.connectivity & 2) != 0) { powerState = state.ciraPower; } else if ((state.connectivity & 4) != 0) { powerState = state.amtPower; }
+ if ((state.powerState == null) || (state.powerState != powerState)) {
+ state.powerState = powerState;
+ eventConnectChange = 1;
+
+ // Set new power state in database
+ obj.db.storePowerEvent({ time: new Date(), nodeid: nodeid, power: powerState, oldPower: oldPowerState }, obj.multiServer);
+ }
+
+ // Event the node connection change
+ if (eventConnectChange == 1) {
+ obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: state.connectivity, pwr: state.powerState, nolog: 1, nopeers: 1, id: Math.random() });
+
+ // Notify any users of device disconnection
+ obj.NotifyUserOfDeviceStateChange(meshid, nodeid, Date.now(), connectType, -1, serverid, false, extraInfo);
+ }
+ } else {
+ // Multi server mode
+
+ // Remove the agent connection from the nodes connection list
+ if (serverid == null) { serverid = obj.serverId; }
+ if (obj.peerConnectivityByNode[serverid] == null) return; // Guard against unknown serverid's
+ var state = obj.peerConnectivityByNode[serverid][nodeid];
+ if (state == null) return;
+
+ // If existing state exist, remove this connection
+ if ((state.connectivity & connectType) != 0) {
+ state.connectivity -= connectType; // Remove one connectivity mode
+
+ // Save indication of node connection change
+ if (serverid == obj.serverId) {
+ const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 0, connectType: connectType, serverid: obj.serverId };
+ if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
+ obj.db.Set(lc);
+ }
+
+ // If the node is completely disconnected, clean it up completely
+ if (state.connectivity == 0) { delete obj.peerConnectivityByNode[serverid][nodeid]; state.powerState = 0; }
+
+ // Notify any users of device disconnection
+ obj.NotifyUserOfDeviceStateChange(meshid, nodeid, Date.now(), connectType, -1, serverid, false, extraInfo);
+ }
+
+ // Clear node power state
+ if (connectType == 1) { state.agentPower = 0; } else if (connectType == 2) { state.ciraPower = 0; } else if (connectType == 4) { state.amtPower = 0; }
+ var powerState = 0;
+ if ((state.connectivity & 1) != 0) { powerState = state.agentPower; } else if ((state.connectivity & 2) != 0) { powerState = state.ciraPower; } else if ((state.connectivity & 4) != 0) { powerState = state.amtPower; }
+ if ((state.powerState == null) || (state.powerState != powerState)) { state.powerState = powerState; }
+
+ // Update the combined node state
+ var x = {}; x[nodeid] = 1;
+ obj.UpdateConnectivityState(x);
+ }
+ };
+
+ // Escape a code string
+ obj.escapeCodeString = function (str, keepUtf8) {
+ const escapeCodeStringTable = { '\'': '\\\'', '\"': '\\"', '\\': '\\\\', '\b': '\\b', '\f': '\\f', '\n': '\\n', '\r': '\\r', '\t': '\\t' };
+ var r = '', c, cr, table;
+ for (var i = 0; i < str.length; i++) {
+ c = str[i];
+ table = escapeCodeStringTable[c];
+ if (table != null) {
+ r += table;
+ } else if (keepUtf8 === true) {
+ r += c;
+ } else {
+ cr = c.charCodeAt(0);
+ if ((cr >= 32) && (cr <= 127)) { r += c; }
+ }
+ }
+ return r;
+ }
+
+ // Update the default mesh core
+ obj.updateMeshCore = function (func, dumpToFile) {
+ // Figure out where meshcore.js is
+ var meshcorePath = obj.datapath;
+ if (obj.fs.existsSync(obj.path.join(meshcorePath, 'meshcore.js')) == false) {
+ meshcorePath = obj.path.join(__dirname, 'agents');
+ if (obj.fs.existsSync(obj.path.join(meshcorePath, 'meshcore.js')) == false) {
+ obj.defaultMeshCores = obj.defaultMeshCoresHash = {}; if (func != null) { func(false); } // meshcore.js not found
+ }
+ }
+
+ // Read meshcore.js and all .js files in the modules folder.
+ var meshCore = null, modulesDir = null;
+ const modulesAdd = {
+ 'windows-amt': ['var addedModules = [];\r\n'],
+ 'linux-amt': ['var addedModules = [];\r\n'],
+ 'linux-noamt': ['var addedModules = [];\r\n']
+ };
+
+ // Read the recovery core if present
+ var meshRecoveryCore = null;
+ if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'recoverycore.js')) == true) {
+ try { meshRecoveryCore = obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'recoverycore.js')).toString(); } catch (ex) { }
+ if (meshRecoveryCore != null) {
+ modulesAdd['windows-recovery'] = ['var addedModules = [];\r\n'];
+ modulesAdd['linux-recovery'] = ['var addedModules = [];\r\n'];
+ }
+ }
+
+ // Read the agent recovery core if present
+ var meshAgentRecoveryCore = null;
+ if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'meshcore_diagnostic.js')) == true) {
+ try { meshAgentRecoveryCore = obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'meshcore_diagnostic.js')).toString(); } catch (ex) { }
+ if (meshAgentRecoveryCore != null) {
+ modulesAdd['windows-agentrecovery'] = ['var addedModules = [];\r\n'];
+ modulesAdd['linux-agentrecovery'] = ['var addedModules = [];\r\n'];
+ }
+ }
+
+ // Read the tiny core if present
+ var meshTinyCore = null;
+ if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'tinycore.js')) == true) {
+ try { meshTinyCore = obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'tinycore.js')).toString(); } catch (ex) { }
+ if (meshTinyCore != null) {
+ modulesAdd['windows-tiny'] = ['var addedModules = [];\r\n'];
+ modulesAdd['linux-tiny'] = ['var addedModules = [];\r\n'];
+ }
+ }
+
+ if (obj.args.minifycore !== false) { try { meshCore = obj.fs.readFileSync(obj.path.join(meshcorePath, 'meshcore.min.js')).toString(); } catch (ex) { } } // Favor minified meshcore if present.
+ if (meshCore == null) { try { meshCore = obj.fs.readFileSync(obj.path.join(meshcorePath, 'meshcore.js')).toString(); } catch (ex) { } } // Use non-minified meshcore.
+ if (meshCore != null) {
+ var moduleDirPath = null;
+ if (obj.args.minifycore !== false) { try { moduleDirPath = obj.path.join(meshcorePath, 'modules_meshcore_min'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Favor minified modules if present.
+ if (modulesDir == null) { try { moduleDirPath = obj.path.join(meshcorePath, 'modules_meshcore'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Use non-minified mofules.
+ if (modulesDir != null) {
+ for (var i in modulesDir) {
+ if (modulesDir[i].toLowerCase().endsWith('.json')) {
+ // We are adding a JSON file to the meshcores
+ var moduleName = modulesDir[i].substring(0, modulesDir[i].length - 5);
+ if (moduleName.endsWith('.min')) { moduleName = moduleName.substring(0, moduleName.length - 6); } // Remove the ".min" for ".min.json" files.
+ const jsonData = obj.escapeCodeString(obj.fs.readFileSync(obj.path.join(moduleDirPath, modulesDir[i])).toString('utf8'), true);
+ const moduleData = ['var ', moduleName, ' = JSON.parse(\'', jsonData, '\');\r\n'];
+
+ // Add to all major cores
+ modulesAdd['windows-amt'].push(...moduleData);
+ modulesAdd['linux-amt'].push(...moduleData);
+ modulesAdd['linux-noamt'].push(...moduleData);
+ }
+ if (modulesDir[i].toLowerCase().endsWith('.js')) {
+ // We are adding a JS file to the meshcores
+ var moduleName = modulesDir[i].substring(0, modulesDir[i].length - 3);
+ if (moduleName.endsWith('.min')) { moduleName = moduleName.substring(0, moduleName.length - 4); } // Remove the ".min" for ".min.js" files.
+ const moduleData = ['try { addModule("', moduleName, '", "', obj.escapeCodeString(obj.fs.readFileSync(obj.path.join(moduleDirPath, modulesDir[i])).toString('binary')), '"); addedModules.push("', moduleName, '"); } catch (ex) { }\r\n'];
+
+ // Merge this module
+ // NOTE: "smbios" module makes some non-AI Linux segfault, only include for IA platforms.
+ if (moduleName.startsWith('amt-') || (moduleName == 'smbios')) {
+ // Add to IA / Intel AMT cores only
+ modulesAdd['windows-amt'].push(...moduleData);
+ modulesAdd['linux-amt'].push(...moduleData);
+ } else if (moduleName.startsWith('win-')) {
+ // Add to Windows cores only
+ modulesAdd['windows-amt'].push(...moduleData);
+ } else if (moduleName.startsWith('linux-')) {
+ // Add to Linux cores only
+ modulesAdd['linux-amt'].push(...moduleData);
+ modulesAdd['linux-noamt'].push(...moduleData);
+ } else {
+ // Add to all cores
+ modulesAdd['windows-amt'].push(...moduleData);
+ modulesAdd['linux-amt'].push(...moduleData);
+ modulesAdd['linux-noamt'].push(...moduleData);
+ }
+
+ // Merge this module to recovery modules if needed
+ if (modulesAdd['windows-recovery'] != null) {
+ if ((moduleName == 'win-console') || (moduleName == 'win-message-pump') || (moduleName == 'win-terminal') || (moduleName == 'win-virtual-terminal')) {
+ modulesAdd['windows-recovery'].push(...moduleData);
+ }
+ }
+
+ // Merge this module to agent recovery modules if needed
+ if (modulesAdd['windows-agentrecovery'] != null) {
+ if ((moduleName == 'win-console') || (moduleName == 'win-message-pump') || (moduleName == 'win-terminal') || (moduleName == 'win-virtual-terminal')) {
+ modulesAdd['windows-agentrecovery'].push(...moduleData);
+ }
+ }
+ }
+ }
+ }
+
+ // Add plugins to cores
+ if (obj.pluginHandler) { obj.pluginHandler.addMeshCoreModules(modulesAdd); }
+
+ // If we need to dump modules to file, create a meshcores folder
+ if (dumpToFile) { try { obj.fs.mkdirSync('meshcores'); } catch (ex) { } }
+
+ // Merge the cores and compute the hashes
+ for (var i in modulesAdd) {
+ if ((i == 'windows-recovery') || (i == 'linux-recovery')) {
+ obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshRecoveryCore].join('');
+ } else if ((i == 'windows-agentrecovery') || (i == 'linux-agentrecovery')) {
+ obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshAgentRecoveryCore].join('');
+ } else if ((i == 'windows-tiny') || (i == 'linux-tiny')) {
+ obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshTinyCore].join('');
+ } else {
+ obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshCore].join('');
+ }
+ obj.defaultMeshCores[i] = Buffer.from(obj.defaultMeshCores[i], 'utf8');
+ obj.defaultMeshCoresHash[i] = obj.crypto.createHash('sha384').update(obj.defaultMeshCores[i]).digest('binary');
+ obj.debug('main', 'Core module ' + i + ' is ' + obj.defaultMeshCores[i].length + ' bytes.');
+
+ // Write all modules to files. Great for debugging.
+ if (dumpToFile) {
+ console.log('Core module ' + i + ' is ' + obj.defaultMeshCores[i].length + ' bytes, saving to meshcores/' + i + '.js.'); // Print the core size and filename
+ obj.fs.writeFile('meshcores/' + i + '.js', obj.defaultMeshCores[i].slice(4), function () { }); // Write the core to file
+ }
+
+ // Compress the mesh cores with DEFLATE
+ const callback = function MeshCoreDeflateCb(err, buffer) { if (err == null) { obj.defaultMeshCoresDeflate[MeshCoreDeflateCb.i] = buffer; } }
+ callback.i = i;
+ require('zlib').deflate(obj.defaultMeshCores[i], { level: require('zlib').Z_BEST_COMPRESSION }, callback);
+ }
+ }
+
+ // We are done creating all the mesh cores.
+ if (func != null) { func(true); }
+ };
+
+ // Update the default meshcmd
+ obj.updateMeshCmdTimer = 'notset';
+ obj.updateMeshCmd = function (func) {
+ // Figure out where meshcmd.js is and read it.
+ var meshCmd = null, meshcmdPath, moduleAdditions = ['var addedModules = [];\r\n'], moduleDirPath, modulesDir = null;
+ if ((obj.args.minifycore !== false) && (obj.fs.existsSync(obj.path.join(obj.datapath, 'meshcmd.min.js')))) { meshcmdPath = obj.path.join(obj.datapath, 'meshcmd.min.js'); meshCmd = obj.fs.readFileSync(meshcmdPath).toString(); }
+ else if (obj.fs.existsSync(obj.path.join(obj.datapath, 'meshcmd.js'))) { meshcmdPath = obj.path.join(obj.datapath, 'meshcmd.js'); meshCmd = obj.fs.readFileSync(meshcmdPath).toString(); }
+ else if ((obj.args.minifycore !== false) && (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'meshcmd.min.js')))) { meshcmdPath = obj.path.join(__dirname, 'agents', 'meshcmd.min.js'); meshCmd = obj.fs.readFileSync(meshcmdPath).toString(); }
+ else if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'meshcmd.js'))) { meshcmdPath = obj.path.join(__dirname, 'agents', 'meshcmd.js'); meshCmd = obj.fs.readFileSync(meshcmdPath).toString(); }
+ else { obj.defaultMeshCmd = null; if (func != null) { func(false); } return; } // meshcmd.js not found
+ meshCmd = meshCmd.replace("'***Mesh*Cmd*Version***'", '\'' + getCurrentVersion() + '\'');
+
+ // Figure out where the modules_meshcmd folder is.
+ if (obj.args.minifycore !== false) { try { moduleDirPath = obj.path.join(meshcmdPath, 'modules_meshcmd_min'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Favor minified modules if present.
+ if (modulesDir == null) { try { moduleDirPath = obj.path.join(meshcmdPath, 'modules_meshcmd'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Use non-minified mofules.
+ if (obj.args.minifycore !== false) { if (modulesDir == null) { try { moduleDirPath = obj.path.join(__dirname, 'agents', 'modules_meshcmd_min'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } } // Favor minified modules if present.
+ if (modulesDir == null) { try { moduleDirPath = obj.path.join(__dirname, 'agents', 'modules_meshcmd'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Use non-minified mofules.
+
+ // Read all .js files in the meshcmd modules folder.
+ if (modulesDir != null) {
+ for (var i in modulesDir) {
+ if (modulesDir[i].toLowerCase().endsWith('.js')) {
+ // Merge this module
+ var moduleName = modulesDir[i].substring(0, modulesDir[i].length - 3);
+ if (moduleName.endsWith('.min')) { moduleName = moduleName.substring(0, moduleName.length - 4); } // Remove the ".min" for ".min.js" files.
+ moduleAdditions.push('try { addModule("', moduleName, '", "', obj.escapeCodeString(obj.fs.readFileSync(obj.path.join(moduleDirPath, modulesDir[i])).toString('binary')), '"); addedModules.push("', moduleName, '"); } catch (ex) { }\r\n');
+ }
+ }
+ }
+
+ // Set the new default meshcmd.js
+ moduleAdditions.push(meshCmd);
+ obj.defaultMeshCmd = moduleAdditions.join('');
+ //console.log('MeshCmd is ' + obj.defaultMeshCmd.length + ' bytes.'); // DEBUG, Print the merged meshcmd.js size
+ //obj.fs.writeFile("C:\\temp\\meshcmd.js", obj.defaultMeshCmd.substring(4)); // DEBUG, Write merged meshcmd.js to file
+ if (func != null) { func(true); }
+
+ // Monitor for changes in meshcmd.js
+ if (obj.updateMeshCmdTimer === 'notset') {
+ obj.updateMeshCmdTimer = null;
+ obj.fs.watch(meshcmdPath, function (eventType, filename) {
+ if (obj.updateMeshCmdTimer != null) { clearTimeout(obj.updateMeshCmdTimer); obj.updateMeshCmdTimer = null; }
+ obj.updateMeshCmdTimer = setTimeout(function () { obj.updateMeshCmd(); }, 5000);
+ });
+ }
+ };
+
+ // List of possible mesh agent install scripts
+ const meshToolsList = {
+ 'MeshCentralRouter': { localname: 'MeshCentralRouter.exe', dlname: 'winrouter' },
+ 'MeshCentralAssistant': { localname: 'MeshCentralAssistant.exe', dlname: 'winassistant', winhash: true }
+ //'MeshCentralRouterMacOS': { localname: 'MeshCentralRouter.dmg', dlname: 'MeshCentralRouter.dmg' }
+ };
+
+ // Update the list of available mesh agents
+ obj.updateMeshTools = function () {
+ for (var toolname in meshToolsList) {
+ if (meshToolsList[toolname].winhash === true) {
+ var toolpath = obj.path.join(__dirname, 'agents', meshToolsList[toolname].localname);
+ const toolpath2 = obj.path.join(obj.datapath, 'agents', meshToolsList[toolname].localname);
+ if (obj.fs.existsSync(toolpath2)) { toolpath = toolpath2; } // If the tool is present in "meshcentral-data/agents", use that one instead.
+
+ var hashStream = obj.crypto.createHash('sha384');
+ hashStream.toolname = toolname;
+ hashStream.toolpath = toolpath;
+ hashStream.dlname = meshToolsList[toolname].dlname;
+ hashStream.hashx = 0;
+ hashStream.on('data', function (data) {
+ obj.meshToolsBinaries[this.toolname] = { hash: data.toString('hex'), hashx: this.hashx, path: this.toolpath, dlname: this.dlname, url: this.url };
+ obj.meshToolsBinaries[this.toolname].url = 'https://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?meshaction=' + this.dlname;
+ var stats = null;
+ try { stats = obj.fs.statSync(this.toolpath); } catch (ex) { }
+ if (stats != null) { obj.meshToolsBinaries[this.toolname].size = stats.size; }
+ });
+ const options = { sourcePath: toolpath, targetStream: hashStream };
+ obj.exeHandler.hashExecutableFile(options);
+ } else {
+ var toolpath = obj.path.join(__dirname, 'agents', meshToolsList[toolname].localname);
+ const toolpath2 = obj.path.join(obj.datapath, 'agents', meshToolsList[toolname].localname);
+ if (obj.fs.existsSync(toolpath2)) { toolpath = toolpath2; } // If the tool is present in "meshcentral-data/agents", use that one instead.
+
+ var stream = null;
+ try {
+ stream = obj.fs.createReadStream(toolpath);
+ stream.on('data', function (data) { this.hash.update(data, 'binary'); this.hashx += data.length; });
+ stream.on('error', function (data) {
+ // If there is an error reading this file, make sure this agent is not in the agent table
+ if (obj.meshToolsBinaries[this.toolname] != null) { delete obj.meshToolsBinaries[this.toolname]; }
+ });
+ stream.on('end', function () {
+ // Add the agent to the agent table with all information and the hash
+ obj.meshToolsBinaries[this.toolname] = {};
+ obj.meshToolsBinaries[this.toolname].hash = this.hash.digest('hex');
+ obj.meshToolsBinaries[this.toolname].hashx = this.hashx;
+ obj.meshToolsBinaries[this.toolname].path = this.agentpath;
+ obj.meshToolsBinaries[this.toolname].dlname = this.dlname;
+ obj.meshToolsBinaries[this.toolname].url = 'https://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?meshaction=' + this.dlname;
+ var stats = null;
+ try { stats = obj.fs.statSync(this.agentpath); } catch (ex) { }
+ if (stats != null) { obj.meshToolsBinaries[this.toolname].size = stats.size; }
+ });
+ stream.toolname = toolname;
+ stream.agentpath = toolpath;
+ stream.dlname = meshToolsList[toolname].dlname;
+ stream.hash = obj.crypto.createHash('sha384', stream);
+ stream.hashx = 0;
+ } catch (ex) { }
+ }
+ }
+ };
+
+ // List of possible mesh agent install scripts
+ const meshAgentsInstallScriptList = {
+ 1: { id: 1, localname: 'meshinstall-linux.sh', rname: 'meshinstall.sh', linux: true },
+ 2: { id: 2, localname: 'meshinstall-initd.sh', rname: 'meshagent', linux: true },
+ 5: { id: 5, localname: 'meshinstall-bsd-rcd.sh', rname: 'meshagent', linux: true },
+ 6: { id: 6, localname: 'meshinstall-linux.js', rname: 'meshinstall.js', linux: true }
+ };
+
+ // Update the list of available mesh agents
+ obj.updateMeshAgentInstallScripts = function () {
+ for (var scriptid in meshAgentsInstallScriptList) {
+ var scriptpath = obj.path.join(__dirname, 'agents', meshAgentsInstallScriptList[scriptid].localname);
+ var stream = null;
+ try {
+ stream = obj.fs.createReadStream(scriptpath);
+ stream.xdata = '';
+ stream.on('data', function (data) { this.hash.update(data, 'binary'); this.xdata += data; });
+ stream.on('error', function (data) {
+ // If there is an error reading this file, make sure this agent is not in the agent table
+ if (obj.meshAgentInstallScripts[this.info.id] != null) { delete obj.meshAgentInstallScripts[this.info.id]; }
+ });
+ stream.on('end', function () {
+ // Add the agent to the agent table with all information and the hash
+ obj.meshAgentInstallScripts[this.info.id] = Object.assign({}, this.info);
+ obj.meshAgentInstallScripts[this.info.id].hash = this.hash.digest('hex');
+ obj.meshAgentInstallScripts[this.info.id].path = this.agentpath;
+ obj.meshAgentInstallScripts[this.info.id].data = this.xdata;
+ obj.meshAgentInstallScripts[this.info.id].url = 'https://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?script=' + this.info.id;
+ var stats = null;
+ try { stats = obj.fs.statSync(this.agentpath); } catch (ex) { }
+ if (stats != null) { obj.meshAgentInstallScripts[this.info.id].size = stats.size; }
+
+ // Place Unit line breaks on Linux scripts if not already present.
+ if (obj.meshAgentInstallScripts[this.info.id].linux === true) { obj.meshAgentInstallScripts[this.info.id].data = obj.meshAgentInstallScripts[this.info.id].data.split('\r\n').join('\n') }
+ });
+ stream.info = meshAgentsInstallScriptList[scriptid];
+ stream.agentpath = scriptpath;
+ stream.hash = obj.crypto.createHash('sha384', stream);
+ } catch (ex) { }
+ }
+ };
+
+ // List of possible mesh agents
+ obj.meshAgentsArchitectureNumbers = {
+ 0: { id: 0, localname: 'Unknown', rname: 'meshconsole.exe', desc: 'Unknown agent', update: false, amt: true, platform: 'unknown', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 1: { id: 1, localname: 'MeshConsole.exe', rname: 'meshconsole32.exe', desc: 'Windows x86-32 console', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
+ 2: { id: 2, localname: 'MeshConsole64.exe', rname: 'meshconsole64.exe', desc: 'Windows x86-64 console', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
+ 3: { id: 3, localname: 'MeshService.exe', rname: 'meshagent32.exe', desc: 'Windows x86-32 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', codesign: true },
+ 4: { id: 4, localname: 'MeshService64.exe', rname: 'meshagent64.exe', desc: 'Windows x86-64 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', codesign: true },
+ 5: { id: 5, localname: 'meshagent_x86', rname: 'meshagent', desc: 'Linux x86-32', update: true, amt: true, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 6: { id: 6, localname: 'meshagent_x86-64', rname: 'meshagent', desc: 'Linux x86-64', update: true, amt: true, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 7: { id: 7, localname: 'meshagent_mips', rname: 'meshagent', desc: 'Linux MIPS', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 8: { id: 8, localname: 'MeshAgent-Linux-XEN-x86-32', rname: 'meshagent', desc: 'XEN x86-64', update: true, amt: false, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 9: { id: 9, localname: 'meshagent_arm', rname: 'meshagent', desc: 'Linux ARM5', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 10: { id: 10, localname: 'MeshAgent-Linux-ARM-PlugPC', rname: 'meshagent', desc: 'Linux ARM PlugPC', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 11: { id: 11, localname: 'meshagent_osx-x86-32', rname: 'meshosx', desc: 'Apple macOS x86-32', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Apple x86-32 binary, no longer supported.
+ 12: { id: 12, localname: 'MeshAgent-Android-x86', rname: 'meshandroid', desc: 'Android x86-32', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 13: { id: 13, localname: 'meshagent_pogo', rname: 'meshagent', desc: 'Linux ARM PogoPlug', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 14: { id: 14, localname: 'meshagent_android.apk', rname: 'meshandroid.apk', desc: 'Android', update: false, amt: false, platform: 'android', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Get this one from Google Play
+ 15: { id: 15, localname: 'meshagent_poky', rname: 'meshagent', desc: 'Linux Poky x86-32', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 16: { id: 16, localname: 'meshagent_osx-x86-64', rname: 'meshagent', desc: 'Apple macOS x86-64', update: true, amt: false, platform: 'osx', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Apple x86-64 binary
+ 17: { id: 17, localname: 'MeshAgent-ChromeOS', rname: 'meshagent', desc: 'Google ChromeOS', update: false, amt: false, platform: 'chromeos', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Get this one from Chrome store
+ 18: { id: 18, localname: 'meshagent_poky64', rname: 'meshagent', desc: 'Linux Poky x86-64', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 19: { id: 19, localname: 'meshagent_x86_nokvm', rname: 'meshagent', desc: 'Linux x86-32 NoKVM', update: true, amt: true, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 20: { id: 20, localname: 'meshagent_x86-64_nokvm', rname: 'meshagent', desc: 'Linux x86-64 NoKVM', update: true, amt: true, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 21: { id: 21, localname: 'MeshAgent-WinMinCore-Console-x86-32.exe', rname: 'meshagent.exe', desc: 'Windows MinCore Console x86-32', update: true, amt: false, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
+ 22: { id: 22, localname: 'MeshAgent-WinMinCore-Service-x86-64.exe', rname: 'meshagent.exe', desc: 'Windows MinCore Service x86-32', update: true, amt: false, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
+ 23: { id: 23, localname: 'MeshAgent-NodeJS', rname: 'meshagent', desc: 'NodeJS', update: false, amt: false, platform: 'node', core: 'nodejs', rcore: 'nodejs', arcore: 'nodejs', tcore: 'nodejs' }, // NodeJS based agent
+ 24: { id: 24, localname: 'meshagent_arm-linaro', rname: 'meshagent', desc: 'Linux ARM Linaro', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 25: { id: 25, localname: 'meshagent_armhf', rname: 'meshagent', desc: 'Linux ARM - HardFloat', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // "armv6l" and "armv7l"
+ 26: { id: 26, localname: 'meshagent_aarch64', rname: 'meshagent', desc: 'Linux ARM 64 bit (glibc/2.24)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // This is replaced by ARCHID 32
+ 27: { id: 27, localname: 'meshagent_armhf2', rname: 'meshagent', desc: 'Linux ARM - HardFloat', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Raspbian 7 2015-02-02 for old Raspberry Pi.
+ 28: { id: 28, localname: 'meshagent_mips24kc', rname: 'meshagent', desc: 'Linux MIPS24KC/MUSL (OpenWRT)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // MIPS Router with OpenWRT
+ 29: { id: 29, localname: 'meshagent_osx-arm-64', rname: 'meshagent', desc: 'Apple macOS ARM-64', update: true, amt: false, platform: 'osx', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Apple Silicon ARM 64bit
+ 30: { id: 30, localname: 'meshagent_freebsd_x86-64', rname: 'meshagent', desc: 'FreeBSD x86-64', update: true, amt: false, platform: 'freebsd', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // FreeBSD x64
+ 32: { id: 32, localname: 'meshagent_aarch64', rname: 'meshagent', desc: 'Linux ARM 64 bit (glibc/2.24)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 33: { id: 33, localname: 'meshagent_openwrt_x86_64', rname: 'meshagent', desc: 'OpenWRT x86-64', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // This is replaced with ARCHID 36.
+ 34: { id: 34, localname: 'assistant_windows', rname: 'meshassistant', desc: 'MeshCentral Assistant (Windows)', update: false, amt: false, platform: 'assistant', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // MeshCentral Assistant for Windows
+ 35: { id: 35, localname: 'meshagent_linux-armada370-hf', rname: 'meshagent', desc: 'Armada370 - ARM32/HF (libc/2.26)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Armada370
+ 36: { id: 36, localname: 'meshagent_openwrt_x86_64', rname: 'meshagent', desc: 'OpenWRT x86-64', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // OpenWRT x86-64
+ 37: { id: 37, localname: 'meshagent_openbsd_x86-64', rname: 'meshagent', desc: 'OpenBSD x86-64', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // OpenBSD x86-64
+ 40: { id: 40, localname: 'meshagent_mipsel24kc', rname: 'meshagent', desc: 'Linux MIPSEL24KC (OpenWRT)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // MIPS Router with OpenWRT
+ 41: { id: 41, localname: 'meshagent_aarch64-cortex-a53', rname: 'meshagent', desc: 'ARMADA/CORTEX-A53/MUSL (OpenWRT)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // OpenWRT Routers
+ 42: { id: 42, localname: 'MeshConsoleARM64.exe', rname: 'meshconsolearm64.exe', desc: 'Windows ARM-64 console', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
+ 43: { id: 43, localname: 'MeshServiceARM64.exe', rname: 'meshagentarm64.exe', desc: 'Windows ARM-64 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', codesign: true },
+ // 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)
+ 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
+ 10003: { id: 10003, localname: 'MeshService.exe', rname: 'meshagent32.exe', desc: 'Windows x86-32 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', unsigned: true },
+ 10004: { id: 10004, localname: 'MeshService64.exe', rname: 'meshagent64.exe', desc: 'Windows x86-64 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', unsigned: true },
+ 10005: { id: 10005, localname: 'meshagent_osx-universal-64', rname: 'meshagent', desc: 'Apple macOS Universal Binary', update: true, amt: false, platform: 'osx', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Apple Silicon + x86 universal binary
+ 10006: { id: 10006, localname: 'MeshCentralAssistant.exe', rname: 'MeshCentralAssistant.exe', desc: 'MeshCentral Assistant for Windows', update: false, amt: false, platform: 'win32' }, // MeshCentral Assistant
+ 11000: { id: 11000, localname: 'MeshCmd.exe', rname: 'MeshCmd.exe', desc: 'Windows x86-32 meshcmd', update: false, amt: true, platform: 'win32', codesign: true }, // MeshCMD for Windows x86 32-bit
+ 11001: { id: 11001, localname: 'MeshCmd64.exe', rname: 'MeshCmd64.exe', desc: 'Windows x86-64 meshcmd', update: false, amt: true, platform: 'win32', codesign: true }, // MeshCMD for Windows x86 64-bit
+ 11002: { id: 11002, localname: 'MeshCmdARM64.exe', rname: 'MeshCmdARM64.exe', desc: 'Windows ARM-64 meshcmd', update: false, amt: true, platform: 'win32', codesign: true } // MeshCMD for Windows ARM 64-bit
+ };
+
+ // Sign windows agents
+ obj.signMeshAgents = function (domain, func) {
+ // Setup the domain is specified
+ var objx = domain, suffix = '';
+ if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
+
+ // Check if a custom agent signing certificate is available
+ var agentSignCertInfo = require('./authenticode.js').loadCertificates([obj.path.join(obj.datapath, 'agentsigningcert.pem')]);
+
+ // If not using a custom signing cert, get agent code signature certificate ready with the full cert chain
+ if ((agentSignCertInfo == null) && (obj.certificates.codesign != null)) {
+ agentSignCertInfo = {
+ cert: obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.codesign.cert),
+ key: obj.certificateOperations.forge.pki.privateKeyFromPem(obj.certificates.codesign.key),
+ extraCerts: [obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.root.cert)]
+ }
+ }
+ if (agentSignCertInfo == null) { func(); return; } // No code signing certificate, nothing to do.
+
+ // Setup the domain is specified
+ var objx = domain, suffix = '';
+ if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
+
+ // Generate the agent signature description and URL
+ const serverSignedAgentsPath = obj.path.join(obj.datapath, 'signedagents' + suffix);
+ const signDesc = (domain.title ? domain.title : agentSignCertInfo.cert.subject.hash);
+ const httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
+ var httpsHost = ((domain.dns != null) ? domain.dns : obj.certificates.CommonName);
+ if (obj.args.agentaliasdns != null) { httpsHost = obj.args.agentaliasdns; }
+ var signUrl = 'https://' + httpsHost;
+ if (httpsPort != 443) { signUrl += ':' + httpsPort; }
+ var xdomain = (domain.dns == null) ? domain.id : '';
+ if (xdomain != '') xdomain += '/';
+ signUrl += '/' + xdomain;
+
+ // If requested, lock the agent to this server
+ if (obj.config.settings.agentsignlock) { signUrl += '?ServerID=' + obj.certificateOperations.getPublicKeyHash(obj.certificates.agent.cert).toUpperCase(); }
+
+ // Setup the time server
+ var timeStampUrl = 'http://timestamp.comodoca.com/authenticode';
+ if (obj.args.agenttimestampserver === false) { timeStampUrl = null; }
+ else if (typeof obj.args.agenttimestampserver == 'string') { timeStampUrl = obj.args.agenttimestampserver; }
+
+ // Setup the time server proxy
+ var timeStampProxy = null;
+ if (typeof obj.args.agenttimestampproxy == 'string') { timeStampProxy = obj.args.agenttimestampproxy; }
+ else if ((obj.args.agenttimestampproxy !== false) && (typeof obj.args.npmproxy == 'string')) { timeStampProxy = obj.args.npmproxy; }
+
+ // Setup the pending operations counter
+ var pendingOperations = 1;
+
+ for (var archid in obj.meshAgentsArchitectureNumbers) {
+ if (obj.meshAgentsArchitectureNumbers[archid].codesign !== true) continue;
+
+ var agentpath;
+ if (domain.id == '') {
+ // Load all agents when processing the default domain
+ agentpath = obj.path.join(__dirname, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
+ var agentpath2 = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
+ if (obj.fs.existsSync(agentpath2)) { agentpath = agentpath2; delete obj.meshAgentsArchitectureNumbers[archid].codesign; } // If the agent is present in "meshcentral-data/agents", use that one instead.
+ } else {
+ // When processing an extra domain, only load agents that are specific to that domain
+ agentpath = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
+ if (obj.fs.existsSync(agentpath)) { delete obj.meshAgentsArchitectureNumbers[archid].codesign; } else { continue; } // If the agent is not present in "meshcentral-data/agents" skip.
+ }
+
+ // Open the original agent with authenticode
+ const signeedagentpath = obj.path.join(serverSignedAgentsPath, obj.meshAgentsArchitectureNumbers[archid].localname);
+ const originalAgent = require('./authenticode.js').createAuthenticodeHandler(agentpath);
+ if (originalAgent != null) {
+ // Check if the agent is already signed correctly
+ const destinationAgent = require('./authenticode.js').createAuthenticodeHandler(signeedagentpath);
+ var destinationAgentOk = (
+ (destinationAgent != null) &&
+ (destinationAgent.fileHashSigned != null) &&
+ (Buffer.compare(destinationAgent.fileHashSigned, destinationAgent.fileHashActual) == 0) &&
+ (destinationAgent.signingAttribs.indexOf(signUrl) >= 0) &&
+ (destinationAgent.signingAttribs.indexOf(signDesc) >= 0)
+ );
+
+ if (destinationAgent != null) {
+ // If the agent is signed correctly, look to see if the resources in the destination agent are correct
+ var orgVersionStrings = originalAgent.getVersionInfo();
+ if (destinationAgentOk == true) {
+ const versionStrings = destinationAgent.getVersionInfo();
+ const versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
+ for (var i in versionProperties) {
+ const prop = versionProperties[i], propl = prop.toLowerCase();
+ if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo[propl] == 'string')) {
+ if (domain.agentfileinfo[propl] != versionStrings[prop]) { destinationAgentOk = false; break; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
+ } else {
+ if (orgVersionStrings[prop] != versionStrings[prop]) { destinationAgentOk = false; break; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
+ }
+ }
+
+ // Check file version number
+ if (destinationAgentOk == true) {
+ if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo['fileversionnumber'] == 'string')) {
+ if (domain.agentfileinfo['fileversionnumber'] != versionStrings['~FileVersion']) { destinationAgentOk = false; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
+ } else {
+ if (orgVersionStrings['~FileVersion'] != versionStrings['~FileVersion']) { destinationAgentOk = false; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
+ }
+ }
+
+ // Check product version number
+ if (destinationAgentOk == true) {
+ if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo['productversionnumber'] == 'string')) {
+ if (domain.agentfileinfo['productversionnumber'] != versionStrings['~ProductVersion']) { destinationAgentOk = false; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
+ } else {
+ if (orgVersionStrings['~ProductVersion'] != versionStrings['~ProductVersion']) { destinationAgentOk = false; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
+ }
+ }
+
+ // Check the agent icon
+ if (destinationAgentOk == true) {
+ if ((domain.agentfileinfo != null) && (domain.agentfileinfo.icon != null)) {
+ // Check if the destination agent matches the icon we want
+ const agentIconGroups = destinationAgent.getIconInfo();
+ if (agentIconGroups != null) {
+ const agentIconGroupNames = Object.keys(agentIconGroups);
+ if (agentIconGroupNames.length > 0) {
+ const agentMainIconGroup = agentIconGroups[agentIconGroupNames[0]];
+ if (agentMainIconGroup.resCount != domain.agentfileinfo.icon.resCount) {
+ destinationAgentOk = false; // The icon image count is different, don't bother hashing to see if the icons are different.
+ } else {
+ const agentMainIconGroupHash = require('./authenticode.js').hashObject(agentMainIconGroup);
+ const iconHash = require('./authenticode.js').hashObject(domain.agentfileinfo.icon);
+ if (agentMainIconGroupHash != iconHash) { destinationAgentOk = false; } // If the existing agent icon does not match the desired icon, we need to re-sign the agent.
+ }
+ }
+ }
+ } else {
+ // Check if the destination agent has the default icon
+ const agentIconGroups1 = destinationAgent.getIconInfo();
+ const agentIconGroups2 = originalAgent.getIconInfo();
+ if (agentIconGroups1.resCount != agentIconGroups2.resCount) {
+ destinationAgentOk = false; // The icon image count is different, don't bother hashing to see if the icons are different.
+ } else {
+ const iconHash1 = require('./authenticode.js').hashObject(agentIconGroups1);
+ const iconHash2 = require('./authenticode.js').hashObject(agentIconGroups2);
+ if (iconHash1 != iconHash2) { destinationAgentOk = false; } // If the existing agent icon does not match the desired icon, we need to re-sign the agent.
+ }
+ }
+ }
+
+ // Check the agent logo
+ if (destinationAgentOk == true) {
+ if ((domain.agentfileinfo != null) && (domain.agentfileinfo.logo != null)) {
+ // Check if the destination agent matches the logo we want
+ const agentBitmaps = destinationAgent.getBitmapInfo();
+ if (agentBitmaps != null) {
+ const agentBitmapNames = Object.keys(agentBitmaps);
+ if (agentBitmapNames.length > 0) {
+ const agentMainBitmap = agentBitmaps[agentBitmapNames[0]];
+ const agentMainBitmapHash = require('./authenticode.js').hashObject(agentMainBitmap);
+ const bitmapHash = require('./authenticode.js').hashObject(domain.agentfileinfo.logo);
+ if (agentMainBitmapHash != bitmapHash) { destinationAgentOk = false; } // If the existing agent logo does not match the desired logo, we need to re-sign the agent.
+ }
+ }
+ } else {
+ // Check if the destination agent has the default icon
+ const agentBitmaps1 = destinationAgent.getBitmapInfo();
+ const agentBitmaps2 = originalAgent.getBitmapInfo();
+ const agentBitmapNames = Object.keys(agentBitmaps1);
+ if (agentBitmapNames.length == 0) {
+ destinationAgentOk = false;
+ } else {
+ const iconHash1 = require('./authenticode.js').hashObject(agentBitmaps1[agentBitmapNames[0]]);
+ const iconHash2 = require('./authenticode.js').hashObject(agentBitmaps2[agentBitmapNames[0]]);
+ if (iconHash1 != iconHash2) { destinationAgentOk = false; } // If the existing agent icon does not match the desired icon, we need to re-sign the agent.
+ }
+ }
+ }
+ }
+
+ // If everything looks ok, runs a hash of the original and destination agent .text, .data and .rdata sections. If different, sign the agent again.
+ if ((destinationAgentOk == true) && (originalAgent.getHashOfSection('sha384', '.text').compare(destinationAgent.getHashOfSection('sha384', '.text')) != 0)) { destinationAgentOk = false; }
+ if ((destinationAgentOk == true) && (originalAgent.getHashOfSection('sha384', '.data').compare(destinationAgent.getHashOfSection('sha384', '.data')) != 0)) { destinationAgentOk = false; }
+ if ((destinationAgentOk == true) && (originalAgent.getHashOfSection('sha384', '.rdata').compare(destinationAgent.getHashOfSection('sha384', '.rdata')) != 0)) { destinationAgentOk = false; }
+
+ // We are done comparing the destination agent, close it.
+ destinationAgent.close();
+ }
+
+ if (destinationAgentOk == false) {
+ // If not signed correctly, sign it. First, create the server signed agent folder if needed
+ try { obj.fs.mkdirSync(serverSignedAgentsPath); } catch (ex) { }
+ const xagentSignedFunc = function agentSignedFunc(err, size) {
+ if (err == null) {
+ // Agent was signed succesfuly
+ console.log(obj.common.format('Code signed {0}.', agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname));
+ } else {
+ // Failed to sign agent
+ addServerWarning('Failed to sign \"' + agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname + '\": ' + err, 22, [agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname, err]);
+ }
+ obj.callExternalSignJob(agentSignedFunc.signingArguments); // Call external signing job regardless of success or failure
+ if (--pendingOperations === 0) { agentSignedFunc.func(); }
+ }
+ pendingOperations++;
+ xagentSignedFunc.func = func;
+ xagentSignedFunc.objx = objx;
+ xagentSignedFunc.archid = archid;
+ xagentSignedFunc.signeedagentpath = signeedagentpath;
+
+ // Parse the resources in the executable and make any required changes
+ var resChanges = false, versionStrings = null;
+ if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object')) {
+ versionStrings = originalAgent.getVersionInfo();
+ var versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
+ // Change the agent string properties
+ for (var i in versionProperties) {
+ const prop = versionProperties[i], propl = prop.toLowerCase();
+ if (domain.agentfileinfo[propl] && (domain.agentfileinfo[propl] != versionStrings[prop])) { versionStrings[prop] = domain.agentfileinfo[propl]; resChanges = true; }
+ }
+ // Change the agent file version
+ if (domain.agentfileinfo['fileversionnumber'] && (domain.agentfileinfo['fileversionnumber'] != versionStrings['~FileVersion'])) {
+ versionStrings['~FileVersion'] = domain.agentfileinfo['fileversionnumber']; resChanges = true;
+ }
+ // Change the agent product version
+ if (domain.agentfileinfo['productversionnumber'] && (domain.agentfileinfo['productversionnumber'] != versionStrings['~ProductVersion'])) {
+ versionStrings['~ProductVersion'] = domain.agentfileinfo['productversionnumber']; resChanges = true;
+ }
+ if (resChanges == true) { originalAgent.setVersionInfo(versionStrings); }
+
+ // Change the agent icon
+ if (domain.agentfileinfo.icon != null) {
+ const agentIconGroups = originalAgent.getIconInfo();
+ if (agentIconGroups != null) {
+ const agentIconGroupNames = Object.keys(agentIconGroups);
+ if (agentIconGroupNames.length > 0) {
+ const agentMainIconGroupName = agentIconGroupNames[0];
+ agentIconGroups[agentIconGroupNames[0]] = domain.agentfileinfo.icon;
+ originalAgent.setIconInfo(agentIconGroups);
+ }
+ }
+ }
+
+ // Change the agent logo
+ if (domain.agentfileinfo.logo != null) {
+ const agentBitmaps = originalAgent.getBitmapInfo();
+ if (agentBitmaps != null) {
+ const agentBitmapNames = Object.keys(agentBitmaps);
+ if (agentBitmapNames.length > 0) {
+ agentBitmaps[agentBitmapNames[0]] = domain.agentfileinfo.logo;
+ originalAgent.setBitmapInfo(agentBitmaps);
+ }
+ }
+ }
+ }
+
+ const signingArguments = { out: signeedagentpath, desc: signDesc, url: signUrl, time: timeStampUrl, proxy: timeStampProxy }; // Shallow clone
+ signingArguments.resChanges = resChanges;
+
+ obj.debug('main', "Code signing with arguments: " + JSON.stringify(signingArguments));
+ xagentSignedFunc.signingArguments = signingArguments; // Attach the signing arguments to the callback function
+ if (resChanges == false) {
+ // Sign the agent the simple way, without changing any resources.
+ originalAgent.sign(agentSignCertInfo, signingArguments, xagentSignedFunc);
+ } else {
+ // Change the agent resources and sign the agent, this is a much more involved process.
+ // NOTE: This is experimental and could corupt the agent.
+ originalAgent.writeExecutable(signingArguments, agentSignCertInfo, xagentSignedFunc);
+ }
+
+ } else {
+ // Signed agent is already ok, use it.
+ originalAgent.close();
+ }
+
+
+ }
+ }
+
+ if (--pendingOperations === 0) { func(); }
+ }
+
+ obj.callExternalSignJob = function (signingArguments) {
+ if (obj.config.settings && !obj.config.settings.externalsignjob) {
+ return;
+ }
+ obj.debug('main', "External signing job called for file: " + signingArguments.out);
+
+ const { spawnSync } = require('child_process');
+
+ const signResult = spawnSync('"' + obj.config.settings.externalsignjob + '"', ['"' + signingArguments.out + '"'], {
+ encoding: 'utf-8',
+ shell: true,
+ stdio: 'inherit'
+ });
+
+ if (signResult.error || signResult.status !== 0) {
+ obj.debug('main', "External signing failed for file: " + signingArguments.out);
+ console.error("External signing failed for file: " + signingArguments.out);
+ return;
+ }
+ }
+
+ // Update the list of available mesh agents
+ obj.updateMeshAgentsTable = function (domain, func) {
+ // Check if a custom agent signing certificate is available
+ var agentSignCertInfo = require('./authenticode.js').loadCertificates([obj.path.join(obj.datapath, 'agentsigningcert.pem')]);
+
+ // If not using a custom signing cert, get agent code signature certificate ready with the full cert chain
+ if ((agentSignCertInfo == null) && (obj.certificates.codesign != null)) {
+ agentSignCertInfo = {
+ cert: obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.codesign.cert),
+ key: obj.certificateOperations.forge.pki.privateKeyFromPem(obj.certificates.codesign.key),
+ extraCerts: [obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.root.cert)]
+ }
+ }
+
+ // Setup the domain is specified
+ var objx = domain, suffix = '';
+ if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
+
+ // Load agent information file. This includes the data & time of the agent.
+ const agentInfo = [];
+ try { agentInfo = JSON.parse(obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'hashagents.json'), 'utf8')); } catch (ex) { }
+
+ var archcount = 0;
+ for (var archid in obj.meshAgentsArchitectureNumbers) {
+ var agentpath;
+ if (domain.id == '') {
+ // Load all agents when processing the default domain
+ agentpath = obj.path.join(__dirname, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
+ if (obj.meshAgentsArchitectureNumbers[archid].unsigned !== true) {
+ const agentpath2 = obj.path.join(obj.datapath, 'signedagents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
+ if (obj.fs.existsSync(agentpath2)) { agentpath = agentpath2; } // If the agent is present in "meshcentral-data/signedagents", use that one instead.
+ const agentpath3 = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
+ if (obj.fs.existsSync(agentpath3)) { agentpath = agentpath3; } // If the agent is present in "meshcentral-data/agents", use that one instead.
+ }
+ } else {
+ // When processing an extra domain, only load agents that are specific to that domain
+ agentpath = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
+ if (obj.fs.existsSync(agentpath)) { delete obj.meshAgentsArchitectureNumbers[archid].codesign; } else { continue; } // If the agent is not present in "meshcentral-data/agents" skip.
+ }
+
+ // Fetch agent binary information
+ var stats = null;
+ try { stats = obj.fs.statSync(agentpath); } catch (ex) { }
+ if ((stats == null)) continue; // If this agent does not exist, skip it.
+
+ // Setup agent information
+ archcount++;
+ objx.meshAgentBinaries[archid] = Object.assign({}, obj.meshAgentsArchitectureNumbers[archid]);
+ objx.meshAgentBinaries[archid].path = agentpath;
+ objx.meshAgentBinaries[archid].url = 'http://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?id=' + archid;
+ objx.meshAgentBinaries[archid].size = stats.size;
+ if ((agentInfo[archid] != null) && (agentInfo[archid].mtime != null)) { objx.meshAgentBinaries[archid].mtime = new Date(agentInfo[archid].mtime); } // Set agent time if available
+
+ // If this is a windows binary, pull binary information
+ if (obj.meshAgentsArchitectureNumbers[archid].platform == 'win32') {
+ try { objx.meshAgentBinaries[archid].pe = obj.exeHandler.parseWindowsExecutable(agentpath); } catch (ex) { }
+ }
+
+ // If agents must be stored in RAM or if this is a Windows 32/64 agent, load the agent in RAM.
+ if ((obj.args.agentsinram === true) || (((archid == 3) || (archid == 4)) && (obj.args.agentsinram !== false))) {
+ if ((archid == 3) || (archid == 4)) {
+ // Load the agent with a random msh added to it.
+ const outStream = new require('stream').Duplex();
+ outStream.meshAgentBinary = objx.meshAgentBinaries[archid];
+ if (agentSignCertInfo) { outStream.meshAgentBinary.randomMsh = agentSignCertInfo.cert.subject.hash; } else { outStream.meshAgentBinary.randomMsh = obj.crypto.randomBytes(16).toString('hex'); }
+ outStream.bufferList = [];
+ outStream._write = function (chunk, encoding, callback) { this.bufferList.push(chunk); if (callback) callback(); }; // Append the chuck.
+ outStream._read = function (size) { }; // Do nothing, this is not going to be called.
+ outStream.on('finish', function () {
+ // Merge all chunks
+ this.meshAgentBinary.data = Buffer.concat(this.bufferList);
+ this.meshAgentBinary.size = this.meshAgentBinary.data.length;
+ delete this.bufferList;
+
+ // Hash the uncompressed binary
+ const hash = obj.crypto.createHash('sha384').update(this.meshAgentBinary.data);
+ this.meshAgentBinary.fileHash = hash.digest('binary');
+ this.meshAgentBinary.fileHashHex = Buffer.from(this.meshAgentBinary.fileHash, 'binary').toString('hex');
+
+ // Compress the agent using ZIP
+ const archive = require('archiver')('zip', { level: 9 }); // Sets the compression method.
+ const onZipData = function onZipData(buffer) { onZipData.x.zacc.push(buffer); }
+ const onZipEnd = function onZipEnd() {
+ // Concat all the buffer for create compressed zip agent
+ const concatData = Buffer.concat(onZipData.x.zacc);
+ delete onZipData.x.zacc;
+
+ // Hash the compressed binary
+ const hash = obj.crypto.createHash('sha384').update(concatData);
+ onZipData.x.zhash = hash.digest('binary');
+ onZipData.x.zhashhex = Buffer.from(onZipData.x.zhash, 'binary').toString('hex');
+
+ // Set the agent
+ onZipData.x.zdata = concatData;
+ onZipData.x.zsize = concatData.length;
+ }
+ const onZipError = function onZipError() { delete onZipData.x.zacc; }
+ this.meshAgentBinary.zacc = [];
+ onZipData.x = this.meshAgentBinary;
+ onZipEnd.x = this.meshAgentBinary;
+ onZipError.x = this.meshAgentBinary;
+ archive.on('data', onZipData);
+ archive.on('end', onZipEnd);
+ archive.on('error', onZipError);
+
+ // Starting with NodeJS v16, passing in a buffer at archive.append() will result a compressed file with zero byte length. To fix this, we pass in the buffer as a stream.
+ // archive.append(this.meshAgentBinary.data, { name: 'meshagent' }); // This is the version that does not work on NodeJS v16.
+ const ReadableStream = require('stream').Readable;
+ const zipInputStream = new ReadableStream();
+ zipInputStream.push(this.meshAgentBinary.data);
+ zipInputStream.push(null);
+ archive.append(zipInputStream, { name: 'meshagent' });
+
+ archive.finalize();
+ })
+ obj.exeHandler.streamExeWithMeshPolicy(
+ {
+ platform: 'win32',
+ sourceFileName: agentpath,
+ destinationStream: outStream,
+ randomPolicy: true, // Indicates that the msh policy is random data.
+ msh: outStream.meshAgentBinary.randomMsh,
+ peinfo: objx.meshAgentBinaries[archid].pe
+ });
+ } else {
+ // Load the agent as-is
+ objx.meshAgentBinaries[archid].data = obj.fs.readFileSync(agentpath);
+
+ // Compress the agent using ZIP
+ const archive = require('archiver')('zip', { level: 9 }); // Sets the compression method.
+
+ const onZipData = function onZipData(buffer) { onZipData.x.zacc.push(buffer); }
+ const onZipEnd = function onZipEnd() {
+ // Concat all the buffer for create compressed zip agent
+ const concatData = Buffer.concat(onZipData.x.zacc);
+ delete onZipData.x.zacc;
+
+ // Hash the compressed binary
+ const hash = obj.crypto.createHash('sha384').update(concatData);
+ onZipData.x.zhash = hash.digest('binary');
+ onZipData.x.zhashhex = Buffer.from(onZipData.x.zhash, 'binary').toString('hex');
+
+ // Set the agent
+ onZipData.x.zdata = concatData;
+ onZipData.x.zsize = concatData.length;
+
+ //console.log('Packed', onZipData.x.size, onZipData.x.zsize);
+ }
+ const onZipError = function onZipError() { delete onZipData.x.zacc; }
+ objx.meshAgentBinaries[archid].zacc = [];
+ onZipData.x = objx.meshAgentBinaries[archid];
+ onZipEnd.x = objx.meshAgentBinaries[archid];
+ onZipError.x = objx.meshAgentBinaries[archid];
+ archive.on('data', onZipData);
+ archive.on('end', onZipEnd);
+ archive.on('error', onZipError);
+ archive.append(objx.meshAgentBinaries[archid].data, { name: 'meshagent' });
+ archive.finalize();
+ }
+ }
+
+ // Hash the binary
+ const hashStream = obj.crypto.createHash('sha384');
+ hashStream.archid = archid;
+ hashStream.on('data', function (data) {
+ objx.meshAgentBinaries[this.archid].hash = data.toString('binary');
+ objx.meshAgentBinaries[this.archid].hashhex = data.toString('hex');
+ if ((--archcount == 0) && (func != null)) { func(); }
+ });
+ const options = { sourcePath: agentpath, targetStream: hashStream, platform: obj.meshAgentsArchitectureNumbers[archid].platform };
+ if (objx.meshAgentBinaries[archid].pe != null) { options.peinfo = objx.meshAgentBinaries[archid].pe; }
+ obj.exeHandler.hashExecutableFile(options);
+
+ // If we are not loading Windows binaries to RAM, compute the RAW file hash of the signed binaries here.
+ if ((obj.args.agentsinram === false) && ((archid == 3) || (archid == 4))) {
+ const hash = obj.crypto.createHash('sha384').update(obj.fs.readFileSync(agentpath));
+ objx.meshAgentBinaries[archid].fileHash = hash.digest('binary');
+ objx.meshAgentBinaries[archid].fileHashHex = Buffer.from(objx.meshAgentBinaries[archid].fileHash, 'binary').toString('hex');
+ }
+ }
+ };
+
+ // Generate a time limited user login token
+ obj.getLoginToken = function (userid, func) {
+ if ((userid == null) || (typeof userid != 'string')) { func('Invalid userid.'); return; }
+ const x = userid.split('/');
+ if (x == null || x.length != 3 || x[0] != 'user') { func('Invalid userid.'); return; }
+ obj.db.Get(userid, function (err, docs) {
+ if (err != null || docs == null || docs.length == 0) {
+ func('User ' + userid + ' not found.'); return;
+ } else {
+ // Load the login cookie encryption key from the database
+ obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
+ if ((docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 160)) {
+ // Key is present, use it.
+ obj.loginCookieEncryptionKey = Buffer.from(docs[0].key, 'hex');
+ func(obj.encodeCookie({ u: userid, a: 3 }, obj.loginCookieEncryptionKey));
+ } else {
+ // Key is not present, generate one.
+ obj.loginCookieEncryptionKey = obj.generateCookieKey();
+ obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() }, function () { func(obj.encodeCookie({ u: userid, a: 3 }, obj.loginCookieEncryptionKey)); });
+ }
+ });
+ }
+ });
+ };
+
+ // Show the user login token generation key
+ obj.showLoginTokenKey = function (func) {
+ // Load the login cookie encryption key from the database
+ obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
+ if ((docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 160)) {
+ // Key is present, use it.
+ func(docs[0].key);
+ } else {
+ // Key is not present, generate one.
+ obj.loginCookieEncryptionKey = obj.generateCookieKey();
+ obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() }, function () { func(obj.loginCookieEncryptionKey.toString('hex')); });
+ }
+ });
+ };
+
+ // Load the list of Intel AMT UUID and passwords from "amtactivation.log"
+ obj.loadAmtActivationLogPasswords = function (func) {
+ const amtlogfilename = obj.path.join(obj.datapath, 'amtactivation.log');
+ obj.fs.readFile(amtlogfilename, 'utf8', function (err, data) {
+ const amtPasswords = {}; // UUID --> [Passwords]
+ if ((err == null) && (data != null)) {
+ const lines = data.split('\n');
+ for (var i in lines) {
+ const line = lines[i];
+ if (line.startsWith('{')) {
+ var j = null;
+ try { j = JSON.parse(line); } catch (ex) { }
+ if ((j != null) && (typeof j == 'object')) {
+ if ((typeof j.amtUuid == 'string') && (typeof j.password == 'string')) {
+ if (amtPasswords[j.amtUuid] == null) {
+ amtPasswords[j.amtUuid] = [j.password]; // Add password to array
+ } else {
+ amtPasswords[j.amtUuid].unshift(j.password); // Add password at the start of the array
+ }
+ }
+ }
+ }
+ }
+ // Remove all duplicates and only keep the 3 last passwords for any given device
+ for (var i in amtPasswords) {
+ amtPasswords[i] = [...new Set(amtPasswords[i])];
+ while (amtPasswords[i].length > 3) { amtPasswords[i].pop(); }
+ }
+ }
+ func(obj.common.sortObj(amtPasswords)); // Sort by UUID
+ });
+ }
+
+ // Encrypt session data
+ obj.encryptSessionData = function (data, key) {
+ if (data == null) return null;
+ if (key == null) { key = obj.loginCookieEncryptionKey; }
+ try {
+ const iv = Buffer.from(obj.crypto.randomBytes(12), 'binary'), cipher = obj.crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv);
+ const crypted = Buffer.concat([cipher.update(JSON.stringify(data), 'utf8'), cipher.final()]);
+ return Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString(obj.args.cookieencoding ? obj.args.cookieencoding : 'base64').replace(/\+/g, '@').replace(/\//g, '$');
+ } catch (ex) { return null; }
+ }
+
+ // Decrypt the session data
+ obj.decryptSessionData = function (data, key) {
+ if ((typeof data != 'string') || (data.length < 13)) return {};
+ if (key == null) { key = obj.loginCookieEncryptionKey; }
+ try {
+ const buf = Buffer.from(data.replace(/\@/g, '+').replace(/\$/g, '/'), obj.args.cookieencoding ? obj.args.cookieencoding : 'base64');
+ const decipher = obj.crypto.createDecipheriv('aes-256-gcm', key.slice(0, 32), buf.slice(0, 12));
+ decipher.setAuthTag(buf.slice(12, 28));
+ return JSON.parse(decipher.update(buf.slice(28), 'binary', 'utf8') + decipher.final('utf8'));
+ } catch (ex) { return {}; }
+ }
+
+ // Generate a cryptographic key used to encode and decode cookies
+ obj.generateCookieKey = function () {
+ return Buffer.from(obj.crypto.randomBytes(80), 'binary');
+ //return Buffer.alloc(80, 0); // Sets the key to zeros, debug only.
+ };
+
+ // Encode an object as a cookie using a key using AES-GCM. (key must be 32 bytes or more)
+ obj.encodeCookie = function (o, key) {
+ try {
+ if (key == null) { key = obj.serverKey; }
+ o.time = Math.floor(Date.now() / 1000); // Add the cookie creation time
+ const iv = Buffer.from(obj.crypto.randomBytes(12), 'binary'), cipher = obj.crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv);
+ const crypted = Buffer.concat([cipher.update(JSON.stringify(o), 'utf8'), cipher.final()]);
+ const r = Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString(obj.args.cookieencoding ? obj.args.cookieencoding : 'base64').replace(/\+/g, '@').replace(/\//g, '$');
+ obj.debug('cookie', 'Encoded AESGCM cookie: ' + JSON.stringify(o));
+ return r;
+ } catch (ex) { obj.debug('cookie', 'ERR: Failed to encode AESGCM cookie due to exception: ' + ex); return null; }
+ };
+
+ // Decode a cookie back into an object using a key using AES256-GCM or AES128-CBC/HMAC-SHA384. Return null if it's not a valid cookie. (key must be 32 bytes or more)
+ obj.decodeCookie = function (cookie, key, timeout) {
+ if (cookie == null) return null;
+ var r = obj.decodeCookieAESGCM(cookie, key, timeout);
+ if (r === -1) { r = obj.decodeCookieAESSHA(cookie, key, timeout); } // If decodeCookieAESGCM() failed to decode, try decodeCookieAESSHA()
+ if ((r == null) && (obj.args.cookieencoding == null) && (cookie.length != 64) && ((cookie == cookie.toLowerCase()) || (cookie == cookie.toUpperCase()))) {
+ obj.debug('cookie', 'Upper/Lowercase cookie, try "CookieEncoding":"hex" in settings section of config.json.');
+ console.log('Upper/Lowercase cookie, try "CookieEncoding":"hex" in settings section of config.json.');
+ }
+ if ((r != null) && (typeof r.once == 'string') && (r.once.length > 0)) {
+ // This cookie must only be used once.
+ if (timeout == null) { timeout = 2; }
+ if (obj.cookieUseOnceTable[r.once] == null) {
+ const ctimeout = (((r.expire) == null || (typeof r.expire != 'number')) ? (r.time + ((timeout + 3) * 60000)) : (r.time + ((r.expire + 3) * 60000)));
+
+ // Store the used cookie in RAM
+ obj.cookieUseOnceTable[r.once] = ctimeout;
+
+ // Store the used cookie in the database
+ // TODO
+
+ // Send the used cookie to peer servers
+ // TODO
+
+ // Clean up the used table
+ if (++obj.cookieUseOnceTableCleanCounter > 20) {
+ const now = Date.now();
+ for (var i in obj.cookieUseOnceTable) { if (obj.cookieUseOnceTable[i] < now) { delete obj.cookieUseOnceTable[i]; } }
+ obj.cookieUseOnceTableCleanCounter = 0;
+ }
+ } else { return null; }
+ }
+ return r;
+ }
+
+ // Decode a cookie back into an object using a key using AES256-GCM. Return null if it's not a valid cookie. (key must be 32 bytes or more)
+ obj.decodeCookieAESGCM = function (cookie, key, timeout) {
+ try {
+ if (key == null) { key = obj.serverKey; }
+ cookie = Buffer.from(cookie.replace(/\@/g, '+').replace(/\$/g, '/'), obj.args.cookieencoding ? obj.args.cookieencoding : 'base64');
+ const decipher = obj.crypto.createDecipheriv('aes-256-gcm', key.slice(0, 32), cookie.slice(0, 12));
+ decipher.setAuthTag(cookie.slice(12, 28));
+ const o = JSON.parse(decipher.update(cookie.slice(28), 'binary', 'utf8') + decipher.final('utf8'));
+ if ((o.time == null) || (o.time == null) || (typeof o.time != 'number')) { obj.debug('cookie', 'ERR: Bad cookie due to invalid time'); return null; }
+ o.time = o.time * 1000; // Decode the cookie creation time
+ o.dtime = Date.now() - o.time; // Decode how long ago the cookie was created (in milliseconds)
+ if ((o.expire) == null || (typeof o.expire != 'number')) {
+ // Use a fixed cookie expire time
+ if (timeout == null) { timeout = 2; }
+ if ((o.dtime > (timeout * 60000)) || (o.dtime < -30000)) { obj.debug('cookie', 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right)
+ } else {
+ // An expire time is included in the cookie (in minutes), use this.
+ if ((o.expire !== 0) && ((o.dtime > (o.expire * 60000)) || (o.dtime < -30000))) { obj.debug('cookie', 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right)
+ }
+ obj.debug('cookie', 'Decoded AESGCM cookie: ' + JSON.stringify(o));
+ return o;
+ } catch (ex) { obj.debug('cookie', 'ERR: Bad AESGCM cookie due to exception: ' + ex); return -1; }
+ };
+
+ // Decode a cookie back into an object using a key using AES256 / HMAC-SHA384. Return null if it's not a valid cookie. (key must be 80 bytes or more)
+ // We do this because poor .NET does not support AES256-GCM.
+ obj.decodeCookieAESSHA = function (cookie, key, timeout) {
+ try {
+ if (key == null) { key = obj.serverKey; }
+ if (key.length < 80) { return null; }
+ cookie = Buffer.from(cookie.replace(/\@/g, '+').replace(/\$/g, '/'), obj.args.cookieencoding ? obj.args.cookieencoding : 'base64');
+ const decipher = obj.crypto.createDecipheriv('aes-256-cbc', key.slice(48, 80), cookie.slice(0, 16));
+ const rawmsg = decipher.update(cookie.slice(16), 'binary', 'binary') + decipher.final('binary');
+ const hmac = obj.crypto.createHmac('sha384', key.slice(0, 48));
+ hmac.update(rawmsg.slice(48));
+ if (Buffer.compare(hmac.digest(), Buffer.from(rawmsg.slice(0, 48))) == false) { return null; }
+ const o = JSON.parse(rawmsg.slice(48).toString('utf8'));
+ if ((o.time == null) || (o.time == null) || (typeof o.time != 'number')) { obj.debug('cookie', 'ERR: Bad cookie due to invalid time'); return null; }
+ o.time = o.time * 1000; // Decode the cookie creation time
+ o.dtime = Date.now() - o.time; // Decode how long ago the cookie was created (in milliseconds)
+ if ((o.expire) == null || (typeof o.expire != 'number')) {
+ // Use a fixed cookie expire time
+ if (timeout == null) { timeout = 2; }
+ if ((o.dtime > (timeout * 60000)) || (o.dtime < -30000)) { obj.debug('cookie', 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right)
+ } else {
+ // An expire time is included in the cookie (in minutes), use this.
+ if ((o.expire !== 0) && ((o.dtime > (o.expire * 60000)) || (o.dtime < -30000))) { obj.debug('cookie', 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right)
+ }
+ obj.debug('cookie', 'Decoded AESSHA cookie: ' + JSON.stringify(o));
+ return o;
+ } catch (ex) { obj.debug('cookie', 'ERR: Bad AESSHA cookie due to exception: ' + ex); return null; }
+ };
+
+ // Debug
+ obj.debug = function (source, ...args) {
+ // Send event to console
+ if ((obj.debugSources != null) && ((obj.debugSources == '*') || (obj.debugSources.indexOf(source) >= 0))) { console.log(source.toUpperCase() + ':', ...args); }
+
+ // Send event to log file
+ if (obj.config.settings && obj.config.settings.log) {
+ if (typeof obj.args.log == 'string') { obj.args.log = obj.args.log.split(','); }
+ if ((obj.args.log.indexOf(source) >= 0) || (obj.args.log[0] == '*')) {
+ const d = new Date();
+ if (obj.xxLogFile == null) {
+ try {
+ obj.xxLogFile = obj.fs.openSync(obj.getConfigFilePath('log.txt'), 'a+', 0o666);
+ obj.fs.writeSync(obj.xxLogFile, '---- Log start at ' + new Date().toLocaleString() + ' ----\r\n');
+ obj.xxLogDateStr = d.toLocaleDateString();
+ } catch (ex) { }
+ }
+ if (obj.xxLogFile != null) {
+ try {
+ if (obj.xxLogDateStr != d.toLocaleDateString()) { obj.xxLogDateStr = d.toLocaleDateString(); obj.fs.writeSync(obj.xxLogFile, '---- ' + d.toLocaleDateString() + ' ----\r\n'); }
+ const formattedArgs = args.map(function (arg) { return (typeof arg === 'object' && arg !== null) ? JSON.stringify(arg) : arg; });
+ obj.fs.writeSync(obj.xxLogFile, new Date().toLocaleTimeString() + ' - ' + source + ': ' + formattedArgs.join(', ') + '\r\n');
+ } catch (ex) { }
+ }
+ }
+ }
+
+ // Send the event to logged in administrators
+ if ((obj.debugRemoteSources != null) && ((obj.debugRemoteSources == '*') || (obj.debugRemoteSources.indexOf(source) >= 0))) {
+ var sendcount = 0;
+ for (var sessionid in obj.webserver.wssessions2) {
+ const ws = obj.webserver.wssessions2[sessionid];
+ if ((ws != null) && (ws.userid != null)) {
+ const user = obj.webserver.users[ws.userid];
+ if ((user != null) && (user.siteadmin == 4294967295)) {
+ try { ws.send(JSON.stringify({ action: 'trace', source: source, args: args, time: Date.now() })); sendcount++; } catch (ex) { }
+ }
+ }
+ }
+ if (sendcount == 0) { obj.debugRemoteSources = null; } // If there are no listeners, remove debug sources.
+ }
+ };
+
+ // Update server state. Writes a server state file.
+ const meshServerState = {};
+ obj.updateServerState = function (name, val) {
+ //console.log('updateServerState', name, val);
+ try {
+ if ((name != null) && (val != null)) {
+ var changed = false;
+ if ((name != null) && (meshServerState[name] != val)) { if ((val == null) && (meshServerState[name] != null)) { delete meshServerState[name]; changed = true; } else { if (meshServerState[name] != val) { meshServerState[name] = val; changed = true; } } }
+ if (changed == false) return;
+ }
+ var r = 'time=' + Date.now() + '\r\n';
+ for (var i in meshServerState) { r += (i + '=' + meshServerState[i] + '\r\n'); }
+ try {
+ obj.fs.writeFileSync(obj.getConfigFilePath('serverstate.txt'), r); // Try to write the server state, this may fail if we don't have permission.
+ } catch (ex) { obj.serverSelfWriteAllowed = false; }
+ } catch (ex) { } // Do nothing since this is not a critical feature.
+ };
+
+ // Read a list of IP addresses from a file
+ function readIpListFromFile(arg) {
+ if ((typeof arg != 'string') || (!arg.startsWith('file:'))) return arg;
+ var lines = null;
+ try { lines = obj.fs.readFileSync(obj.path.join(obj.datapath, arg.substring(5))).toString().split(/\r?\n/).join('\r').split('\r'); } catch (ex) { }
+ if (lines == null) return null;
+ const validLines = [];
+ for (var i in lines) { if ((lines[i].length > 0) && (((lines[i].charAt(0) > '0') && (lines[i].charAt(0) < '9')) || (lines[i].charAt(0) == ':'))) validLines.push(lines[i]); }
+ return validLines;
+ }
+
+ // Logging funtions
+ function logException(e) { e += ''; logErrorEvent(e); }
+ function logInfoEvent(msg) { if (obj.servicelog != null) { obj.servicelog.info(msg); } console.log(msg); }
+ function logWarnEvent(msg) { if (obj.servicelog != null) { obj.servicelog.warn(msg); } console.log(msg); }
+ function logErrorEvent(msg) { if (obj.servicelog != null) { obj.servicelog.error(msg); } console.error(msg); }
+ obj.getServerWarnings = function () { return serverWarnings; }
+ // TODO: migrate from other addServerWarning function and add timestamp
+ obj.addServerWarning = function (msg, id, args, print) { serverWarnings.push({ msg: msg, id: id, args: args }); if (print !== false) { console.log("WARNING: " + msg); } }
+
+ // auth.log functions
+ obj.authLog = function (server, msg, args) {
+ if (typeof msg != 'string') return;
+ var str = msg;
+ if (args != null) {
+ if (typeof args.sessionid == 'string') { str += ', SessionID: ' + args.sessionid; }
+ if (typeof args.useragent == 'string') { const userAgentInfo = obj.webserver.getUserAgentInfo(args.useragent); str += ', Browser: ' + userAgentInfo.browserStr + ', OS: ' + userAgentInfo.osStr; }
+ }
+ obj.debug('authlog', str);
+ if (obj.syslogauth != null) { try { obj.syslogauth.log(obj.syslogauth.LOG_INFO, str); } catch (ex) { } }
+ if (obj.authlogfile != null) { // Write authlog to file
+ try {
+ const d = new Date(), month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][d.getMonth()];
+ str = month + ' ' + d.getDate() + ' ' + obj.common.zeroPad(d.getHours(), 2) + ':' + obj.common.zeroPad(d.getMinutes(), 2) + ':' + d.getSeconds() + ' meshcentral ' + server + '[' + process.pid + ']: ' + msg + ((obj.platform == 'win32') ? '\r\n' : '\n');
+ obj.fs.write(obj.authlogfile, str, function (err, written, string) { if (err) { console.error(err); } });
+ } catch (ex) { console.error(ex); }
+ }
+ }
+
+ // Return the path of a file into the meshcentral-data path
+ obj.getConfigFilePath = function (filename) {
+ if ((obj.config != null) && (obj.config.configfiles != null) && (obj.config.configfiles[filename] != null) && (typeof obj.config.configfiles[filename] == 'string')) {
+ //console.log('getConfigFilePath(\"' + filename + '\") = ' + obj.config.configfiles[filename]);
+ return obj.config.configfiles[filename];
+ }
+ //console.log('getConfigFilePath(\"' + filename + '\") = ' + obj.path.join(obj.datapath, filename));
+ return obj.path.join(obj.datapath, filename);
+ };
+
+ return obj;
+}
+
+// Resolve a list of names, call back with list of failed resolves.
+function checkResolveAll(names, func) {
+ const dns = require('dns'), state = { func: func, count: names.length, err: null };
+ for (var i in names) {
+ dns.lookup(names[i], { all: true }, function (err, records) {
+ if (err != null) { if (this.state.err == null) { this.state.err = [this.name]; } else { this.state.err.push(this.name); } }
+ if (--this.state.count == 0) { this.state.func(this.state.err); }
+ }.bind({ name: names[i], state: state }))
+ }
+}
+
+// Resolve a list of domains to IP addresses, return a flat array of IPs.
+async function resolveDomainsToIps(originalArray) {
+ if (!Array.isArray(originalArray)) { return undefined; }
+ const flatResult = [];
+ for (const item of originalArray) {
+ if (new require('ipcheck')(item).valid) {
+ flatResult.push(item);
+ continue;
+ }
+ try {
+ const results = await require('dns').promises.lookup(item, { all: true });
+ flatResult.push(...results.map(r => r.address));
+ } catch (err) {
+ console.log(`Could not resolve ${item}`);
+ }
+ }
+ if (flatResult.length == 0) { return undefined; }
+ return flatResult;
+}
+
+// Return the server configuration
+function getConfig(createSampleConfig) {
+ // Figure out the datapath location
+ var i, datapath = null;
+ const fs = require('fs'), path = require('path'), args = require('minimist')(process.argv.slice(2));
+ if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) {
+ datapath = path.join(__dirname, '../../meshcentral-data');
+ } else {
+ datapath = path.join(__dirname, '../meshcentral-data');
+ }
+ if (args.datapath) { datapath = args.datapath; }
+ try { fs.mkdirSync(datapath); } catch (ex) { }
+
+ // Read configuration file if present and change arguments.
+ var config = {}, configFilePath = path.join(datapath, 'config.json');
+ if (args.configfile) { configFilePath = common.joinPath(datapath, args.configfile); }
+ if (fs.existsSync(configFilePath)) {
+ // Load and validate the configuration file
+ try { config = require(configFilePath); } catch (ex) { console.log('ERROR: Unable to parse ' + configFilePath + '.'); return null; }
+ if (config.domains == null) { config.domains = {}; }
+ for (i in config.domains) { if ((i.split('/').length > 1) || (i.split(' ').length > 1)) { console.log("ERROR: Error in config.json, domain names can't have spaces or /."); return null; } }
+ } else {
+ if (createSampleConfig === true) {
+ // Copy the "sample-config.json" to give users a starting point
+ const sampleConfigPath = path.join(__dirname, 'sample-config.json');
+ if (fs.existsSync(sampleConfigPath)) { fs.createReadStream(sampleConfigPath).pipe(fs.createWriteStream(configFilePath)); }
+ }
+ }
+
+ // Set the command line arguments to the config file if they are not present
+ if (!config.settings) { config.settings = {}; }
+ for (i in args) { config.settings[i] = args[i]; }
+
+ // Lower case all keys in the config file
+ try {
+ require('./common.js').objKeysToLower(config, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'telegram/proxy']);
+ } catch (ex) {
+ console.log('CRITICAL ERROR: Unable to access the file \"./common.js\".\r\nCheck folder & file permissions.');
+ process.exit();
+ }
+
+ return config;
+}
+
+// Check if a list of modules are present and install any missing ones
+function InstallModules(modules, args, func) {
+ var missingModules = [];
+ if (modules.length > 0) {
+ const dependencies = require('./package.json').dependencies;
+ for (var i in modules) {
+ // Modules may contain a version tag (foobar@1.0.0), remove it so the module can be found using require
+ const moduleNameAndVersion = modules[i];
+ const moduleInfo = moduleNameAndVersion.split('@', 3);
+ var moduleName = null;
+ var moduleVersion = null;
+ if(moduleInfo.length == 1){ // normal package without version
+ moduleName = moduleInfo[0];
+ } else if (moduleInfo.length == 2) { // normal package with a version OR custom repo package with no version
+ moduleName = moduleInfo[0] === '' ? moduleNameAndVersion : moduleInfo[0];
+ moduleVersion = moduleInfo[0] === '' ? null : moduleInfo[1];
+ } else if (moduleInfo.length == 3) { // custom repo package and package with a version
+ moduleName = "@" + moduleInfo[1];
+ moduleVersion = moduleInfo[2];
+ }
+ try {
+ // Does the module need a specific version?
+ if (moduleVersion) {
+ var versionMatch = false;
+ var modulePath = null;
+ // This is the first way to test if a module is already installed.
+ try { versionMatch = (require(`${moduleName}/package.json`).version == moduleVersion) } catch (ex) {
+ if (ex.code == "ERR_PACKAGE_PATH_NOT_EXPORTED") { modulePath = ("" + ex).split(' ').at(-1); } else { throw new Error(); }
+ }
+ // If the module is not installed, but we get the ERR_PACKAGE_PATH_NOT_EXPORTED error, try a second way.
+ if ((versionMatch == false) && (modulePath != null)) {
+ if (JSON.parse(require('fs').readFileSync(modulePath, 'utf8')).version != moduleVersion) { throw new Error(); }
+ } else if (versionMatch == false) {
+ throw new Error();
+ }
+ } else {
+ // For all other modules, do the check here.
+ // Is the module in package.json? Install exact version.
+ if (typeof dependencies[moduleName] != null) { moduleVersion = dependencies[moduleName]; }
+ require(moduleName);
+ }
+ } catch (ex) {
+ missingModules.push(moduleNameAndVersion);
+ }
+ }
+
+ if (missingModules.length > 0) { if (args.debug) { console.log('Missing Modules: ' + missingModules.join(', ')); } InstallModuleEx(missingModules, args, func); } else { func(); }
+ }
+}
+
+// Install all missing modules at once. We will be running "npm install" once, with a full list of all modules we need, no matter if they area already installed or not,
+// this is to make sure NPM gives us exactly what we need. Also, we install the meshcentral with current version, so that NPM does not update it - which it will do if obmitted.
+function InstallModuleEx(modulenames, args, func) {
+ var names = modulenames.join(' ');
+ console.log('Installing modules', modulenames);
+ const child_process = require('child_process');
+ var parentpath = __dirname;
+ function getCurrentVersion() { try { return JSON.parse(require('fs').readFileSync(require('path').join(__dirname, 'package.json'), 'utf8')).version; } catch (ex) { } return null; } // Fetch server version
+ //const meshCentralVersion = getCurrentVersion();
+ //if ((meshCentralVersion != null) && (args.dev == null)) { names = 'meshcentral@' + getCurrentVersion() + ' ' + names; }
+
+ // Get the working directory
+ if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) { parentpath = require('path').join(__dirname, '../..'); }
+
+ if (args.debug) { console.log('NPM Command Line: ' + npmpath + ` install --save-exact --no-audit --omit=optional --no-fund ${names}`); }
+ // always use --save-exact - https://stackoverflow.com/a/64507176/1210734
+ child_process.exec(npmpath + ` install --save-exact --no-audit --no-optional --omit=optional ${names}`, { maxBuffer: 512000, timeout: 300000, cwd: parentpath }, function (error, stdout, stderr) {
+ if ((error != null) && (error != '')) {
+ var mcpath = __dirname;
+ if (mcpath.endsWith('\\node_modules\\meshcentral') || mcpath.endsWith('/node_modules/meshcentral')) { mcpath = require('path').join(mcpath, '..', '..'); }
+ console.log('ERROR: Unable to install required modules. MeshCentral may not have access to npm, or npm may not have suffisent rights to load the new module. To manualy install this module try:\r\n\r\n cd "' + mcpath + '"\r\n npm install --no-audit --no-optional --omit=optional ' + names + '\r\n node node_modules' + ((require('os').platform() == 'win32') ? '\\' : '/') + 'meshcentral');
+ process.exit();
+ return;
+ }
+ func();
+ return;
+ });
+}
+
+// Detect CTRL-C on Linux and stop nicely
+process.on('SIGINT', function () { if (meshserver != null) { meshserver.Stop(); meshserver = null; } console.log('Server Ctrl-C exit...'); process.exit(); });
+
+// Add a server warning, warnings will be shown to the administrator on the web application
+// TODO: migrate to obj.addServerWarning?
+const serverWarnings = [];
+function addServerWarning(msg, id, args, print) { serverWarnings.push({ msg: msg, id: id, args: args }); if (print !== false) { console.log("WARNING: " + msg); } }
+
+/*
+var ServerWarnings = {
+ 1: "",
+ 2: "Missing WebDAV parameters.",
+ 3: "Unrecognized configuration option \"{0}\".",
+ 4: "WebSocket compression is disabled, this feature is broken in NodeJS v11.11 to v12.15 and v13.2",
+ 5: "Unable to load Intel AMT TLS root certificate for default domain.",
+ 6: "Unable to load Intel AMT TLS root certificate for domain {0}.",
+ 7: "CIRA local FQDN's ignored when server in LAN-only or WAN-only mode.",
+ 8: "Can't have more than 4 CIRA local FQDN's. Ignoring value.",
+ 9: "Agent hash checking is being skipped, this is unsafe.",
+ 10: "Missing Let's Encrypt email address.",
+ 11: "Invalid Let's Encrypt host names.",
+ 12: "Invalid Let's Encrypt names, can't contain a *.",
+ 13: "Unable to setup Let's Encrypt module.",
+ 14: "Invalid Let's Encrypt names, unable to resolve: {0}",
+ 15: "Invalid Let's Encrypt email address, unable to resolve: {0}",
+ 16: "Unable to load CloudFlare trusted proxy IPv6 address list.",
+ 17: "SendGrid server has limited use in LAN mode.",
+ 18: "SMTP server has limited use in LAN mode.",
+ 19: "SMS gateway has limited use in LAN mode.",
+ 20: "Invalid \"LoginCookieEncryptionKey\" in config.json.",
+ 21: "Backup path can't be set within meshcentral-data folder, backup settings ignored.",
+ 22: "Failed to sign agent {0}: {1}",
+ 23: "Unable to load agent icon file: {0}.",
+ 24: "Unable to load agent logo file: {0}.",
+ 25: "This NodeJS version does not support OpenID.",
+ 26: "This NodeJS version does not support Discord.js.",
+ 27: "Firebase now requires a service account JSON file, Firebase disabled."
+};
+*/
+
+// Load the really basic modules
+var npmpath = 'npm';
+var meshserver = null;
+var childProcess = null;
+var previouslyInstalledModules = {};
+function mainStart() {
+ // Check the NodeJS is version 16 or better.
+ if (Number(process.version.match(/^v(\d+\.\d+)/)[1]) < 16) { console.log("MeshCentral requires Node v16 or above, current version is " + process.version + "."); return; }
+
+ // If running within the node_modules folder, move working directory to the parent of the node_modules folder.
+ if (__dirname.endsWith('\\node_modules\\meshcentral') || __dirname.endsWith('/node_modules/meshcentral')) { process.chdir(require('path').join(__dirname, '..', '..')); }
+
+ // Check for any missing modules.
+ InstallModules(['minimist'], {}, function () {
+ // Parse inbound arguments
+ const args = require('minimist')(process.argv.slice(2));
+
+ // Setup the NPM path
+ if (args.npmpath == null) {
+ try {
+ var xnodepath = process.argv[0];
+ var xnpmpath = require('path').join(require('path').dirname(process.argv[0]), 'npm');
+ if (require('fs').existsSync(xnodepath) && require('fs').existsSync(xnpmpath)) {
+ if (xnodepath.indexOf(' ') >= 0) { xnodepath = '"' + xnodepath + '"'; }
+ if (xnpmpath.indexOf(' ') >= 0) { xnpmpath = '"' + xnpmpath + '"'; }
+ if (require('os').platform() == 'win32') { npmpath = xnpmpath; } else { npmpath = (xnodepath + ' ' + xnpmpath); }
+ }
+ } catch (ex) { console.log(ex); }
+ } else {
+ npmpath = args.npmpath;
+ }
+
+ // Get the server configuration
+ var config = getConfig(false);
+ if (config == null) { process.exit(); }
+
+ // Lowercase the auth value if present
+ for (var i in config.domains) { if (typeof config.domains[i].auth == 'string') { config.domains[i].auth = config.domains[i].auth.toLowerCase(); } }
+
+ // Get the current node version
+ const verSplit = process.version.substring(1).split('.');
+ var nodeVersion = parseInt(verSplit[0]) + (parseInt(verSplit[1]) / 100);
+
+ // Check if RDP support if present
+ var mstsc = true;
+ try { require('./rdp') } catch (ex) { mstsc = false; }
+
+ // Check if Windows SSPI, LDAP, Passport and YubiKey OTP will be used
+ var sspi = false;
+ var ldap = false;
+ var passport = [];
+ var allsspi = true;
+ var yubikey = false;
+ var ssh = false;
+ var sessionRecording = false;
+ var domainCount = 0;
+ var wildleek = false;
+ var nodemailer = false;
+ var sendgrid = false;
+ var captcha = false;
+ if (require('os').platform() == 'win32') { for (var i in config.domains) { domainCount++; if (config.domains[i].auth == 'sspi') { sspi = true; } else { allsspi = false; } } } else { allsspi = false; }
+ if (domainCount == 0) { allsspi = false; }
+ for (var i in config.domains) {
+ if (i.startsWith('_')) continue;
+ if (((config.domains[i].smtp != null) && (config.domains[i].smtp.name != 'console')) || (config.domains[i].sendmail != null)) { nodemailer = true; }
+ if (config.domains[i].sendgrid != null) { sendgrid = true; }
+ if (config.domains[i].yubikey != null) { yubikey = true; }
+ if (config.domains[i].auth == 'ldap') { ldap = true; }
+ if (mstsc == false) { config.domains[i].mstsc = false; }
+ if (config.domains[i].ssh == true) { ssh = true; }
+ if ((typeof config.domains[i].authstrategies == 'object')) {
+ if (passport.indexOf('passport') == -1) { passport.push('passport','connect-flash'); } // Passport v0.6.0 requires a patch, see https://github.com/jaredhanson/passport/issues/904 and include connect-flash here to display errors
+ if ((typeof config.domains[i].authstrategies.twitter == 'object') && (typeof config.domains[i].authstrategies.twitter.clientid == 'string') && (typeof config.domains[i].authstrategies.twitter.clientsecret == 'string') && (passport.indexOf('passport-twitter') == -1)) { passport.push('passport-twitter'); }
+ if ((typeof config.domains[i].authstrategies.google == 'object') && (typeof config.domains[i].authstrategies.google.clientid == 'string') && (typeof config.domains[i].authstrategies.google.clientsecret == 'string') && (passport.indexOf('passport-google-oauth20') == -1)) { passport.push('passport-google-oauth20'); }
+ if ((typeof config.domains[i].authstrategies.github == 'object') && (typeof config.domains[i].authstrategies.github.clientid == 'string') && (typeof config.domains[i].authstrategies.github.clientsecret == 'string') && (passport.indexOf('passport-github2') == -1)) { passport.push('passport-github2'); }
+ if ((typeof config.domains[i].authstrategies.azure == 'object') && (typeof config.domains[i].authstrategies.azure.clientid == 'string') && (typeof config.domains[i].authstrategies.azure.clientsecret == 'string') && (typeof config.domains[i].authstrategies.azure.tenantid == 'string') && (passport.indexOf('passport-azure-oauth2') == -1)) { passport.push('passport-azure-oauth2'); passport.push('jwt-simple'); }
+ if ((typeof config.domains[i].authstrategies.oidc == 'object') && (passport.indexOf('openid-client@5.7.1') == -1)) {
+ if ((nodeVersion >= 17)
+ || ((Math.floor(nodeVersion) == 16) && (nodeVersion >= 16.13))
+ || ((Math.floor(nodeVersion) == 14) && (nodeVersion >= 14.15))
+ || ((Math.floor(nodeVersion) == 12) && (nodeVersion >= 12.19))) {
+ passport.push('openid-client@5.7.1');
+ } else {
+ addServerWarning('This NodeJS version does not support OpenID Connect on MeshCentral.', 25);
+ delete config.domains[i].authstrategies.oidc;
+ }
+ }
+ if ((typeof config.domains[i].authstrategies.saml == 'object') || (typeof config.domains[i].authstrategies.jumpcloud == 'object')) { passport.push('passport-saml'); }
+ }
+ if (config.domains[i].sessionrecording != null) { sessionRecording = true; }
+ if ((config.domains[i].passwordrequirements != null) && (config.domains[i].passwordrequirements.bancommonpasswords == true)) { wildleek = true; }
+ if ((config.domains[i].newaccountscaptcha != null) && (config.domains[i].newaccountscaptcha !== false)) { captcha = true; }
+ if ((typeof config.domains[i].duo2factor == 'object') && (passport.indexOf('@duosecurity/duo_universal') == -1)) { passport.push('@duosecurity/duo_universal@2.1.0'); }
+ }
+
+ // Build the list of required modules
+ // NOTE: ALL MODULES MUST HAVE A VERSION NUMBER AND THE VERSION MUST MATCH THAT USED IN Dockerfile
+ var modules = ['archiver@7.0.1', 'body-parser@1.20.3', 'cbor@5.2.0', 'compression@1.8.1', 'cookie-session@2.1.1', 'express@4.21.2', 'express-handlebars@7.1.3', 'express-ws@5.0.2', 'ipcheck@0.1.0', 'minimist@1.2.8', 'multiparty@4.2.3', '@seald-io/nedb', 'node-forge@1.3.1', 'ua-parser-js@1.0.40', 'ua-client-hints-js@0.1.2', 'ws@8.18.0', 'yauzl@2.10.0'];
+ if (require('os').platform() == 'win32') { modules.push('node-windows@0.1.14'); modules.push('loadavg-windows@1.1.1'); if (sspi == true) { modules.push('node-sspi@0.2.10'); } } // Add Windows modules
+ if (ldap == true) { modules.push('ldapauth-fork@5.0.5'); }
+ if (ssh == true) { modules.push('ssh2@1.16.0'); }
+ if (passport != null) { modules.push(...passport); }
+ if (captcha == true) { modules.push('svg-captcha@1.4.0'); }
+
+ if (sessionRecording == true) { modules.push('image-size@2.0.2'); } // Need to get the remote desktop JPEG sizes to index the recording file.
+ if (config.letsencrypt != null) { modules.push('acme-client@4.2.5'); } // Add acme-client module. We need to force v4.2.4 or higher since olver versions using SHA-1 which is no longer supported by Let's Encrypt.
+ if (config.settings.mqtt != null) { modules.push('aedes@0.39.0'); } // Add MQTT Modules
+ if (config.settings.mysql != null) { modules.push('mysql2@3.11.4'); } // Add MySQL.
+ //if (config.settings.mysql != null) { modules.push('@mysql/xdevapi@8.0.33'); } // Add MySQL, official driver (https://dev.mysql.com/doc/dev/connector-nodejs/8.0/)
+ if (config.settings.mongodb != null) { modules.push('mongodb@4.17.2'); } // Add MongoDB, official driver. 4.17.0 and above now includes saslprep by default https://github.com/mongodb/node-mongodb-native/releases/tag/v4.17.0
+ if (config.settings.postgres != null) { modules.push('pg@8.14.1') } // Add Postgres, official driver.
+ if (config.settings.mariadb != null) { modules.push('mariadb@3.4.0'); } // Add MariaDB, official driver.
+ if (config.settings.acebase != null) { modules.push('acebase@1.29.5'); } // Add AceBase, official driver.
+ if (config.settings.sqlite3 != null) { modules.push('sqlite3@5.1.7'); } // Add sqlite3, official driver.
+ if (config.settings.vault != null) { modules.push('node-vault@0.10.2'); } // Add official HashiCorp's Vault module.
+ if ((config.settings.plugins != null) && (config.settings.plugins.proxy != null)) { modules.push('https-proxy-agent@7.0.2'); } // Required for HTTP/HTTPS proxy support
+ else if (config.settings.xmongodb != null) { modules.push('mongojs@3.1.0'); } // Add MongoJS, old driver.
+ if (nodemailer || ((config.smtp != null) && (config.smtp.name != 'console')) || (config.sendmail != null)) { modules.push('nodemailer@6.10.1'); } // Add SMTP support
+ if (sendgrid || (config.sendgrid != null)) { modules.push('@sendgrid/mail'); } // Add SendGrid support
+ if ((args.translate || args.dev) && (Number(process.version.match(/^v(\d+\.\d+)/)[1]) >= 16)) { modules.push('jsdom@22.1.0'); modules.push('esprima@4.0.1'); modules.push('html-minifier-terser@7.2.0'); } // Translation support
+ if (typeof config.settings.crowdsec == 'object') { modules.push('@crowdsec/express-bouncer@0.1.0'); } // Add CrowdSec bounser module (https://www.npmjs.com/package/@crowdsec/express-bouncer)
+ if (config.settings.prometheus != null) { modules.push('prom-client'); } // Add Prometheus Metrics support
+
+ if (typeof config.settings.autobackup == 'object') {
+ // Setup encrypted zip support if needed
+ if (config.settings.autobackup.zippassword) { modules.push('archiver-zip-encrypted@2.0.0'); }
+ // Enable Google Drive Support
+ if (typeof config.settings.autobackup.googledrive == 'object') { modules.push('googleapis@128.0.0'); }
+ // Enable WebDAV Support
+ if (typeof config.settings.autobackup.webdav == 'object') {
+ if ((typeof config.settings.autobackup.webdav.url != 'string') || (typeof config.settings.autobackup.webdav.username != 'string') || (typeof config.settings.autobackup.webdav.password != 'string')) { addServerWarning("Missing WebDAV parameters.", 2, null, !args.launch); } else { modules.push('webdav@5.8.0'); }
+ }
+ // Enable S3 Support
+ if (typeof config.settings.autobackup.s3 == 'object') { modules.push('minio@8.0.2'); }
+ }
+
+ // Setup common password blocking
+ if (wildleek == true) { modules.push('wildleek@2.0.0'); }
+
+ // Setup 2nd factor authentication
+ if (config.settings.no2factorauth !== true) {
+ // Setup YubiKey OTP if configured
+ if (yubikey == true) { modules.push('yub@0.11.1'); } // Add YubiKey OTP support (replaced yubikeyotp due to form-data issues)
+ if (allsspi == false) { modules.push('otplib@12.0.1'); } // Google Authenticator support (v10 supports older NodeJS versions).
+ }
+
+ // Desktop multiplexor support
+ if (config.settings.desktopmultiplex === true) { modules.push('image-size@2.0.2'); }
+
+ // SMS support
+ if (config.sms != null) {
+ if (config.sms.provider == 'twilio') { modules.push('twilio@4.19.0'); }
+ if (config.sms.provider == 'plivo') { modules.push('plivo@4.58.0'); }
+ if (config.sms.provider == 'telnyx') { modules.push('telnyx@1.25.5'); }
+ }
+
+ // Messaging support
+ if (config.messaging != null) {
+ if (config.messaging.telegram != null) { modules.push('telegram@2.19.8'); modules.push('input@1.0.1'); }
+ if (config.messaging.discord != null) { if (nodeVersion >= 17) { modules.push('discord.js@14.6.0'); } else { delete config.messaging.discord; addServerWarning('This NodeJS version does not support Discord.js.', 26); } }
+ if (config.messaging.xmpp != null) { modules.push('@xmpp/client@0.13.1'); }
+ if (config.messaging.pushover != null) { modules.push('node-pushover@1.0.0'); }
+ if (config.messaging.zulip != null) { modules.push('zulip@0.1.0'); }
+ }
+
+ // Setup web based push notifications
+ if ((typeof config.settings.webpush == 'object') && (typeof config.settings.webpush.email == 'string')) { modules.push('web-push@3.6.6'); }
+
+ // Firebase Support
+ if ((config.firebase != null) && (typeof config.firebase.serviceaccountfile == 'string')) { modules.push('firebase-admin@12.7.0'); }
+
+ // Syslog support
+ if ((require('os').platform() != 'win32') && (config.settings.syslog || config.settings.syslogjson)) { modules.push('modern-syslog@1.2.0'); }
+ if (config.settings.syslogtcp) { modules.push('syslog@0.1.1-1'); }
+
+ // Setup heapdump support if needed, useful for memory leak debugging
+ // https://www.arbazsiddiqui.me/a-practical-guide-to-memory-leaks-in-nodejs/
+ if (config.settings.heapdump === true) { modules.push('heapdump@0.3.15'); }
+
+ // Install any missing modules and launch the server
+ InstallModules(modules, args, function () {
+ if (require('os').platform() == 'win32') { try { require('node-windows'); } catch (ex) { console.log("Module node-windows can't be loaded. Restart MeshCentral."); process.exit(); return; } }
+ meshserver = CreateMeshCentralServer(config, args);
+ meshserver.Start();
+ });
+
+ // On exit, also terminate the child process if applicable
+ process.on('exit', function () { if (childProcess) { childProcess.kill(); childProcess = null; } });
+
+ // If our parent exits, we also exit
+ if (args.launch) {
+ process.stderr.on('end', function () { process.exit(); });
+ process.stdout.on('end', function () { process.exit(); });
+ process.stdin.on('end', function () { process.exit(); });
+ process.stdin.on('data', function (data) { });
+ }
+ });
+}
+
+if (require.main === module) {
+ mainStart(); // Called directly, launch normally.
+} else {
+ module.exports.mainStart = mainStart; // Required as a module, useful for winservice.js
+}
diff --git a/meshctrl.js b/meshctrl.js
index 8a747db88b..76e86dd288 100644
--- a/meshctrl.js
+++ b/meshctrl.js
@@ -1,3179 +1,3181 @@
-#!/usr/bin/env node
-
-/**
-* @description MeshCentral command line tool
-* @author Ylian Saint-Hilaire
-* @copyright Intel Corporation 2018-2022
-* @license Apache-2.0
-* @version v0.0.1
-*/
-
-// Make sure we have the dependency modules
-try { require('minimist'); } catch (ex) { console.log('Missing module "minimist", type "npm install minimist" to install it.'); return; }
-try { require('ws'); } catch (ex) { console.log('Missing module "ws", type "npm install ws" to install it.'); return; }
-
-var settings = {};
-const crypto = require('crypto');
-const args = require('minimist')(process.argv.slice(2));
-const path = require('path');
-const possibleCommands = ['edituser', 'listusers', 'listusersessions', 'listdevicegroups', 'listdevices', 'listusersofdevicegroup', 'listevents', 'logintokens', 'serverinfo', 'userinfo', 'adduser', 'removeuser', 'adddevicegroup', 'removedevicegroup', 'editdevicegroup', 'broadcast', 'showevents', 'addusertodevicegroup', 'removeuserfromdevicegroup', 'addusertodevice', 'removeuserfromdevice', 'sendinviteemail', 'generateinvitelink', 'config', 'movetodevicegroup', 'deviceinfo', 'removedevice', 'editdevice', 'addlocaldevice', 'addamtdevice', 'addusergroup', 'listusergroups', 'removeusergroup', 'runcommand', 'shell', 'upload', 'download', 'deviceopenurl', 'devicemessage', 'devicetoast', 'addtousergroup', 'removefromusergroup', 'removeallusersfromusergroup', 'devicesharing', 'devicepower', 'indexagenterrorlog', 'agentdownload', 'report', 'grouptoast', 'groupmessage', 'webrelay'];
-if (args.proxy != null) { try { require('https-proxy-agent'); } catch (ex) { console.log('Missing module "https-proxy-agent", type "npm install https-proxy-agent" to install it.'); return; } }
-
-if (args['_'].length == 0) {
- console.log("MeshCtrl performs command line actions on a MeshCentral server.");
- console.log("Information at: https://meshcentral.com");
- console.log("No action specified, use MeshCtrl like this:\r\n\r\n meshctrl [action] [arguments]\r\n");
- console.log("Supported actions:");
- console.log(" Help [action] - Get help on an action.");
- console.log(" ServerInfo - Show server information.");
- console.log(" UserInfo - Show user information.");
- console.log(" ListUsers - List user accounts.");
- console.log(" ListUserSessions - List online users.");
- console.log(" ListUserGroups - List user groups.");
- console.log(" ListDevices - List devices.");
- console.log(" ListDeviceGroups - List device groups.");
- console.log(" ListUsersOfDeviceGroup - List the users in a device group.");
- console.log(" ListEvents - List server events.");
- console.log(" LoginTokens - List, create and remove login tokens.");
- console.log(" DeviceInfo - Show information about a device.");
- console.log(" AddLocalDevice - Add a local device.");
- console.log(" AddAmtDevice - Add a AMT device.");
- console.log(" EditDevice - Make changes to a device.");
- console.log(" RemoveDevice - Delete a device.");
- console.log(" Config - Perform operation on config.json file.");
- console.log(" AddUser - Create a new user account.");
- console.log(" EditUser - Change a user account.");
- console.log(" RemoveUser - Delete a user account.");
- console.log(" AddUserGroup - Create a new user group.");
- console.log(" RemoveUserGroup - Delete a user group.");
- console.log(" AddToUserGroup - Add a user, device or device group to a user group.");
- console.log(" RemoveFromUserGroup - Remove a user, device or device group from a user group.");
- console.log(" RemoveAllUsersFromUserGroup - Remove all users from a user group.");
- console.log(" AddDeviceGroup - Create a new device group.");
- console.log(" RemoveDeviceGroup - Delete a device group.");
- console.log(" EditDeviceGroup - Change a device group values.");
- console.log(" MoveToDeviceGroup - Move a device to a different device group.");
- console.log(" AddUserToDeviceGroup - Add a user to a device group.");
- console.log(" RemoveUserFromDeviceGroup - Remove a user from a device group.");
- console.log(" AddUserToDevice - Add a user to a device.");
- console.log(" RemoveUserFromDevice - Remove a user from a device.");
- console.log(" SendInviteEmail - Send an agent install invitation email.");
- console.log(" GenerateInviteLink - Create an invitation link.");
- console.log(" Broadcast - Display a message to all online users.");
- console.log(" ShowEvents - Display real-time server events in JSON format.");
- console.log(" RunCommand - Run a shell command on a remote device.");
- console.log(" Shell - Access command shell of a remote device.");
- console.log(" Upload - Upload a file to a remote device.");
- console.log(" Download - Download a file from a remote device.");
- console.log(" WebRelay - Creates a HTTP/HTTPS webrelay link for a remote device.");
- console.log(" DeviceOpenUrl - Open a URL on a remote device.");
- console.log(" DeviceMessage - Open a message box on a remote device.");
- console.log(" DeviceToast - Display a toast notification on a remote device.");
- console.log(" GroupMessage - Open a message box on remote devices in a specific device group.");
- console.log(" GroupToast - Display a toast notification on remote devices in a specific device group.");
- console.log(" DevicePower - Perform wake/sleep/reset/off operations on remote devices.");
- console.log(" DeviceSharing - View, add and remove sharing links for a given device.");
- console.log(" AgentDownload - Download an agent of a specific type for a device group.");
- console.log(" Report - Create and show a CSV report.");
- console.log("\r\nSupported login arguments:");
- console.log(" --url [wss://server] - Server url, wss://localhost:443 is default.");
- console.log(" - Use wss://localhost:443?key=xxx if login key is required.");
- console.log(" --loginuser [username] - Login username, admin is default.");
- console.log(" --loginpass [password] - Login password OR Leave blank to enter password at prompt");
- console.log(" --token [number] - 2nd factor authentication token.");
- console.log(" --loginkey [hex] - Server login key in hex.");
- console.log(" --loginkeyfile [file] - File containing server login key in hex.");
- console.log(" --logindomain [domainid] - Domain id, default is empty, only used with loginkey.");
- console.log(" --proxy [http://proxy:123] - Specify an HTTP proxy.");
- return;
-} else {
- settings.cmd = args['_'][0].toLowerCase();
- if ((possibleCommands.indexOf(settings.cmd) == -1) && (settings.cmd != 'help')) { console.log("Invalid command. Possible commands are: " + possibleCommands.join(', ') + '.'); return; }
- //console.log(settings.cmd);
-
- var ok = false;
- switch (settings.cmd) {
- case 'config': { performConfigOperations(args); return; }
- case 'indexagenterrorlog': { indexAgentErrorLog(); return; }
- case 'serverinfo': { ok = true; break; }
- case 'userinfo': { ok = true; break; }
- case 'listusers': { ok = true; break; }
- case 'listusersessions': { ok = true; break; }
- case 'listusergroups': { ok = true; break; }
- case 'listdevicegroups': { ok = true; break; }
- case 'listdevices': { ok = true; break; }
- case 'listevents': { ok = true; break; }
- case 'logintokens': { ok = true; break; }
- case 'listusersofdevicegroup':
- case 'deviceinfo':
- case 'removedevice':
- case 'editdevice': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else { ok = true; }
- break;
- }
- case 'addlocaldevice': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else if (args.devicename == null) { console.log(winRemoveSingleQuotes("Missing devicename, use --devicename [devicename]")); }
- else if (args.hostname == null) { console.log(winRemoveSingleQuotes("Missing hostname, use --hostname [hostname]")); }
- else { ok = true; }
- break;
- }
- case 'addamtdevice': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else if (args.devicename == null) { console.log(winRemoveSingleQuotes("Missing devicename, use --devicename [devicename]")); }
- else if (args.hostname == null) { console.log(winRemoveSingleQuotes("Missing hostname, use --hostname [hostname]")); }
- else if (args.user == null) { console.log(winRemoveSingleQuotes("Missing user, use --user [user]")); }
- else if (args.pass == null) { console.log(winRemoveSingleQuotes("Missing pass, use --pass [pass]")); }
- else { ok = true; }
- break;
- }
- case 'addusertodevicegroup': {
- if ((args.id == null) && (args.group == null)) { console.log(winRemoveSingleQuotes("Device group identifier missing, use --id '[groupid]' or --group [groupname]")); }
- else if (args.userid == null) { console.log("Add user to group missing useid, use --userid [userid]"); }
- else { ok = true; }
- break;
- }
- case 'removeuserfromdevicegroup': {
- if ((args.id == null) && (args.group == null)) { console.log(winRemoveSingleQuotes("Device group identifier missing, use --id '[groupid]' or --group [groupname]")); }
- else if (args.userid == null) { console.log("Remove user from group missing useid, use --userid [userid]"); }
- else { ok = true; }
- break;
- }
- case 'addusertodevice': {
- if (args.userid == null) { console.log("Add user to device missing userid, use --userid [userid]"); }
- else if (args.id == null) { console.log(winRemoveSingleQuotes("Add user to device missing device id, use --id '[deviceid]'")); }
- else { ok = true; }
- break;
- }
- case 'removeuserfromdevice': {
- if (args.userid == null) { console.log("Remove user from device missing userid, use --userid [userid]"); }
- else if (args.id == null) { console.log(winRemoveSingleQuotes("Remove user from device missing device id, use --id '[deviceid]'")); }
- else { ok = true; }
- break;
- }
- case 'adddevicegroup': {
- if (args.name == null) { console.log("Message group name, use --name [name]"); }
- else { ok = true; }
- break;
- }
- case 'editdevicegroup':
- case 'removedevicegroup': {
- if ((args.id == null) && (args.group == null)) { console.log(winRemoveSingleQuotes("Device group identifier missing, use --id '[groupid]' or --group [groupname]")); }
- else { ok = true; }
- break;
- }
- case 'movetodevicegroup': {
- if ((args.id == null) && (args.group == null)) { console.log(winRemoveSingleQuotes("Device group identifier missing, use --id '[groupid]' or --group [groupname]")); }
- else if (args.devid == null) { console.log(winRemoveSingleQuotes("Device identifier missing, use --devid '[deviceid]'")); }
- else { ok = true; }
- break;
- }
- case 'broadcast': {
- if (args.msg == null) { console.log("Message missing, use --msg [message]"); }
- else { ok = true; }
- break;
- }
- case 'showevents': {
- ok = true;
- break;
- }
- case 'adduser': {
- if (args.user == null) { console.log("New account name missing, use --user [name]"); }
- else if ((args.pass == null) && (args.randompass == null)) { console.log("New account password missing, use --pass [password] or --randompass"); }
- else { ok = true; }
- break;
- }
- case 'edituser': {
- if (args.userid == null) { console.log("Edit account user missing, use --userid [id]"); }
- else { ok = true; }
- break;
- }
- case 'removeuser': {
- if (args.userid == null) { console.log("Remove account userid missing, use --userid [id]"); }
- else { ok = true; }
- break;
- }
- case 'addusergroup': {
- if (args.name == null) { console.log("New user group name missing, use --name [name]"); }
- else { ok = true; }
- break;
- }
- case 'removeusergroup': {
- if (args.groupid == null) { console.log(winRemoveSingleQuotes("Remove user group id missing, use --groupid '[id]'")); }
- else { ok = true; }
- break;
- }
- case 'addtousergroup': {
- if (args.groupid == null) { console.log(winRemoveSingleQuotes("Group id missing, use --groupid '[id]'")); }
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing identifier to add, use --id [id]")); }
- else { ok = true; }
- break;
- }
- case 'removefromusergroup': {
- if (args.groupid == null) { console.log(winRemoveSingleQuotes("Group id missing, use --groupid '[id]'")); }
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing identifier to remove, use --id [id]")); }
- else { ok = true; }
- break;
- }
- case 'removeallusersfromusergroup': {
- if (args.groupid == null) { console.log(winRemoveSingleQuotes("Group id missing, use --groupid '[id]'")); }
- else { ok = true; }
- break;
- }
- case 'sendinviteemail': {
- if ((args.id == null) && (args.group == null)) { console.log("Device group identifier missing, use --id '[groupid]' or --group [groupname]"); }
- else if (args.email == null) { console.log("Device email is missing, use --email [email]"); }
- else { ok = true; }
- break;
- }
- case 'generateinvitelink': {
- if ((args.id == null) && (args.group == null)) { console.log("Device group identifier missing, use --id '[groupid]' or --group [groupname]"); }
- else if (args.hours == null) { console.log("Invitation validity period missing, use --hours [hours]"); }
- else { ok = true; }
- break;
- }
- case 'runcommand': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else if (args.run == null) { console.log("Missing run, use --run \"command\""); }
- else { ok = true; }
- break;
- }
- case 'shell': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else { ok = true; }
- break;
- }
- case 'devicepower': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else { ok = true; }
- break;
- }
- case 'devicesharing': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else if ((args.daily != null) && (args.weekly != null)) { console.log(winRemoveSingleQuotes("Can't specify both --daily and --weekly at the same time.")); }
- else { ok = true; }
- break;
- }
- case 'agentdownload': {
- if (args.type == null) { console.log(winRemoveSingleQuotes("Missing device type, use --type [agenttype]")); }
- else if ((parseInt(args.type) == null) || isNaN(parseInt(args.type)) || (parseInt(args.type) < 1) || (parseInt(args.type) > 11000)) { console.log(winRemoveSingleQuotes("Invalid agent type, must be a number.")); }
- else if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[meshid]'")); }
- else if ((typeof args.id != 'string') || (args.id.length != 64)) { console.log(winRemoveSingleQuotes("Invalid meshid.")); }
- else { ok = true; }
- break;
- }
- case 'upload': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else if (args.file == null) { console.log("Local file missing, use --file [file] specify the file to upload"); }
- else if (args.target == null) { console.log("Remote target path missing, use --target [path] to specify the remote location"); }
- else if (require('fs').existsSync(args.file) == false) { console.log("Local file does not exists, check --file"); }
- else { ok = true; }
- break;
- }
- case 'download': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else if (args.file == null) { console.log("Remote file missing, use --file [file] specify the remote file to download"); }
- else if (args.target == null) { console.log("Target path missing, use --target [path] to specify the local download location"); }
- else { ok = true; }
- break;
- }
- case 'webrelay': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else if (args.type == null) { console.log(winRemoveSingleQuotes("Missing protocol type, use --type [http,https]")); }
- else { ok = true; }
- break;
- }
- case 'deviceopenurl': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else if (args.openurl == null) { console.log("Remote URL, use --openurl [url] specify the link to open."); }
- else { ok = true; }
- break;
- }
- case 'devicemessage': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else if (args.msg == null) { console.log("Remote message, use --msg \"[message]\" specify a remote message."); }
- else { ok = true; }
- break;
- }
- case 'devicetoast': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else if (args.msg == null) { console.log("Remote message, use --msg \"[message]\" specify a remote message."); }
- else { ok = true; }
- break;
- }
- case 'groupmessage': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device group id, use --id '[devicegroupid]'")); }
- else if (args.msg == null) { console.log("Remote message, use --msg \"[message]\" specify a remote message."); }
- else { ok = true; }
- break;
- }
- case 'grouptoast': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device group id, use --id '[devicegroupid]'")); }
- else if (args.msg == null) { console.log("Remote message, use --msg \"[message]\" specify a remote message."); }
- else { ok = true; }
- break;
- }
- case 'report': {
- if (args.type == null) { console.log(winRemoveSingleQuotes("Missing report type, use --type '[reporttype]'")); }
- else { ok = true; }
- break;
- }
- case 'help': {
- if (args['_'].length < 2) {
- console.log("Get help on an action. Type:\r\n\r\n help [action]\r\n\r\nPossible actions are: " + possibleCommands.join(', ') + '.');
- } else {
- switch (args['_'][1].toLowerCase()) {
- case 'config': {
- displayConfigHelp();
- break;
- }
- case 'sendinviteemail': {
- console.log("Send invitation email with instructions on how to install the mesh agent for a specific device group. Example usage:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl SendInviteEmail --id 'groupid' --message \"msg\" --email user@sample.com"));
- console.log(winRemoveSingleQuotes(" MeshCtrl SendInviteEmail --group \"My Computers\" --name \"Jack\" --email user@sample.com"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [groupid] - Device group identifier (or --group).");
- } else {
- console.log(" --id '[groupid]' - Device group identifier (or --group).");
- }
- console.log(" --group [groupname] - Device group name (or --id).");
- console.log(" --email [email] - Email address.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --name (name) - Name of recipient to be included in the email.");
- console.log(" --message (msg) - Message to be included in the email.");
- break;
- }
- case 'generateinvitelink': {
- console.log("Generate a agent invitation URL for a given group. Example usage:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl GenerateInviteLink --id 'groupid' --hours 24"));
- console.log(" MeshCtrl GenerateInviteLink --group \"My Computers\" --hours 0");
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [groupid] - Device group identifier (or --group).");
- } else {
- console.log(" --id '[groupid]' - Device group identifier (or --group).");
- }
- console.log(" --group [groupname] - Device group name (or --id).");
- console.log(" --hours [hours] - Validity period in hours or 0 for infinite.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --flags [mode] - Mode flag for link type (0 = both, 1 = interactive only, 2 = background only)");
- break;
- }
- case 'showevents': {
- console.log("Show the server's event stream for this user account. Example usage:\r\n");
- console.log(" MeshCtrl ShowEvents");
- console.log(" MeshCtrl ShowEvents --filter nodeconnect");
- console.log(" MeshCtrl ShowEvents --filter uicustomevent,changenode");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --filter [actions] - Show only specified actions.");
- break;
- }
- case 'serverinfo': {
- console.log("Get information on the MeshCentral server, Example usages:\r\n");
- console.log(" MeshCtrl ServerInfo --loginuser myaccountname --loginpass mypassword");
- console.log(" MeshCtrl ServerInfo --loginuser myaccountname --loginkeyfile key.txt");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --json - Show result as JSON.");
- break;
- }
- case 'userinfo': {
- console.log("Get account information for the login account, Example usages:\r\n");
- console.log(" MeshCtrl UserInfo --loginuser myaccountname --loginpass mypassword");
- console.log(" MeshCtrl UserInfo --loginuser myaccountname --loginkeyfile key.txt");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --json - Show result as JSON.");
- break;
- }
- case 'listusers': {
- console.log("List the account on the MeshCentral server, Example usages:\r\n");
- console.log(" MeshCtrl ListUsers");
- console.log(" MeshCtrl ListUsers --json");
- console.log(" MeshCtrl ListUsers --nameexists \"bob\"");
- console.log(" MeshCtrl ListUsers --filter 2fa");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --idexists [id] - Return 1 if id exists, 0 if not.");
- console.log(" --nameexists [name] - Return id if name exists.");
- console.log(" --filter [filter1,...] - Filter user names: 2FA, NO2FA.");
- console.log(" --json - Show result as JSON.");
- break;
- }
- case 'listusersessions': {
- console.log("List active user sessions on the MeshCentral server, Example usages:\r\n");
- console.log(" MeshCtrl ListUserSessions");
- console.log(" MeshCtrl ListUserSessions --json");
- break;
- }
- case 'listusergroups': {
- console.log("List user groups on the MeshCentral server, Example usages:\r\n");
- console.log(" MeshCtrl ListUserGroups");
- console.log(" MeshCtrl ListUserGroups --json");
- break;
- }
- case 'listdevicegroups': {
- console.log("List the device groups for this account. Example usages:\r\n");
- console.log(" MeshCtrl ListDeviceGroups ");
- console.log(" MeshCtrl ListDeviceGroups --json");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --idexists [id] - Return 1 if id exists, 0 if not.");
- console.log(" --nameexists [name] - Return id if name exists.");
- console.log(" --emailexists [email] - Return id if email exists.");
- console.log(" --hex - Display meshid in hex format.");
- console.log(" --json - Show result as JSON.");
- break;
- }
- case 'listdevices': {
- console.log("List devices. Example usages:\r\n");
- console.log(" MeshCtrl ListDevices");
- console.log(winRemoveSingleQuotes(" MeshCtrl ListDevices -id '[groupid]' --json"));
- console.log("\r\nOptional arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [groupid] - Filter by group identifier (or --group).");
- } else {
- console.log(" --id '[groupid]' - Filter by group identifier (or --group).");
- }
- console.log(" --group [groupname] - Filter by group name (or --id).");
- console.log(" --count - Only return the device count.");
- console.log(" --json - Show result as JSON.");
- console.log(" --csv - Show result as comma separated values.");
- console.log(" --filter \"[filter]\" - Filter devices using a filter string.");
- console.log(" \"x\" - Devices with \"x\" in the name.");
- console.log(" \"user:x or u:x\" - Devices with \"x\" in the name of currently logged in user.");
- console.log(" \"ip:x\" - Devices \"x\" IP address.");
- console.log(" \"group:x or g:x\" - Devices with \"x\" in device group name.");
- console.log(" \"tag:x or t:x\" - Devices with \"x\" in device tag.");
- console.log(" \"atag:x or a:x\" - Devices with \"x\" in device agent tag.");
- console.log(" \"os:x\" - Devices with \"x\" in the device OS description.");
- console.log(" \"amt:x\" - Devices with Intel AMT provisioning state (0, 1, 2).");
- console.log(" \"desc:x\" - Devices with \"x\" in device description.");
- console.log(" \"wsc:ok\" - Devices with Windows Security Center ok.");
- console.log(" \"wsc:noav\" - Devices with Windows Security Center with anti-virus problem.");
- console.log(" \"wsc:noupdate\" - Devices with Windows Security Center with update problem.");
- console.log(" \"wsc:nofirewall\" - Devices with Windows Security Center with firewall problem.");
- console.log(" \"wsc:any\" - Devices with Windows Security Center with any problem.");
- console.log(" \"a and b\" - Match both conditions with precedence over OR. For example: \"lab and g:home\".");
- console.log(" \"a or b\" - Math one of the conditions, for example: \"lab or g:home\".");
- console.log(" --filterid [id,id...] - Show only results for devices with included id.");
- console.log(" --details - Show all device details.");
- break;
- }
- case 'listusersofdevicegroup': {
- console.log("List users that have permissions for a given device group. Example usage:\r\n");
- console.log(" MeshCtrl ListUserOfDeviceGroup ");
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [groupid] - Device group identifier.");
- } else {
- console.log(" --id '[groupid]' - Device group identifier.");
- }
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --json - Show result as JSON.");
- break;
- }
- case 'listevents': {
- console.log("List server events optionally filtered by user or device. Example usage:\r\n");
- console.log(" MeshCtrl ListEvents ");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --userid [name] - User account identifier.");
- console.log(" --id [deviceid] - The device identifier.");
- console.log(" --limit [number] - Maximum number of events to list.");
- console.log(" --raw - Output raw data in JSON format.");
- console.log(" --json - Give results in JSON format.");
- break;
- }
- case 'logintokens': {
- console.log("List account login tokens and allow addition and removal. Example usage:\r\n");
- console.log(" MeshCtrl LoginTokens ");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --remove [name] - Remove a login token.");
- console.log(" --add [name] - Add a login token.");
- console.log(" --expire [minutes] - When adding a token, minutes until expire.");
- console.log(" --json - Show login tokens in JSON format.");
- break;
- }
- case 'adduser': {
- console.log("Add a new user account. Example usages:\r\n");
- console.log(" MeshCtrl AddUser --user newaccountname --pass newpassword");
- console.log(" MeshCtrl AddUser --user newaccountname --randompass --rights full");
- console.log("\r\nRequired arguments:\r\n");
- console.log(" --user [name] - New account name.");
- console.log(" --pass [password] - New account password.");
- console.log(" --randompass - Create account with a random password.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --domain [domain] - Account domain, only for cross-domain admins.");
- console.log(" --email [email] - New account email address.");
- console.log(" --emailverified - New account email is verified.");
- console.log(" --resetpass - Request password reset on next login.");
- console.log(" --realname [name] - Set the real name for this account.");
- console.log(" --phone [number] - Set the account phone number.");
- console.log(" --rights [none|full|a,b,c] - Comma separated list of server permissions. Possible values:");
- console.log(" manageusers,serverbackup,serverrestore,serverupdate,fileaccess,locked,nonewgroups,notools,usergroups,recordings,locksettings,allevents,nonewdevices");
- break;
- }
- case 'edituser': {
- console.log("Edit a user account, Example usages:\r\n");
- console.log(" MeshCtrl EditUser --userid user --rights locked,locksettings");
- console.log(" MeshCtrl EditUser --userid user --realname Jones");
- console.log("\r\nRequired arguments:\r\n");
- console.log(" --userid [name] - User account identifier.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --domain [domain] - Account domain, only for cross-domain admins.");
- console.log(" --email [email] - Account email address.");
- console.log(" --emailverified - Account email is verified.");
- console.log(" --resetpass - Request password reset on next login.");
- console.log(" --realname [name] - Set the real name for this account.");
- console.log(" --phone [number] - Set the account phone number.");
- console.log(" --rights [none|full|a,b,c] - Comma separated list of server permissions. Possible values:");
- console.log(" manageusers,serverbackup,serverrestore,serverupdate,fileaccess,locked,nonewgroups,notools,usergroups,recordings,locksettings,allevents,nonewdevices");
- break;
- }
- case 'removeuser': {
- console.log("Delete a user account, Example usages:\r\n");
- console.log(" MeshCtrl RemoveUser --userid accountid");
- console.log("\r\nRequired arguments:\r\n");
- console.log(" --userid [id] - Account identifier.");
- break;
- }
- case 'addusergroup': {
- console.log("Create a new user group, Example usages:\r\n");
- console.log(" MeshCtrl AddUserGroup --name \"Test Group\"");
- console.log("\r\nRequired arguments:\r\n");
- console.log(" --name [name] - Name of the user group.");
- break;
- }
- case 'removeusergroup': {
- console.log("Remove a user group, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl RemoveUserGroup --groupid 'ugrp//abcdf'"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --groupid [groupid] - User group identifier.");
- } else {
- console.log(" --groupid '[groupid]' - User group identifier.");
- }
- break;
- }
- case 'addtousergroup': {
- console.log("Add a user, device or device group to a user group, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl AddToUserGroup --id 'user//abcdef' --groupid 'ugrp//abcdf'"));
- console.log(winRemoveSingleQuotes(" MeshCtrl AddToUserGroup --id 'node//abcdef' --groupid 'ugrp//abcdf' --rights [rights]"));
- console.log(winRemoveSingleQuotes(" MeshCtrl AddToUserGroup --id 'mesh//abcdef' --groupid 'ugrp//abcdf' --rights [rights]"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [id] - Identifier to add.");
- console.log(" --groupid [groupid] - User group identifier.");
- } else {
- console.log(" --id '[id]' - Identifier to add.");
- console.log(" --groupid '[groupid]' - User group identifier.");
- }
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --rights [number] - Rights granted for adding device or device group.");
- console.log(" - 4294967295 for full admin or the sum of the following numbers.");
- console.log(" 1 = Edit Device Group 2 = Manage Users ");
- console.log(" 4 = Manage Computers 8 = Remote Control ");
- console.log(" 16 = Agent Console 32 = Server Files ");
- console.log(" 64 = Wake Device 128 = Set Notes ");
- console.log(" 256 = Remote View Only 512 = No Terminal ");
- console.log(" 1024 = No Files 2048 = No Intel AMT ");
- console.log(" 4096 = Desktop Limited Input 8192 = Limit Events ");
- console.log(" 16384 = Chat / Notify 32768 = Uninstall Agent ");
- console.log(" 65536 = No Remote Desktop 131072 = Remote Commands ");
- console.log(" 262144 = Reset / Power off ");
- break;
- }
- case 'removefromusergroup': {
- console.log("Remove a user, device or device group from a user group, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl RemoveUserFromUserGroup --userid 'user//abcdef' --groupid 'ugrp//abcdf'"));
- console.log(winRemoveSingleQuotes(" MeshCtrl RemoveUserFromUserGroup --userid 'node//abcdef' --groupid 'ugrp//abcdf'"));
- console.log(winRemoveSingleQuotes(" MeshCtrl RemoveUserFromUserGroup --userid 'mesh//abcdef' --groupid 'ugrp//abcdf'"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [userid] - Identifier to remove.");
- console.log(" --groupid [groupid] - User group identifier.");
- } else {
- console.log(" --id '[userid]' - Identifier to remove.");
- console.log(" --groupid '[groupid]' - User group identifier.");
- }
- break;
- }
- case 'removeallusersfromusergroup': {
- console.log("Remove all users from a user group, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl RemoveAllUsersFromUserGroup --groupid 'ugrp//abcdf'"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --groupid [groupid] - User group identifier.");
- } else {
- console.log(" --groupid '[groupid]' - User group identifier.");
- }
- break;
- }
- case 'adddevicegroup': {
- console.log("Add a device group, Example usages:\r\n");
- console.log(" MeshCtrl AddDeviceGroup --name newgroupname");
- console.log(" MeshCtrl AddDeviceGroup --name newgroupname --desc description --amtonly");
- console.log(" MeshCtrl AddDeviceGroup --name newgroupname --features 1 --consent 7");
- console.log("\r\nRequired arguments:\r\n");
- console.log(" --name [name] - Name of the new group.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --desc [description] - New group description.");
- console.log(" --amtonly - New group is agent-less, Intel AMT only.");
- console.log(" --agentless - New group is agent-less only.");
- console.log(" --features [number] - Set device group features, sum of numbers below.");
- console.log(" 1 = Auto-Remove 2 = Hostname Sync");
- console.log(" 4 = Record Sessions");
- console.log(" --consent [number] - Set device group user consent, sum of numbers below.");
- console.log(" 1 = Desktop notify user 2 = Terminal notify user ");
- console.log(" 4 = Files notify user 8 = Desktop prompt user ");
- console.log(" 16 = Terminal prompt user 32 = Files prompt user ");
- console.log(" 64 = Desktop Toolbar ");
- break;
- }
- case 'removedevicegroup': {
- console.log("Remove a device group, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl RemoveDeviceGroup --id 'groupid'"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [groupid] - Device group identifier (or --group).");
- } else {
- console.log(" --id '[groupid]' - Device group identifier (or --group).");
- }
- console.log(" --group [groupname] - Device group name (or --id).");
- break;
- }
- case 'editdevicegroup': {
- console.log("Edit a device group, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl EditDeviceGroup --id 'groupid' --name \"New Name\""));
- console.log(winRemoveSingleQuotes(" MeshCtrl EditDeviceGroup --id 'groupid' --desc \"Description\" --consent 63"));
- console.log(winRemoveSingleQuotes(" MeshCtrl EditDeviceGroup --id 'groupid' --invitecodes \"code1,code2\" --backgroundonly"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [groupid] - Device group identifier (or --group).");
- } else {
- console.log(" --id '[groupid]' - Device group identifier (or --group).");
- }
- console.log(" --group [groupname] - Device group name (or --id).");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --name [name] - Set new device group name.");
- console.log(" --desc [description] - Set new device group description, blank to clear.");
- console.log(" --flags [number] - Set device group flags, sum of the values below, 0 for none.");
- console.log(" 1 = Auto remove device on disconnect.");
- console.log(" 2 = Sync hostname.");
- console.log(" --consent [number] - Set device group consent options, sum of the values below, 0 for none.");
- console.log(" 1 = Desktop notify user.");
- console.log(" 2 = Terminal notify user.");
- console.log(" 4 = Files notify user.");
- console.log(" 8 = Desktop prompt for user consent.");
- console.log(" 16 = Terminal prompt for user consent.");
- console.log(" 32 = Files prompt for user consent.");
- console.log(" 64 = Desktop show connection toolbar.");
- console.log(" --invitecodes [aa,bb] - Comma separated list of invite codes, blank to clear.");
- console.log(" --backgroundonly - When used with invitecodes, set agent to only install in background.");
- console.log(" --interactiveonly - When used with invitecodes, set agent to only run on demand.");
- break;
- }
- case 'movetodevicegroup': {
- console.log("Move a device to a new device group, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl MoveToDeviceGroup --devid 'deviceid' --id 'groupid'"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [groupid] - Device group identifier (or --group).");
- } else {
- console.log(" --id '[groupid]' - Device group identifier (or --group).");
- }
- console.log(" --group [groupname] - Device group name (or --id).");
- if (process.platform == 'win32') {
- console.log(" --devid [deviceid] - Device identifier.");
- } else {
- console.log(" --devid '[deviceid]' - Device identifier.");
- }
- break;
- }
- case 'addusertodevicegroup': {
- console.log("Add a user to a device group, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl AddUserToDeviceGroup --id 'groupid' --userid userid --fullrights"));
- console.log(" MeshCtrl AddUserToDeviceGroup --group groupname --userid userid --editgroup --manageusers");
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [groupid] - Device group identifier (or --group).");
- } else {
- console.log(" --id '[groupid]' - Device group identifier (or --group).");
- }
- console.log(" --group [groupname] - Device group name (or --id).");
- console.log(" --userid [userid] - The user identifier.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --fullrights - Allow full rights over this device group.");
- console.log(" --editgroup - Allow the user to edit group information.");
- console.log(" --manageusers - Allow the user to add/remove users.");
- console.log(" --managedevices - Allow the user to edit device information.");
- console.log(" --remotecontrol - Allow device remote control operations.");
- console.log(" --agentconsole - Allow agent console operations.");
- console.log(" --serverfiles - Allow access to group server files.");
- console.log(" --wakedevices - Allow device wake operation.");
- console.log(" --notes - Allow editing of device notes.");
- console.log(" --desktopviewonly - Restrict user to view-only remote desktop.");
- console.log(" --limiteddesktop - Limit remote desktop keys.");
- console.log(" --noterminal - Hide the terminal tab from this user.");
- console.log(" --nofiles - Hide the files tab from this user.");
- console.log(" --noamt - Hide the Intel AMT tab from this user.");
- console.log(" --limitedevents - User can only see his own events.");
- console.log(" --chatnotify - Allow chat and notification options.");
- console.log(" --uninstall - Allow remote uninstall of the agent.");
- if (args.limiteddesktop) { meshrights |= 4096; }
- if (args.limitedevents) { meshrights |= 8192; }
- if (args.chatnotify) { meshrights |= 16384; }
- if (args.uninstall) { meshrights |= 32768; }
-
- break;
- }
- case 'removeuserfromdevicegroup': {
- console.log("Remove a user from a device group, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl RemoveuserFromDeviceGroup --id 'groupid' --userid userid"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [groupid] - Device group identifier (or --group).");
- } else {
- console.log(" --id '[groupid]' - Device group identifier (or --group).");
- }
- console.log(" --group [groupname] - Device group name (or --id).");
- console.log(" --userid [userid] - The user identifier.");
- break;
- }
- case 'addusertodevice': {
- console.log("Add a user to a device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl AddUserToDevice --id 'deviceid' --userid userid --fullrights"));
- console.log(winRemoveSingleQuotes(" MeshCtrl AddUserToDevice --id 'deviceid' --userid userid --remotecontrol"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log(" --userid [userid] - The user identifier.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --fullrights - Allow full rights over this device.");
- console.log(" --remotecontrol - Allow device remote control operations.");
- console.log(" --agentconsole - Allow agent console operations.");
- console.log(" --serverfiles - Allow access to group server files.");
- console.log(" --wakedevices - Allow device wake operation.");
- console.log(" --notes - Allow editing of device notes.");
- console.log(" --desktopviewonly - Restrict user to view-only remote desktop.");
- console.log(" --limiteddesktop - Limit remote desktop keys.");
- console.log(" --noterminal - Hide the terminal tab from this user.");
- console.log(" --nofiles - Hide the files tab from this user.");
- console.log(" --noamt - Hide the Intel AMT tab from this user.");
- console.log(" --limitedevents - User can only see his own events.");
- console.log(" --chatnotify - Allow chat and notification options.");
- console.log(" --uninstall - Allow remote uninstall of the agent.");
- break;
- }
- case 'removeuserfromdevice': {
- console.log("Remove a user from a device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl RemoveuserFromDeviceGroup --id 'deviceid' --userid userid"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log(" --userid [userid] - The user identifier.");
- break;
- }
- case 'broadcast': {
- console.log("Display a message to one or all logged in users, Example usages:\r\n");
- console.log(" MeshCtrl Broadcast --msg \"This is a test\"");
- console.log("\r\nRequired arguments:\r\n");
- console.log(" --msg [message] - Message to display.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --user [userid] - Send the message to the specified user.");
- break;
- }
- case 'deviceinfo': {
- console.log("Display information about a device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceInfo --id 'deviceid'"));
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceInfo --id 'deviceid' --json"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --raw - Output raw data in JSON format.");
- console.log(" --json - Give results in JSON format.");
- break;
- }
- case 'removedevice': {
- console.log("Delete a device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl RemoveDevice --id 'deviceid'"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- break;
- }
- case 'addlocaldevice': {
- console.log("Add a Local Device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl AddLocalDevice --id 'meshid' --devicename 'devicename' --hostname 'hostname'"));
- console.log(winRemoveSingleQuotes(" MeshCtrl AddLocalDevice --id 'meshid' --devicename 'devicename' --hostname 'hostname' --type 6"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [meshid] - The mesh identifier.");
- console.log(" --devicename [devicename] - The device name.");
- console.log(" --hostname [hostname] - The devices hostname or ip address.");
- } else {
- console.log(" --id '[meshid]' - The mesh identifier.");
- console.log(" --devicename '[devicename]' - The device name.");
- console.log(" --hostname '[hostname]' - The devices hostname or ip address.");
- }
-
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --type [TypeNumber] - With the following choices:");
- console.log(" type 4 - Default, Windows (RDP)");
- console.log(" type 6 - Linux (SSH/SCP/VNC)");
- console.log(" type 29 - macOS (SSH/SCP/VNC)");
- break;
- }
- case 'addamtdevice': {
- console.log("Add an Intel AMT Device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl AddAmtDevice --id 'meshid' --devicename 'devicename' --hostname 'hostname --user 'admin' --pass 'admin'"));
- console.log(winRemoveSingleQuotes(" MeshCtrl AddAmtDevice --id 'meshid' --devicename 'devicename' --hostname 'hostname --user 'admin' --pass 'admin' --notls"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [meshid] - The mesh identifier.");
- console.log(" --devicename [devicename] - The device name.");
- console.log(" --hostname [hostname] - The devices hostname or ip address.");
- console.log(" --user [user] - The devices AMT username.");
- console.log(" --pass [pass] - The devices AMT password.");
- console.log("")
- } else {
- console.log(" --id '[meshid]' - The mesh identifier.");
- console.log(" --devicename '[devicename]' - The device name.");
- console.log(" --hostname '[hostname]' - The devices hostname or ip address.");
- console.log(" --user '[user]' - The devices AMT username.");
- console.log(" --pass '[pass]' - The devices AMT password.");
- }
- console.log("\r\nOptional arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --notls - Use No TLS Security.");
- } else {
- console.log(" --notls - Use No TLS Security.");
- }
- break;
- }
- case 'editdevice': {
- console.log("Change information about a device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl EditDevice --id 'deviceid' --name 'device1'"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log("\r\nOptional arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --name [name] - Change device name.");
- console.log(" --desc [description] - Change device description.");
- console.log(" --tags [tag1,tags2] - Change device tags.");
- } else {
- console.log(" --name '[name]' - Change device name.");
- console.log(" --desc '[description]' - Change device description.");
- console.log(" --tags '[tag1,tags2]' - Change device tags.");
- }
- console.log(" --icon [number] - Change the device icon (1 to 8).");
- console.log(" --consent [flags] - Sum of the following numbers:");
- console.log(" 1 = Desktop notify 2 = Terminal notify");
- console.log(" 4 = Files notify 8 = Desktop prompt");
- console.log(" 16 = Terminal prompt 32 = Files prompt");
- console.log(" 64 = Desktop privacy bar");
- break;
- }
- case 'runcommand': {
- console.log("Run a shell command on a remote device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl RunCommand --id 'deviceid' --run \"command\""));
- console.log(winRemoveSingleQuotes(" MeshCtrl RunCommand --id 'deviceid' --run \"command\" --powershell"));
- console.log(winRemoveSingleQuotes(" MeshCtrl RunCommand --id 'deviceid' --run \"command\" --reply"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log(" --run \"[command]\" - Shell command to execute on the remote device.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --powershell - Run in Windows PowerShell.");
- console.log(" --runasuser - Attempt to run the command as logged in user.");
- console.log(" --runasuseronly - Only run the command as the logged in user.");
- console.log(" --reply - Return with the output from running the command.");
- break;
- }
- case 'shell': {
- console.log("Access a command shell on a remote device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl Shell --id 'deviceid'"));
- console.log(winRemoveSingleQuotes(" MeshCtrl Shell --id 'deviceid' --powershell"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --powershell - Run a Windows PowerShell.");
- break;
- }
- case 'devicepower': {
- console.log("Perform power operations on remote devices, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl DevicePower --wake --id 'deviceid'"));
- console.log(winRemoveSingleQuotes(" MeshCtrl DevicePower --sleep --id 'deviceid'"));
- console.log(winRemoveSingleQuotes(" MeshCtrl DevicePower --reset --id 'deviceid'"));
- console.log(winRemoveSingleQuotes(" MeshCtrl DevicePower --off --id 'deviceid1,deviceid2'"));
- console.log("\r\nNote that some power operations may take up to a minute to execute.\r\n");
- console.log("Required arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid1,deviceid2] - Device identifiers.");
- } else {
- console.log(" --id '[deviceid1,deviceid2]' - Device identifiers.");
- }
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --wake - Attempt to wake up the remote device.");
- console.log(" --reset - Attempt to remote the remote device.");
- console.log(" --sleep - Attempt to place the remote device in low power mode.");
- console.log(" --off - Attempt to power off the remote device.");
- console.log(" --amtoff - Attempt to power off the remote device using Intel AMT.");
- console.log(" --amton - Attempt to power on the remote device using Intel AMT.");
- console.log(" --amtreset - Attempt to reset the remote device using Intel AMT.");
- break;
- }
- case 'devicesharing': {
- var tzoffset = (new Date()).getTimezoneOffset() * 60000; // Offset in milliseconds
- var localISOTime = (new Date(Date.now() - tzoffset)).toISOString().slice(0, -5);
- console.log("List sharing links for a specified device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid'"));
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --remove abcdef"));
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --add Guest --start " + localISOTime + " --duration 30"));
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --add Guest --start " + localISOTime + " --duration 30 --daily"));
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --add Guest --type desktop,terminal --consent prompt"));
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --add Guest --type http --port 80"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --remove [shareid] - Remove a device sharing link.");
- console.log(" --add [guestname] - Add a device sharing link.");
- console.log(" --type [desktop,terminal,files,http,https] - Type of sharing to add, can be combined. default is desktop.");
- console.log(" --viewonly - Make desktop sharing view only.");
- console.log(" --consent [notify,prompt,none] - Consent flags, default is notify.");
- console.log(" --start [yyyy-mm-ddThh:mm:ss] - Start time, default is now.");
- console.log(" --end [yyyy-mm-ddThh:mm:ss] - End time.");
- console.log(" --duration [minutes] - Duration of the share, default is 60 minutes.");
- console.log(" --daily - Add recurring daily device share.");
- console.log(" --weekly - Add recurring weekly device share.");
- console.log(" --port [portnumber] - Set alternative port for http or https, default is 80 for http and 443 for https.");
- break;
- }
- case 'agentdownload': {
- console.log("Download an agent of a specific type for a given device group, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl AgentDownload --id 'groupid' --type 3"));
- console.log(winRemoveSingleQuotes(" MeshCtrl AgentDownload --id 'groupid' --type 3 --installflags 1"));
- console.log("\r\nRequired arguments:\r\n");
- console.log(" --type [ArchitectureNumber] - Agent architecture number.");
- if (process.platform == 'win32') {
- console.log(" --id [groupid] - The device group identifier.");
- } else {
- console.log(" --id '[groupid]' - The device group identifier.");
- }
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --installflags [InstallFlagsNumber] - With the following choices:");
- console.log(" installflags 0 - Default, Interactive & Background, offers connect button & install/uninstall");
- console.log(" installflags 1 - Interactive only, offers only connect button, not install/uninstall");
- console.log(" installflags 2 - Background only, offers only install/uninstall, not connect");
- break;
- }
- case 'upload': {
- console.log("Upload a local file to a remote device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl Upload --id 'deviceid' --file sample.txt --target c:\\"));
- console.log(winRemoveSingleQuotes(" MeshCtrl Upload --id 'deviceid' --file sample.txt --target /tmp"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log(" --file [localfile] - The local file to upload.");
- console.log(" --target [remotepath] - The remote path to upload the file to.");
- break;
- }
- case 'download': {
- console.log("Download a file from a remote device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl Download --id 'deviceid' --file C:\\sample.txt --target c:\\temp"));
- console.log(winRemoveSingleQuotes(" MeshCtrl Download --id 'deviceid' --file /tmp/sample.txt --target /tmp"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log(" --file [remotefile] - The remote file to download.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --target [localpath] - The local path to download the file to.");
- break;
- }
- case 'webrelay': {
- console.log("Generate a webrelay URL to access a HTTP/HTTPS service on a remote device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl WebRelay --id 'deviceid' --type http --port 80"));
- console.log(winRemoveSingleQuotes(" MeshCtrl WebRelay --id 'deviceid' --type https --port 443"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log(" --type [http,https] - Type of relay from remote device, http or https.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --port [portnumber] - Set alternative port for http or https, default is 80 for http and 443 for https.");
- break;
- }
- case 'deviceopenurl': {
- console.log("Open a web page on a remote device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceOpenUrl --id 'deviceid' --openurl http://meshcentral.com"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log(" --openurl [url] - Link to the web page.");
- break;
- }
- case 'devicemessage': {
- console.log("Display a message on the remote device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceMessage --id 'deviceid' --msg \"message\""));
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceMessage --id 'deviceid' --msg \"message\" --title \"title\""));
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceMessage --id 'deviceid' --msg \"message\" --title \"title\" --timeout 120000"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log(" --msg [message] - The message to display.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --title [title] - Messagebox title, default is \"MeshCentral\".");
- console.log(" --timeout [miliseconds] - After timeout messagebox vanishes, 0 keeps messagebox open until closed manually, default is 120000 (2 Minutes).");
- break;
- }
- case 'devicetoast': {
- console.log("Display a toast message on the remote device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceToast --id 'deviceid' --msg \"message\""));
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceToast --id 'deviceid' --msg \"message\" --title \"title\""));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log(" --msg [message] - The message to display.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --title [title] - Toast title, default is \"MeshCentral\".");
- break;
- }
- case 'groupmessage': {
- console.log("Open a message box on remote devices in a specific device group, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl GroupMessage --id 'devicegroupid' --msg \"message\""));
- console.log(winRemoveSingleQuotes(" MeshCtrl GroupMessage --id 'devicegroupid' --msg \"message\" --title \"title\""));
- console.log(winRemoveSingleQuotes(" MeshCtrl GroupMessage --id 'devicegroupid' --msg \"message\" --title \"title\" --timeout 120000"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [devicegroupid] - The device identifier.");
- } else {
- console.log(" --id '[devicegroupid]' - The device identifier.");
- }
- console.log(" --msg [message] - The message to display.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --title [title] - Messagebox title, default is \"MeshCentral\".");
- console.log(" --timeout [miliseconds] - After timeout messagebox vanishes, 0 keeps messagebox open until closed manually, default is 120000 (2 Minutes).");
- break;
- }
- case 'grouptoast': {
- console.log("Display a toast notification on remote devices in a specific device group, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl GroupToast --id 'devicegroupid' --msg \"message\""));
- console.log(winRemoveSingleQuotes(" MeshCtrl GroupToast --id 'devicegroupid' --msg \"message\" --title \"title\""));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [devicegroupid] - The device identifier.");
- } else {
- console.log(" --id '[devicegroupid]' - The device identifier.");
- }
- console.log(" --msg [message] - The message to display.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --title [title] - Toast title, default is \"MeshCentral\".");
- break;
- }
- case 'report': {
- console.log("Generate a CSV report, Example usages:\r\n");
- console.log(" MeshCtrl Report --type sessions --devicegroup mesh//...");
- console.log(" MeshCtrl Report --type traffic --json");
- console.log(" MeshCtrl Report --type logins --groupby day");
- console.log(" MeshCtrl Report --type db");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --start [yyyy-mm-ddThh:mm:ss] - Filter the results starting at that date. Defaults to last 24h and last week when used with --groupby day. Usable with sessions, traffic and logins");
- console.log(" --end [yyyy-mm-ddThh:mm:ss] - Filter the results ending at that date. Defaults to now. Usable with sessions, traffic and logins");
- console.log(" --groupby [name] - How to group results. Options: user, day, device. Defaults to user. User and day usable in sessions and logins, device usable in sessions.");
- console.log(" --devicegroup [devicegroupid] - Filter the results by device group. Usable in sessions");
- console.log(" --showtraffic - Add traffic data in sessions report");
- break;
- }
- default: {
- console.log("Get help on an action. Type:\r\n\r\n help [action]\r\n\r\nPossible actions are: " + possibleCommands.join(', ') + '.');
- }
- }
- }
- break;
- }
- }
-
- if (ok) {
- if(args.loginpass===true){
- const readline = require('readline');
- const rl = readline.createInterface({
- input: process.stdin,
- output: process.stdout,
- terminal: false
- });
- process.stdout.write('Enter your password: ');
- const stdin = process.openStdin();
- stdin.setRawMode(true); // Set raw mode to prevent echoing of characters
- stdin.resume();
- args.loginpass = '';
- process.stdin.on('data', (char) => {
- char = char + '';
- switch (char) {
- case '\n':
- case '\r':
- case '\u0004': // They've finished entering their password
- stdin.setRawMode(false);
- stdin.pause();
- process.stdout.clearLine(); process.stdout.cursorTo(0);
- rl.close();
- serverConnect();
- break;
- case '\u0003': // Ctrl+C
- process.stdout.write('\n');
- process.exit();
- break;
- default: // Mask the password with "*"
- args.loginpass += char;
- process.stdout.clearLine(); process.stdout.cursorTo(0);
- process.stdout.write('Enter your password: ' + '*'.repeat(args.loginpass.length));
- break;
- }
- });
- }else{
- serverConnect();
- }
- }
-}
-
-function displayConfigHelp() {
- console.log("Perform operations on the config.json file. Example usage:\r\n");
- console.log(" MeshCtrl config --show");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --show - Display the config.json file.");
- console.log(" --listdomains - Display non-default domains.");
- console.log(" --adddomain [domain] - Add a domain.");
- console.log(" --removedomain [domain] - Remove a domain.");
- console.log(" --settodomain [domain] - Set values to the domain.");
- console.log(" --removefromdomain [domain] - Remove values from the domain.");
- console.log("\r\nWith adddomain, removedomain, settodomain and removefromdomain you can add the key and value pair. For example:\r\n");
- console.log(" --adddomain \"MyDomain\" --title \"My Server Name\" --newAccounts false");
- console.log(" --settodomain \"MyDomain\" --title \"My Server Name\"");
- console.log(" --removefromdomain \"MyDomain\" --title");
-}
-
-function performConfigOperations(args) {
- var domainValues = ['title', 'title2', 'titlepicture', 'trustedcert', 'welcomepicture', 'welcometext', 'userquota', 'meshquota', 'newaccounts', 'usernameisemail', 'newaccountemaildomains', 'newaccountspass', 'newaccountsrights', 'geolocation', 'lockagentdownload', 'userconsentflags', 'Usersessionidletimeout', 'auth', 'ldapoptions', 'ldapusername', 'ldapuserbinarykey', 'ldapuseremail', 'footer', 'certurl', 'loginKey', 'userallowedip', 'agentallowedip', 'agentnoproxy', 'agentconfig', 'orphanagentuser', 'httpheaders', 'yubikey', 'passwordrequirements', 'limits', 'amtacmactivation', 'redirects', 'sessionrecording', 'hide'];
- var domainObjectValues = ['ldapoptions', 'httpheaders', 'yubikey', 'passwordrequirements', 'limits', 'amtacmactivation', 'redirects', 'sessionrecording'];
- var domainArrayValues = ['newaccountemaildomains', 'newaccountsrights', 'loginkey', 'agentconfig'];
- var configChange = false;
- var fs = require('fs');
- var path = require('path');
- var configFile = 'config.json';
- var didSomething = 0;
- if (fs.existsSync(configFile) == false) { configFile = path.join('meshcentral-data', 'config.json'); }
- if (fs.existsSync(configFile) == false) { configFile = path.join(__dirname, 'config.json'); }
- if (fs.existsSync(configFile) == false) { configFile = path.join(__dirname, 'meshcentral-data', 'config.json'); }
- if (fs.existsSync(configFile) == false) { configFile = path.join(__dirname, '..', 'meshcentral-data', 'config.json'); }
- if (fs.existsSync(configFile) == false) { configFile = path.join(__dirname, '..', '..', 'meshcentral-data', 'config.json'); }
- if (fs.existsSync(configFile) == false) { console.log("Unable to find config.json."); return; }
- var config = null;
- try { config = fs.readFileSync(configFile).toString('utf8'); } catch (ex) { console.log("Error: Unable to read config.json"); return; }
- try { config = JSON.parse(fs.readFileSync(configFile)); } catch (e) { console.log('ERROR: Unable to parse ' + configFile + '.'); return null; }
- if (args.adddomain != null) {
- didSomething++;
- if (config.domains == null) { config.domains = {}; }
- if (config.domains[args.adddomain] != null) { console.log("Error: Domain \"" + args.adddomain + "\" already exists"); }
- else {
- configChange = true;
- config.domains[args.adddomain] = {};
- for (var i in args) {
- if (domainValues.indexOf(i.toLowerCase()) >= 0) {
- if (args[i] == 'true') { args[i] = true; } else if (args[i] == 'false') { args[i] = false; } else if (parseInt(args[i]) == args[i]) { args[i] = parseInt(args[i]); }
- config.domains[args.adddomain][i] = args[i];
- configChange = true;
- }
- }
- }
- }
- if (args.removedomain != null) {
- didSomething++;
- if (config.domains == null) { config.domains = {}; }
- if (config.domains[args.removedomain] == null) { console.log("Error: Domain \"" + args.removedomain + "\" does not exist"); }
- else { delete config.domains[args.removedomain]; configChange = true; }
- }
- if (args.settodomain != null) {
- didSomething++;
- if (config.domains == null) { config.domains = {}; }
- if (args.settodomain == true) { args.settodomain = ''; }
- if (config.domains[args.settodomain] == null) { console.log("Error: Domain \"" + args.settodomain + "\" does not exist"); }
- else {
- for (var i in args) {
- if ((i == '_') || (i == 'settodomain')) continue;
- if (domainValues.indexOf(i.toLowerCase()) >= 0) {
- var isObj = (domainObjectValues.indexOf(i.toLowerCase()) >= 0);
- var isArr = (domainArrayValues.indexOf(i.toLowerCase()) >= 0);
- if ((isObj == false) && (isArr == false)) {
- // Simple value set
- if (args[i] == '') { delete config.domains[args.settodomain][i]; configChange = true; } else {
- if (args[i] == 'true') { args[i] = true; } else if (args[i] == 'false') { args[i] = false; } else if (parseInt(args[i]) == args[i]) { args[i] = parseInt(args[i]); }
- config.domains[args.settodomain][i] = args[i];
- configChange = true;
- }
- } else if (isObj || isArr) {
- // Set an object/array value
- if (args[i] == '') { delete config.domains[args.settodomain][i]; configChange = true; } else {
- var x = null;
- try { x = JSON.parse(args[i]); } catch (ex) { }
- if ((x == null) || (typeof x != 'object')) { console.log("Unable to parse JSON for " + i + "."); } else {
- if (isArr && Array.isArray(x) == false) {
- console.log("Value " + i + " must be an array.");
- } else if (!isArr && Array.isArray(x) == true) {
- console.log("Value " + i + " must be an object.");
- } else {
- config.domains[args.settodomain][i] = x;
- configChange = true;
- }
- }
- }
- }
- } else {
- console.log('Invalid configuration value: ' + i);
- }
- }
- }
- }
- if (args.removefromdomain != null) {
- didSomething++;
- if (config.domains == null) { config.domains = {}; }
- if (config.domains[args.removefromdomain] == null) { console.log("Error: Domain \"" + args.removefromdomain + "\" does not exist"); }
- else { for (var i in args) { if (domainValues.indexOf(i.toLowerCase()) >= 0) { delete config.domains[args.removefromdomain][i]; configChange = true; } } }
- }
- if (configChange) {
- try { fs.writeFileSync(configFile, JSON.stringify(config, null, 2)); } catch (ex) { console.log("Error: Unable to read config.json"); return; }
- }
- if (args.show == 1) {
- console.log(JSON.stringify(config, null, 2)); return;
- } else if (args.listdomains == 1) {
- if (config.domains == null) {
- console.log('No domains found.'); return;
- } else {
- // Show the list of active domains, skip the default one.
- for (var i in config.domains) { if ((i != '') && (i[0] != '_')) { console.log(i); } } return;
- }
- } else {
- if (didSomething == 0) {
- displayConfigHelp();
- } else {
- console.log("Done.");
- }
- }
-}
-
-function onVerifyServer(clientName, certs) { return null; }
-
-function serverConnect() {
- const WebSocket = require('ws');
-
- var url = 'wss://localhost/control.ashx';
- if (args.url) {
- url = args.url;
- if (url.length < 5) { console.log("Invalid url."); process.exit(); return; }
- if ((url.startsWith('wss://') == false) && (url.startsWith('ws://') == false)) { console.log("Invalid url."); process.exit(); return; }
- var i = url.indexOf('?key='), loginKey = null;
- if (i >= 0) { loginKey = url.substring(i + 5); url = url.substring(0, i); }
- if (url.endsWith('/') == false) { url += '/'; }
- url += 'control.ashx';
- if (loginKey != null) { url += '?key=' + loginKey; }
- }
-
- // TODO: checkServerIdentity does not work???
- var options = { rejectUnauthorized: false, checkServerIdentity: onVerifyServer }
-
- // Setup the HTTP proxy if needed
- if (args.proxy != null) {
- const HttpsProxyAgent = require('https-proxy-agent');
- options.agent = new HttpsProxyAgent(require('url').parse(args.proxy));
- }
-
- // Password authentication
- if (args.loginpass != null) {
- var username = 'admin';
- if (args.loginuser != null) { username = args.loginuser; }
- var token = '';
- if (args.token != null) { token = ',' + Buffer.from('' + args.token).toString('base64'); }
- options.headers = { 'x-meshauth': Buffer.from('' + username).toString('base64') + ',' + Buffer.from('' + args.loginpass).toString('base64') + token }
- }
-
- // Cookie authentication
- var ckey = null, loginCookie = null;
- if (args.loginkey != null) {
- // User key passed in as argument hex
- if (args.loginkey.length != 160) { loginCookie = args.loginkey; }
- ckey = Buffer.from(args.loginkey, 'hex');
- if (ckey.length != 80) { ckey = null; loginCookie = args.loginkey; }
- } else if (args.loginkeyfile != null) {
- // Load key from hex file
- var fs = require('fs');
- try {
- var keydata = fs.readFileSync(args.loginkeyfile, 'utf8').split(' ').join('').split('\r').join('').split('\n').join('');
- ckey = Buffer.from(keydata, 'hex');
- if (ckey.length != 80) { ckey = null; loginCookie = args.loginkey; }
- } catch (ex) { console.log(ex.message); process.exit(); return; }
- }
-
- settings.xxurl = url;
- if (ckey != null) {
- var domainid = '', username = 'admin';
- if (args.logindomain != null) { domainid = args.logindomain; }
- if (args.loginuser != null) { username = args.loginuser; }
- url += (url.indexOf('?key=') >= 0 ? '&auth=' : '?auth=') + encodeCookie({ userid: 'user/' + domainid + '/' + username, domainid: domainid }, ckey);
- } else {
- if (args.logindomain != null) { console.log("--logindomain can only be used along with --loginkey."); process.exit(); return; }
- if (loginCookie != null) { url += (url.indexOf('?key=') >= 0 ? '&auth=' : '?auth=') + loginCookie; }
- }
-
- const ws = new WebSocket(url, options);
- //console.log('Connecting to ' + url);
-
- ws.on('open', function open() {
- //console.log('Connected.');
- switch (settings.cmd) {
- case 'serverinfo': { break; }
- case 'userinfo': { break; }
- case 'listusers': { ws.send(JSON.stringify({ action: 'users', responseid: 'meshctrl' })); break; }
- case 'listusersessions': { ws.send(JSON.stringify({ action: 'wssessioncount', responseid: 'meshctrl' })); break; }
- case 'removeallusersfromusergroup':
- case 'listusergroups': { ws.send(JSON.stringify({ action: 'usergroups', responseid: 'meshctrl' })); break; }
- case 'listdevicegroups': { ws.send(JSON.stringify({ action: 'meshes', responseid: 'meshctrl' })); break; }
- case 'listusersofdevicegroup': { ws.send(JSON.stringify({ action: 'meshes', responseid: 'meshctrl' })); break; }
- case 'listdevices': {
- if (args.details) {
- // Get list of devices with lots of details
- ws.send(JSON.stringify({ action: 'getDeviceDetails', type: (args.csv) ? 'csv' : 'json' }));
- } else if (args.group) {
- ws.send(JSON.stringify({ action: 'nodes', meshname: args.group, responseid: 'meshctrl' }));
- } else if (args.id) {
- ws.send(JSON.stringify({ action: 'nodes', meshid: args.id, responseid: 'meshctrl' }));
- } else {
- ws.send(JSON.stringify({ action: 'meshes' }));
- ws.send(JSON.stringify({ action: 'nodes', responseid: 'meshctrl' }));
- }
- break;
- }
- case 'listevents': {
- limit = null;
- if (args.limit) { limit = parseInt(args.limit); }
- if ((typeof limit != 'number') || (limit < 1)) { limit = null; }
-
- var cmd = null;
- if (args.userid) {
- cmd = { action: 'events', user: args.userid, responseid: 'meshctrl' };
- } else if (args.id) {
- cmd = { action: 'events', nodeid: args.id, responseid: 'meshctrl' };
- } else {
- cmd = { action: 'events', responseid: 'meshctrl' };
- }
- if (typeof limit == 'number') { cmd.limit = limit; }
- ws.send(JSON.stringify(cmd));
- break;
- }
- case 'logintokens': {
- if (args.add) {
- var cmd = { action: 'createLoginToken', name: args.add, expire: 0, responseid: 'meshctrl' };
- if (args.expire) { cmd.expire = parseInt(args.expire); }
- ws.send(JSON.stringify(cmd));
- } else {
- var cmd = { action: 'loginTokens', responseid: 'meshctrl' };
- if (args.remove) { cmd.remove = [args.remove]; }
- ws.send(JSON.stringify(cmd));
- }
- break;
- }
- case 'adduser': {
- var siteadmin = getSiteAdminRights(args);
- if (args.randompass) { args.pass = getRandomAmtPassword(); }
- var op = { action: 'adduser', username: args.user, pass: args.pass, responseid: 'meshctrl' };
- if (args.email) { op.email = args.email; if (args.emailverified) { op.emailVerified = true; } }
- if (args.resetpass) { op.resetNextLogin = true; }
- if (siteadmin != -1) { op.siteadmin = siteadmin; }
- if (args.domain) { op.domain = args.domain; }
- if (args.phone === true) { op.phone = ''; }
- if (typeof args.phone == 'string') { op.phone = args.phone; }
- if (typeof args.realname == 'string') { op.realname = args.realname; }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'edituser': {
- var userid = args.userid;
- if ((args.domain != null) && (userid.indexOf('/') < 0)) { userid = 'user/' + args.domain + '/' + userid; }
- var siteadmin = getSiteAdminRights(args);
- var op = { action: 'edituser', userid: userid, responseid: 'meshctrl' };
- if (args.email) { op.email = args.email; if (args.emailverified) { op.emailVerified = true; } }
- if (args.resetpass) { op.resetNextLogin = true; }
- if (siteadmin != -1) { op.siteadmin = siteadmin; }
- if (args.domain) { op.domain = args.domain; }
- if (args.phone === true) { op.phone = ''; }
- if (typeof args.phone == 'string') { op.phone = args.phone; }
- if (typeof args.realname == 'string') { op.realname = args.realname; }
- if (args.realname === true) { op.realname = ''; }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'removeuser': {
- var userid = args.userid;
- if ((args.domain != null) && (userid.indexOf('/') < 0)) { userid = 'user/' + args.domain + '/' + userid; }
- ws.send(JSON.stringify({ action: 'deleteuser', userid: userid, responseid: 'meshctrl' }));
- break;
- }
- case 'addusergroup': {
- var op = { action: 'createusergroup', name: args.name, desc: args.desc, responseid: 'meshctrl' };
- if (args.domain) { op.domain = args.domain; }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'removeusergroup': {
- var ugrpid = args.groupid;
- if ((args.domain != null) && (userid.indexOf('/') < 0)) { ugrpid = 'ugrp/' + args.domain + '/' + ugrpid; }
- ws.send(JSON.stringify({ action: 'deleteusergroup', ugrpid: ugrpid, responseid: 'meshctrl' }));
- break;
- }
- case 'addtousergroup': {
- var ugrpid = args.groupid;
- if ((args.domain != null) && (userid.indexOf('/') < 0)) { ugrpid = 'ugrp/' + args.domain + '/' + ugrpid; }
-
- // Add a user to a user group
- if (args.userid != null) {
- var userid = args.userid;
- if ((args.domain != null) && (userid.indexOf('/') < 0)) { userid = 'user/' + args.domain + '/' + userid; }
- ws.send(JSON.stringify({ action: 'addusertousergroup', ugrpid: ugrpid, usernames: [userid.split('/')[2]], responseid: 'meshctrl' }));
- break;
- }
-
- if ((args.id != null) && (args.id.startsWith('user/'))) {
- ws.send(JSON.stringify({ action: 'addusertousergroup', ugrpid: ugrpid, usernames: [args.id.split('/')[2]], responseid: 'meshctrl' }));
- break;
- }
-
- var rights = 0;
- if (args.rights != null) { rights = parseInt(args.rights); }
-
- // Add a device group to a user group
- if (args.meshid != null) {
- var meshid = args.meshid;
- if ((args.domain != null) && (userid.indexOf('/') < 0)) { meshid = 'mesh/' + args.domain + '/' + meshid; }
- ws.send(JSON.stringify({ action: 'addmeshuser', meshid: meshid, userid: ugrpid, meshadmin: rights, responseid: 'meshctrl' }));
- break;
- }
-
- if ((args.id != null) && (args.id.startsWith('mesh/'))) {
- ws.send(JSON.stringify({ action: 'addmeshuser', meshid: args.id, userid: ugrpid, meshadmin: rights, responseid: 'meshctrl' }));
- break;
- }
-
- // Add a device to a user group
- if (args.nodeid != null) {
- var nodeid = args.nodeid;
- if ((args.domain != null) && (userid.indexOf('/') < 0)) { nodeid = 'node/' + args.domain + '/' + nodeid; }
- ws.send(JSON.stringify({ action: 'adddeviceuser', nodeid: nodeid, userids: [ugrpid], rights: rights, responseid: 'meshctrl' }));
- break;
- }
-
- if ((args.id != null) && (args.id.startsWith('node/'))) {
- ws.send(JSON.stringify({ action: 'adddeviceuser', nodeid: args.id, userids: [ugrpid], rights: rights, responseid: 'meshctrl' }));
- break;
- }
-
- break;
- }
- case 'removefromusergroup': {
- var ugrpid = args.groupid;
- if ((args.domain != null) && (userid.indexOf('/') < 0)) { ugrpid = 'ugrp/' + args.domain + '/' + ugrpid; }
-
- // Remove a user from a user group
- if (args.userid != null) {
- var userid = args.userid;
- if ((args.domain != null) && (userid.indexOf('/') < 0)) { userid = 'user/' + args.domain + '/' + userid; }
- ws.send(JSON.stringify({ action: 'removeuserfromusergroup', ugrpid: ugrpid, userid: userid, responseid: 'meshctrl' }));
- break;
- }
-
- if ((args.id != null) && (args.id.startsWith('user/'))) {
- ws.send(JSON.stringify({ action: 'removeuserfromusergroup', ugrpid: ugrpid, userid: args.id, responseid: 'meshctrl' }));
- break;
- }
-
- // Remove a device group from a user group
- if (args.meshid != null) {
- var meshid = args.meshid;
- if ((args.domain != null) && (userid.indexOf('/') < 0)) { meshid = 'mesh/' + args.domain + '/' + meshid; }
- ws.send(JSON.stringify({ action: 'removemeshuser', meshid: meshid, userid: ugrpid, responseid: 'meshctrl' }));
- break;
- }
-
- if ((args.id != null) && (args.id.startsWith('mesh/'))) {
- ws.send(JSON.stringify({ action: 'removemeshuser', meshid: args.id, userid: ugrpid, responseid: 'meshctrl' }));
- break;
- }
-
- // Remove a device from a user group
- if (args.nodeid != null) {
- var nodeid = args.nodeid;
- if ((args.domain != null) && (userid.indexOf('/') < 0)) { nodeid = 'node/' + args.domain + '/' + nodeid; }
- ws.send(JSON.stringify({ action: 'adddeviceuser', nodeid: nodeid, userids: [ugrpid], rights: 0, responseid: 'meshctrl', remove: true }));
- break;
- }
-
- if ((args.id != null) && (args.id.startsWith('node/'))) {
- ws.send(JSON.stringify({ action: 'adddeviceuser', nodeid: args.id, userids: [ugrpid], rights: 0, responseid: 'meshctrl', remove: true }));
- break;
- }
-
- break;
- }
- case 'adddevicegroup': {
- var op = { action: 'createmesh', meshname: args.name, meshtype: 2, responseid: 'meshctrl' };
- if (args.desc) { op.desc = args.desc; }
- if (args.amtonly) { op.meshtype = 1; }
- if (args.agentless) { op.meshtype = 3; }
- if (args.features) { op.flags = parseInt(args.features); }
- if (args.consent) { op.consent = parseInt(args.consent); }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'removedevicegroup': {
- var op = { action: 'deletemesh', responseid: 'meshctrl' };
- if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'addamtdevice': {
- var op = { action: 'addamtdevice', amttls: 1, responseid: 'meshctrl' };
- if (args.id) { op.meshid = args.id; }
- if ((typeof args.devicename == 'string') && (args.devicename != '')) { op.devicename = args.devicename; }
- if ((typeof args.hostname == 'string') && (args.hostname != '')) { op.hostname = args.hostname; }
- if ((typeof args.user == 'string') && (args.user != '')) { op.amtusername = args.user; }
- if ((typeof args.pass == 'string') && (args.pass != '')) { op.amtpassword = args.pass; }
- if (args.notls) { op.amttls = 0; }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'addlocaldevice': {
- var op = { action: 'addlocaldevice', type: 4, responseid: 'meshctrl' };
- if (args.id) { op.meshid = args.id; }
- if ((typeof args.devicename == 'string') && (args.devicename != '')) { op.devicename = args.devicename; }
- if ((typeof args.hostname == 'string') && (args.hostname != '')) { op.hostname = args.hostname; }
- if (args.type) {
- if ((typeof parseInt(args.type) != 'number') || isNaN(parseInt(args.type))) { console.log("Invalid type."); process.exit(1); return; }
- op.type = args.type;
- }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'editdevicegroup': {
- var op = { action: 'editmesh', responseid: 'meshctrl' };
- if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshidname = args.group; }
- if ((typeof args.name == 'string') && (args.name != '')) { op.meshname = args.name; }
- if (args.desc === true) { op.desc = ""; } else if (typeof args.desc == 'string') { op.desc = args.desc; }
- if (args.invitecodes === true) { op.invite = "*"; } else if (typeof args.invitecodes == 'string') {
- var invitecodes = args.invitecodes.split(','), invitecodes2 = [];
- for (var i in invitecodes) { if (invitecodes[i].length > 0) { invitecodes2.push(invitecodes[i]); } }
- if (invitecodes2.length > 0) {
- op.invite = { codes: invitecodes2, flags: 0 };
- if (args.backgroundonly === true) { op.invite.flags = 2; }
- else if (args.interactiveonly === true) { op.invite.flags = 1; }
- }
- }
- if (args.flags != null) {
- var flags = parseInt(args.flags);
- if (typeof flags == 'number') { op.flags = flags; }
- }
- if (args.consent != null) {
- var consent = parseInt(args.consent);
- if (typeof consent == 'number') { op.consent = consent; }
- }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'movetodevicegroup': {
- var op = { action: 'changeDeviceMesh', responseid: 'meshctrl', nodeids: [args.devid] };
- if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'addusertodevicegroup': {
- var meshrights = 0;
- if (args.fullrights) { meshrights = 0xFFFFFFFF; }
- if (args.editgroup) { meshrights |= 1; }
- if (args.manageusers) { meshrights |= 2; }
- if (args.managedevices) { meshrights |= 4; }
- if (args.remotecontrol) { meshrights |= 8; }
- if (args.agentconsole) { meshrights |= 16; }
- if (args.serverfiles) { meshrights |= 32; }
- if (args.wakedevices) { meshrights |= 64; }
- if (args.notes) { meshrights |= 128; }
- if (args.desktopviewonly) { meshrights |= 256; }
- if (args.noterminal) { meshrights |= 512; }
- if (args.nofiles) { meshrights |= 1024; }
- if (args.noamt) { meshrights |= 2048; }
- if (args.limiteddesktop) { meshrights |= 4096; }
- if (args.limitedevents) { meshrights |= 8192; }
- if (args.chatnotify) { meshrights |= 16384; }
- if (args.uninstall) { meshrights |= 32768; }
- var op = { action: 'addmeshuser', usernames: [args.userid], meshadmin: meshrights, responseid: 'meshctrl' };
- if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'removeuserfromdevicegroup': {
- var op = { action: 'removemeshuser', userid: args.userid, responseid: 'meshctrl' };
- if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'addusertodevice': {
- var meshrights = 0;
- if (args.fullrights) { meshrights = (8 + 16 + 32 + 64 + 128 + 16384 + 32768); }
- if (args.remotecontrol) { meshrights |= 8; }
- if (args.agentconsole) { meshrights |= 16; }
- if (args.serverfiles) { meshrights |= 32; }
- if (args.wakedevices) { meshrights |= 64; }
- if (args.notes) { meshrights |= 128; }
- if (args.desktopviewonly) { meshrights |= 256; }
- if (args.noterminal) { meshrights |= 512; }
- if (args.nofiles) { meshrights |= 1024; }
- if (args.noamt) { meshrights |= 2048; }
- if (args.limiteddesktop) { meshrights |= 4096; }
- if (args.limitedevents) { meshrights |= 8192; }
- if (args.chatnotify) { meshrights |= 16384; }
- if (args.uninstall) { meshrights |= 32768; }
- var op = { action: 'adddeviceuser', nodeid: args.id, usernames: [args.userid], rights: meshrights, responseid: 'meshctrl' };
- ws.send(JSON.stringify(op));
- break;
- }
- case 'removeuserfromdevice': {
- var op = { action: 'adddeviceuser', nodeid: args.id, usernames: [args.userid], rights: 0, remove: true, responseid: 'meshctrl' };
- ws.send(JSON.stringify(op));
- break;
- }
- case 'sendinviteemail': {
- var op = { action: 'inviteAgent', email: args.email, name: '', os: '0', responseid: 'meshctrl' }
- if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
- if (args.name) { op.name = args.name; }
- if (args.message) { op.msg = args.message; }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'generateinvitelink': {
- var op = { action: 'createInviteLink', expire: args.hours, flags: 0, responseid: 'meshctrl' }
- if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
- if (args.flags) { op.flags = args.flags; }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'broadcast': {
- var op = { action: 'userbroadcast', msg: args.msg, responseid: 'meshctrl' };
- if (args.user) { op.userid = args.user; }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'showevents': {
- console.log('Connected. Press ctrl-c to end.');
- break;
- }
- case 'deviceinfo': {
- settings.deviceinfocount = 4;
- ws.send(JSON.stringify({ action: 'nodes' }));
- ws.send(JSON.stringify({ action: 'getnetworkinfo', nodeid: args.id, responseid: 'meshctrl' }));
- ws.send(JSON.stringify({ action: 'lastconnect', nodeid: args.id, responseid: 'meshctrl' }));
- ws.send(JSON.stringify({ action: 'getsysinfo', nodeid: args.id, nodeinfo: true, responseid: 'meshctrl' }));
- break;
- }
- case 'removedevice': {
- var op = { action: 'removedevices', nodeids: [ args.id ], responseid: 'meshctrl' };
- ws.send(JSON.stringify(op));
- break;
- }
- case 'editdevice': {
- var op = { action: 'changedevice', nodeid: args.id, responseid: 'meshctrl' };
- if (typeof args.name == 'string') { op.name = args.name; }
- if (typeof args.name == 'number') { op.name = '' + args.name; }
- if (args.desc) { if (args.desc === true) { op.desc = ''; } else if (typeof args.desc == 'string') { op.desc = args.desc; } else if (typeof args.desc == 'number') { op.desc = '' + args.desc; } }
- if (args.tags) { if (args.tags === true) { op.tags = ''; } else if (typeof args.tags == 'string') { op.tags = args.tags.split(','); } else if (typeof args.tags == 'number') { op.tags = '' + args.tags; } }
- if (args.icon) { op.icon = parseInt(args.icon); if ((typeof op.icon != 'number') || isNaN(op.icon) || (op.icon < 1) || (op.icon > 8)) { console.log("Icon must be between 1 and 8."); process.exit(1); return; } }
- if (args.consent) { op.consent = parseInt(args.consent); if ((typeof op.consent != 'number') || isNaN(op.consent) || (op.consent < 1)) { console.log("Invalid consent flags."); process.exit(1); return; } }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'runcommand': {
- var runAsUser = 0;
- if (args.runasuser) { runAsUser = 1; } else if (args.runasuseronly) { runAsUser = 2; }
- var reply = false;
- if (args.reply) { reply = true; }
- ws.send(JSON.stringify({ action: 'runcommands', nodeids: [args.id], type: ((args.powershell) ? 2 : 0), cmds: args.run, responseid: 'meshctrl', runAsUser: runAsUser, reply: reply }));
- break;
- }
- case 'shell':
- case 'upload':
- case 'download': {
- ws.send("{\"action\":\"authcookie\"}");
- break;
- }
- case 'devicepower': {
- var nodes = args.id.split(',');
- if (args.wake) {
- // Wake operation
- ws.send(JSON.stringify({ action: 'wakedevices', nodeids: nodes, responseid: 'meshctrl' }));
- } else if (args.off) {
- // Power off operation
- ws.send(JSON.stringify({ action: 'poweraction', nodeids: nodes, actiontype: 2, responseid: 'meshctrl' }));
- } else if (args.reset) {
- // Reset operation
- ws.send(JSON.stringify({ action: 'poweraction', nodeids: nodes, actiontype: 3, responseid: 'meshctrl' }));
- } else if (args.sleep) {
- // Sleep operation
- ws.send(JSON.stringify({ action: 'poweraction', nodeids: nodes, actiontype: 4, responseid: 'meshctrl' }));
- } else if (args.amton) {
- // Intel AMT Power on operation
- ws.send(JSON.stringify({ action: 'poweraction', nodeids: nodes, actiontype: 302, responseid: 'meshctrl' }));
- } else if (args.amtoff) {
- // Intel AMT Power off operation
- ws.send(JSON.stringify({ action: 'poweraction', nodeids: nodes, actiontype: 308, responseid: 'meshctrl' }));
- } else if (args.amtreset) {
- // Intel AMT Power reset operation
- ws.send(JSON.stringify({ action: 'poweraction', nodeids: nodes, actiontype: 310, responseid: 'meshctrl' }));
- } else {
- console.log('No power operation specified.');
- process.exit(1);
- }
- break;
- }
- case 'agentdownload': {
- // Download an agent
- var u = settings.xxurl.replace('wss://', 'https://').replace('/control.ashx', '/meshagents');
- if (u.indexOf('?') > 0) { u += '&'; } else { u += '?'; }
- u += 'id=' + args.type + '&meshid=' + args.id;
- if (args.installflags) {
- if ((typeof parseInt(args.installflags) != 'number') || isNaN(parseInt(args.installflags)) || (parseInt(args.installflags) < 0) || (parseInt(args.installflags) > 2)) { console.log("Invalid Installflags."); process.exit(1); return; }
- u += '&installflags=' + args.installflags;
- }
- const options = { rejectUnauthorized: false, checkServerIdentity: onVerifyServer }
- const fs = require('fs');
- const https = require('https');
- var downloadSize = 0;
- const req = https.request(u, options, function (res) {
- if (res.statusCode != 200) {
- console.log('Download error, statusCode: ' + res.statusCode);
- process.exit(1);
- } else {
- // Agent the agent filename
- var agentFileName = 'meshagent';
- if ((res.headers) && (res.headers['content-disposition'] != null)) {
- var i = res.headers['content-disposition'].indexOf('filename=\"');
- if (i >= 0) {
- agentFileName = res.headers['content-disposition'].substring(i + 10);
- i = agentFileName.indexOf('\"');
- if (i >= 0) { agentFileName = agentFileName.substring(0, i); }
- }
- }
- // Check if this file already exists
- if (fs.existsSync(agentFileName)) { console.log('File \"' + agentFileName + '\" already exists.'); process.exit(1); }
- var fd = fs.openSync(agentFileName, 'w'); // Open the file for writing
- res.on('data', function (d) {
- downloadSize += d.length;
- fs.writeSync(fd, d); // Save to file
- });
- res.on('end', function (d) {
- fs.closeSync(fd); // Close file
- console.log('Downloaded ' + downloadSize + ' byte(s) to \"' + agentFileName + '\"');
- process.exit(1);
- });
- }
- })
- req.on('error', function (error) { console.error(error); process.exit(1); })
- req.end()
- break;
- }
- case 'webrelay': {
- var protocol = null;
- if (args.type != null) {
- if (args.type == 'http') {
- protocol = 1;
- } else if (args.type == 'https') {
- protocol = 2;
- } else {
- console.log("Unknown protocol type: " + args.type); process.exit(1);
- }
- }
- var port = null;
- if (typeof args.port == 'number') {
- if ((args.port < 1) || (args.port > 65535)) { console.log("Port number must be between 1 and 65535."); process.exit(1); }
- port = args.port;
- } else if (protocol == 1) {
- port = 80;
- } else if (protocol == 2) {
- port = 443;
- }
- ws.send(JSON.stringify({ action: 'webrelay', nodeid: args.id, port: port, appid: protocol, responseid: 'meshctrl' }));
- break;
- }
- case 'devicesharing': {
- if (args.add) {
- if (args.add.length == 0) { console.log("Invalid guest name."); process.exit(1); }
-
- // Sharing type, desktop or terminal
- var p = 0;
- if (args.type != null) {
- var shareTypes = args.type.toLowerCase().split(',');
- for (var i in shareTypes) { if ((shareTypes[i] != 'terminal') && (shareTypes[i] != 'desktop') && (shareTypes[i] != 'files') && (shareTypes[i] != 'http') && (shareTypes[i] != 'https')) { console.log("Unknown sharing type: " + shareTypes[i]); process.exit(1); } }
- if (shareTypes.indexOf('terminal') >= 0) { p |= 1; }
- if (shareTypes.indexOf('desktop') >= 0) { p |= 2; }
- if (shareTypes.indexOf('files') >= 0) { p |= 4; }
- if (shareTypes.indexOf('http') >= 0) { p |= 8; }
- if (shareTypes.indexOf('https') >= 0) { p |= 16; }
- }
- if (p == 0) { p = 2; } // Desktop
-
- // Sharing view only
- var viewOnly = false;
- if (args.viewonly) { viewOnly = true; }
-
- // User consent
- var consent = 0;
- if (args.consent == null) {
- if ((p & 1) != 0) { consent = 0x0002; } // Terminal notify
- if ((p & 2) != 0) { consent = 0x0001; } // Desktop notify
- if ((p & 4) != 0) { consent = 0x0004; } // Files notify
- } else {
- if (typeof args.consent == 'string') {
- var flagStrs = args.consent.split(',');
- for (var i in flagStrs) {
- var flagStr = flagStrs[i].toLowerCase();
- if (flagStr == 'none') { consent = 0; }
- else if (flagStr == 'notify') {
- if ((p & 1) != 0) { consent |= 0x0002; } // Terminal notify
- if ((p & 2) != 0) { consent |= 0x0001; } // Desktop notify
- if ((p & 4) != 0) { consent |= 0x0004; } // Files notify
- } else if (flagStr == 'prompt') {
- if ((p & 1) != 0) { consent |= 0x0010; } // Terminal prompt
- if ((p & 2) != 0) { consent |= 0x0008; } // Desktop prompt
- if ((p & 4) != 0) { consent |= 0x0020; } // Files prompt
- } else if (flagStr == 'bar') {
- if ((p & 2) != 0) { consent |= 0x0040; } // Desktop toolbar
- } else { console.log("Unknown consent type."); process.exit(1); return; }
- }
- }
- }
-
- var port = null;
- // Set Port Number if http or https
- if ((p & 8) || (p & 16)) {
- if (typeof args.port == 'number') {
- if ((args.port < 1) || (args.port > 65535)) { console.log("Port number must be between 1 and 65535."); process.exit(1); }
- port = args.port;
- } else if ((p & 8)) {
- port = 80;
- } else if ((p & 16)) {
- port = 443;
- }
- }
-
- // Start and end time
- var start = null, end = null;
- if (args.start) { start = Math.floor(Date.parse(args.start) / 1000); end = start + (60 * 60); }
- if (args.end) { if (start == null) { start = Math.floor(Date.now() / 1000) } end = Math.floor(Date.parse(args.end) / 1000); if (end <= start) { console.log("End time must be ahead of start time."); process.exit(1); return; } }
- if (args.duration) { if (start == null) { start = Math.floor(Date.now() / 1000) } end = start + parseInt(args.duration * 60); }
-
- // Recurring
- var recurring = 0;
- if (args.daily) { recurring = 1; } else if (args.weekly) { recurring = 2; }
- if (recurring > 0) {
- if (args.end != null) { console.log("End time can't be specified for recurring shares, use --duration only."); process.exit(1); return; }
- if (args.duration == null) { args.duration = 60; } else { args.duration = parseInt(args.duration); }
- if (start == null) { start = Math.floor(Date.now() / 1000) }
- if ((typeof args.duration != 'number') || (args.duration < 1)) { console.log("Invalid duration value."); process.exit(1); return; }
-
- // Recurring sharing
- ws.send(JSON.stringify({ action: 'createDeviceShareLink', nodeid: args.id, guestname: args.add, p: p, consent: consent, start: start, expire: args.duration, recurring: recurring, viewOnly: viewOnly, port: port, responseid: 'meshctrl' }));
- } else {
- if ((start == null) && (end == null)) {
- // Unlimited sharing
- ws.send(JSON.stringify({ action: 'createDeviceShareLink', nodeid: args.id, guestname: args.add, p: p, consent: consent, expire: 0, viewOnly: viewOnly, port: port, responseid: 'meshctrl' }));
- } else {
- // Time limited sharing
- ws.send(JSON.stringify({ action: 'createDeviceShareLink', nodeid: args.id, guestname: args.add, p: p, consent: consent, start: start, end: end, viewOnly: viewOnly, port: port, responseid: 'meshctrl' }));
- }
- }
- } else if (args.remove) {
- ws.send(JSON.stringify({ action: 'removeDeviceShare', nodeid: args.id, publicid: args.remove, responseid: 'meshctrl' }));
- } else {
- ws.send(JSON.stringify({ action: 'deviceShares', nodeid: args.id, responseid: 'meshctrl' }));
- }
- break;
- }
- case 'deviceopenurl': {
- ws.send(JSON.stringify({ action: 'msg', type: 'openUrl', nodeid: args.id, url: args.openurl, responseid: 'meshctrl' }));
- break;
- }
- case 'devicemessage': {
- ws.send(JSON.stringify({ action: 'msg', type: 'messagebox', nodeid: args.id, title: args.title ? args.title : "MeshCentral", msg: args.msg, timeout: args.timeout ? args.timeout : 120000, responseid: 'meshctrl' }));
- break;
- }
- case 'devicetoast': {
- ws.send(JSON.stringify({ action: 'toast', nodeids: [args.id], title: args.title ? args.title : "MeshCentral", msg: args.msg, responseid: 'meshctrl' }));
- break;
- }
- case 'groupmessage': {
- ws.send(JSON.stringify({ action: 'nodes', meshid: args.id, responseid: 'meshctrl' }));
- break;
- }
- case 'grouptoast': {
- ws.send(JSON.stringify({ action: 'nodes', meshid: args.id, responseid: 'meshctrl' }));
- break;
- }
- case 'report': {
- var reporttype = 1;
- switch(args.type) {
- case 'traffic':
- reporttype = 2;
- break;
- case 'logins':
- reporttype = 3;
- break;
- case 'db':
- reporttype = 4;
- break;
- }
-
- var reportgroupby = 1;
- if(args.groupby){
- reportgroupby = args.groupby === 'device' ? 2 : args.groupby === 'day' ? 3: 1;
- }
-
- var start = null, end = null;
- if (args.start) {
- start = Math.floor(Date.parse(args.start) / 1000);
- } else {
- start = reportgroupby === 3 ? Math.round(new Date().getTime() / 1000) - (168 * 3600) : Math.round(new Date().getTime() / 1000) - (24 * 3600);
- }
- if (args.end) {
- end = Math.floor(Date.parse(args.end) / 1000);
- } else {
- end = Math.round(new Date().getTime() / 1000);
- }
- if (end <= start) { console.log("End time must be ahead of start time."); process.exit(1); return; }
-
- ws.send(JSON.stringify({ action: 'report', type: reporttype, groupBy: reportgroupby, devGroup: args.devicegroup || null, start, end, tz: Intl.DateTimeFormat().resolvedOptions().timeZone, tf: new Date().getTimezoneOffset(), showTraffic: args.hasOwnProperty('showtraffic'), l: 'en', responseid: 'meshctrl' }));
- break;
- }
- }
- });
-
- function getSiteAdminRights(args) {
- var siteadmin = -1;
- if (typeof args.rights == 'number') {
- siteadmin = args.rights;
- } else if (typeof args.rights == 'string') {
- siteadmin = 0;
- var srights = args.rights.toLowerCase().split(',');
- if (srights.indexOf('full') != -1) { siteadmin = 0xFFFFFFFF; }
- if (srights.indexOf('none') != -1) { siteadmin = 0x00000000; }
- if (srights.indexOf('backup') != -1 || srights.indexOf('serverbackup') != -1) { siteadmin |= 0x00000001; }
- if (srights.indexOf('manageusers') != -1) { siteadmin |= 0x00000002; }
- if (srights.indexOf('restore') != -1 || srights.indexOf('serverrestore') != -1) { siteadmin |= 0x00000004; }
- if (srights.indexOf('fileaccess') != -1) { siteadmin |= 0x00000008; }
- if (srights.indexOf('update') != -1 || srights.indexOf('serverupdate') != -1) { siteadmin |= 0x00000010; }
- if (srights.indexOf('locked') != -1) { siteadmin |= 0x00000020; }
- if (srights.indexOf('nonewgroups') != -1) { siteadmin |= 0x00000040; }
- if (srights.indexOf('notools') != -1) { siteadmin |= 0x00000080; }
- if (srights.indexOf('usergroups') != -1) { siteadmin |= 0x00000100; }
- if (srights.indexOf('recordings') != -1) { siteadmin |= 0x00000200; }
- if (srights.indexOf('locksettings') != -1) { siteadmin |= 0x00000400; }
- if (srights.indexOf('allevents') != -1) { siteadmin |= 0x00000800; }
- if (srights.indexOf('nonewdevices') != -1) { siteadmin |= 0x00001000; }
- }
-
- if (args.siteadmin) { siteadmin = 0xFFFFFFFF; }
- if (args.manageusers) { if (siteadmin == -1) { siteadmin = 0; } siteadmin |= 2; }
- if (args.fileaccess) { if (siteadmin == -1) { siteadmin = 0; } siteadmin |= 8; }
- if (args.serverupdate) { if (siteadmin == -1) { siteadmin = 0; } siteadmin |= 16; }
- if (args.locked) { if (siteadmin == -1) { siteadmin = 0; } siteadmin |= 32; }
- if (args.nonewgroups) { if (siteadmin == -1) { siteadmin = 0; } siteadmin |= 64; }
- if (args.notools) { if (siteadmin == -1) { siteadmin = 0; } siteadmin |= 128; }
- return siteadmin;
- }
-
- ws.on('close', function () { process.exit(); });
- ws.on('error', function (err) {
- if (err.code == 'ENOTFOUND') { console.log('Unable to resolve ' + url); }
- else if (err.code == 'ECONNREFUSED') { console.log('Unable to connect to ' + url); }
- else { console.log('Unable to connect to ' + url); }
- process.exit();
- });
-
- ws.on('message', function incoming(rawdata) {
- var data = null;
- try { data = JSON.parse(rawdata); } catch (ex) { }
- if (data == null) { console.log('Unable to parse data: ' + rawdata); }
- if (settings.cmd == 'showevents') {
- if (args.filter == null) {
- // Display all events
- console.log(JSON.stringify(data, null, 2));
- } else {
- // Display select events
- var filters = args.filter.split(',');
- if (typeof data.event == 'object') {
- if (filters.indexOf(data.event.action) >= 0) { console.log(JSON.stringify(data, null, 2) + '\r\n'); }
- } else {
- if (filters.indexOf(data.action) >= 0) { console.log(JSON.stringify(data, null, 2) + '\r\n'); }
- }
- }
- return;
- }
- switch (data.action) {
- case 'serverinfo': { // SERVERINFO
- settings.currentDomain = data.serverinfo.domain;
- if (settings.cmd == 'serverinfo') {
- if (args.json) {
- console.log(JSON.stringify(data.serverinfo, ' ', 2));
- } else {
- for (var i in data.serverinfo) { console.log(i + ':', data.serverinfo[i]); }
- }
- process.exit();
- }
- break;
- }
- case 'events': {
- if (settings.cmd == 'listevents') {
- if (args.raw) {
- // RAW JSON
- console.log(JSON.stringify(data.events));
- } else if (args.json) {
- // Formatted JSON
- console.log(JSON.stringify(data.events, null, 2));
- } else {
- if ((args.id == null) && (args.userid == null)) {
- // CSV format
- console.log("time,type,action,nodeid,userid,msg");
- for (var i in data.events) {
- var x = [];
- x.push(data.events[i].time);
- x.push(data.events[i].etype);
- x.push(data.events[i].action);
- x.push(data.events[i].nodeid);
- x.push(data.events[i].userid);
- x.push(data.events[i].msg);
- console.log(csvFormatArray(x));
- }
- } else if (args.id != null) {
- // CSV format
- console.log("time,type,action,userid,msg");
- for (var i in data.events) {
- var x = [];
- x.push(data.events[i].time);
- x.push(data.events[i].etype);
- x.push(data.events[i].action);
- x.push(data.events[i].userid);
- x.push(data.events[i].msg);
- console.log(csvFormatArray(x));
- }
- } else if (args.userid != null) {
- // CSV format
- console.log("time,type,action,nodeid,msg");
- for (var i in data.events) {
- var x = [];
- x.push(data.events[i].time);
- x.push(data.events[i].etype);
- x.push(data.events[i].action);
- x.push(data.events[i].nodeid);
- x.push(data.events[i].msg);
- console.log(csvFormatArray(x));
- }
- }
- }
- process.exit();
- }
- break;
- }
- case 'authcookie': { // SHELL, UPLOAD, DOWNLOAD
- if ((settings.cmd == 'shell') || (settings.cmd == 'upload') || (settings.cmd == 'download')) {
- var protocol = 1; // Terminal
- if ((settings.cmd == 'upload') || (settings.cmd == 'download')) { protocol = 5; } // Files
- if ((args.id.split('/') != 3) && (settings.currentDomain != null)) { args.id = 'node/' + settings.currentDomain + '/' + args.id; }
- var id = getRandomHex(6);
- ws.send(JSON.stringify({ action: 'msg', nodeid: args.id, type: 'tunnel', usage: 1, value: '*/meshrelay.ashx?p=' + protocol + '&nodeid=' + args.id + '&id=' + id + '&rauth=' + data.rcookie, responseid: 'meshctrl' }));
- connectTunnel(url.replace('/control.ashx', '/meshrelay.ashx?browser=1&p=' + protocol + '&nodeid=' + encodeURIComponent(args.id) + '&id=' + id + '&auth=' + data.cookie));
- }
- break;
- }
- case 'deviceShares': { // DEVICESHARING
- if (data.result != null) {
- console.log(data.result);
- } else {
- if ((data.deviceShares == null) || (data.deviceShares.length == 0)) {
- console.log('No device sharing links for this device.');
- } else {
- if (args.json) {
- console.log(data.deviceShares);
- } else {
- for (var i in data.deviceShares) {
- var share = data.deviceShares[i];
- var shareType = [];
- if ((share.p & 1) != 0) { shareType.push("Terminal"); }
- if ((share.p & 2) != 0) { if (share.viewOnly) { shareType.push("View Only Desktop"); } else { shareType.push("Desktop"); } }
- if ((share.p & 4) != 0) { shareType.push("Files"); }
- shareType = shareType.join(' + ');
- if (shareType == '') { shareType = "Unknown"; }
- var consent = [];
- if ((share.consent & 0x0001) != 0) { consent.push("Desktop Notify"); }
- if ((share.consent & 0x0008) != 0) { consent.push("Desktop Prompt"); }
- if ((share.consent & 0x0040) != 0) { consent.push("Desktop Connection Toolbar"); }
- if ((share.consent & 0x0002) != 0) { consent.push("Terminal Notify"); }
- if ((share.consent & 0x0010) != 0) { consent.push("Terminal Prompt"); }
- if ((share.consent & 0x0004) != 0) { consent.push("Files Notify"); }
- if ((share.consent & 0x0020) != 0) { consent.push("Files Prompt"); }
- console.log('----------');
- console.log('Identifier: ' + share.publicid);
- console.log('Type: ' + shareType);
- console.log('UserId: ' + share.userid);
- console.log('Guest Name: ' + share.guestName);
- console.log('User Consent: ' + consent.join(', '));
- if (share.startTime) { console.log('Start Time: ' + new Date(share.startTime).toLocaleString()); }
- if (share.expireTime) { console.log('Expire Time: ' + new Date(share.expireTime).toLocaleString()); }
- if (share.duration) { console.log('Duration: ' + share.duration + ' minute' + ((share.duration > 1) ? 's' : '')); }
- if (share.recurring == 1) { console.log('Recurring: ' + 'Daily'); }
- if (share.recurring == 2) { console.log('Recurring: ' + 'Weekly'); }
- console.log('URL: ' + share.url);
- }
- }
- }
- }
- process.exit();
- break;
- }
- case 'userinfo': { // USERINFO
- if (settings.cmd == 'userinfo') {
- if (args.json) {
- console.log(JSON.stringify(data.userinfo, ' ', 2));
- } else {
- for (var i in data.userinfo) { console.log(i + ':', data.userinfo[i]); }
- }
- process.exit();
- }
- break;
- }
- case 'getsysinfo': { // DEVICEINFO
- if (settings.cmd == 'deviceinfo') {
- settings.sysinfo = (data.result) ? null : data;
- if (--settings.deviceinfocount == 0) { displayDeviceInfo(settings.sysinfo, settings.lastconnect, settings.networking, settings.nodes); process.exit(); }
- }
- break;
- }
- case 'lastconnect': {
- if (settings.cmd == 'deviceinfo') {
- settings.lastconnect = (data.result) ? null : data;
- if (--settings.deviceinfocount == 0) { displayDeviceInfo(settings.sysinfo, settings.lastconnect, settings.networking, settings.nodes); process.exit(); }
- }
- break;
- }
- case 'getnetworkinfo': {
- if (settings.cmd == 'deviceinfo') {
- settings.networking = (data.result) ? null : data;
- if (--settings.deviceinfocount == 0) { displayDeviceInfo(settings.sysinfo, settings.lastconnect, settings.networking, settings.nodes); process.exit(); }
- }
- break;
- }
- case 'msg': // SHELL
- case 'toast': // TOAST
- case 'adduser': // ADDUSER
- case 'edituser': // EDITUSER
- case 'addamtdevice': // ADDAMTDEVICE
- case 'addlocaldevice': // ADDLOCALDEVICE
- case 'removedevices': // REMOVEDEVICE
- case 'changedevice': // EDITDEVICE
- case 'deleteuser': // REMOVEUSER
- case 'createmesh': // ADDDEVICEGROUP
- case 'deletemesh': // REMOVEDEVICEGROUP
- case 'editmesh': // EDITDEVICEGROUP
- case 'wakedevices':
- case 'changeDeviceMesh':
- case 'addmeshuser': //
- case 'removemeshuser': //
- case 'wakedevices': //
- case 'inviteAgent': //
- case 'adddeviceuser': //
- case 'createusergroup': //
- case 'deleteusergroup': //
- case 'runcommands':
- case 'poweraction':
- case 'addusertousergroup':
- case 'removeuserfromusergroup':
- case 'removeDeviceShare':
- case 'userbroadcast': { // BROADCAST
- if (((settings.cmd == 'shell') || (settings.cmd == 'upload') || (settings.cmd == 'download')) && (data.result == 'OK')) return;
- if ((data.type == 'runcommands') && (settings.cmd != 'runcommand')) return;
- if ((settings.multiresponse != null) && (settings.multiresponse > 1)) { settings.multiresponse--; break; }
- if (data.responseid == 'meshctrl') {
- if (data.meshid) { console.log(data.result, data.meshid); }
- else if (data.userid) { console.log(data.result, data.userid); }
- else console.log(data.result);
- process.exit();
- }
- break;
- }
- case 'createDeviceShareLink':
- case 'webrelay':
- if (data.result == 'OK') {
- if (data.publicid) { console.log('ID: ' + data.publicid); }
- console.log('URL: ' + data.url);
- } else {
- console.log(data.result);
- }
- process.exit();
- break;
- case 'createInviteLink':
- if (data.responseid == 'meshctrl') {
- if (data.url) { console.log(data.url); }
- else console.log(data.result);
- process.exit();
- }
- break;
- case 'wssessioncount': { // LIST USER SESSIONS
- if (args.json) {
- console.log(JSON.stringify(data.wssessions, ' ', 2));
- } else {
- for (var i in data.wssessions) { console.log(i + ', ' + ((data.wssessions[i] > 1) ? (data.wssessions[i] + ' sessions.') : ("1 session."))); }
- }
- process.exit();
- break;
- }
- case 'usergroups': { // LIST USER GROUPS
- if (settings.cmd == 'listusergroups') {
- if (args.json) {
- console.log(JSON.stringify(data.ugroups, ' ', 2));
- } else {
- for (var i in data.ugroups) {
- var x = i + ', ' + data.ugroups[i].name;
- if (data.ugroups[i].desc && (data.ugroups[i].desc != '')) { x += ', ' + data.ugroups[i].desc; }
- console.log(x);
- var mesh = [], user = [], node = [];
- if (data.ugroups[i].links != null) { for (var j in data.ugroups[i].links) { if (j.startsWith('mesh/')) { mesh.push(j); } if (j.startsWith('user/')) { user.push(j); } if (j.startsWith('node/')) { node.push(j); } } }
- console.log(' Users:');
- if (user.length > 0) { for (var j in user) { console.log(' ' + user[j]); } } else { console.log(' (None)'); }
- console.log(' Device Groups:');
- if (mesh.length > 0) { for (var j in mesh) { console.log(' ' + mesh[j] + ', ' + data.ugroups[i].links[mesh[j]].rights); } } else { console.log(' (None)'); }
- console.log(' Devices:');
- if (node.length > 0) { for (var j in node) { console.log(' ' + node[j] + ', ' + data.ugroups[i].links[node[j]].rights); } } else { console.log(' (None)'); }
- }
- }
- process.exit();
- } else if (settings.cmd == 'removeallusersfromusergroup') {
- var ugrpid = args.groupid, exit = false;
- if ((args.domain != null) && (userid.indexOf('/') < 0)) { ugrpid = 'ugrp/' + args.domain + '/' + ugrpid; }
- var ugroup = data.ugroups[ugrpid];
- if (ugroup == null) {
- console.log('User group not found.');
- exit = true;
- } else {
- var usercount = 0;
- if (ugroup.links) {
- for (var i in ugroup.links) {
- if (i.startsWith('user/')) {
- usercount++;
- ws.send(JSON.stringify({ action: 'removeuserfromusergroup', ugrpid: ugrpid, userid: i, responseid: 'meshctrl' }));
- console.log('Removing ' + i);
- }
- }
- }
- if (usercount == 0) { console.log('No users in this user group.'); exit = true; } else { settings.multiresponse = usercount; }
- }
- if (exit) { process.exit(); }
- }
- break;
- }
- case 'users': { // LISTUSERS
- if (data.result) { console.log(data.result); process.exit(); return; }
- if (args.filter) {
- // Filter the list of users
- var filters = args.filter.toLowerCase().split(',');
- var filteredusers = [];
- for (var i in data.users) {
- var ok = false;
- if ((filters.indexOf('2fa') >= 0) && ((data.users[i].otphkeys != null) || (data.users[i].otpkeys != null) || (data.users[i].otpsecret != null))) { ok = true; }
- if ((filters.indexOf('no2fa') >= 0) && ((data.users[i].otphkeys == null) && (data.users[i].otpkeys == null) && (data.users[i].otpsecret == null))) { ok = true; }
- if (ok == true) { filteredusers.push(data.users[i]); }
- }
- data.users = filteredusers;
- }
- if (args.json) {
- console.log(JSON.stringify(data.users, ' ', 2));
- } else {
- if (args.idexists) { for (var i in data.users) { const u = data.users[i]; if ((u._id == args.idexists) || (u._id.split('/')[2] == args.idexists)) { console.log('1'); process.exit(); return; } } console.log('0'); process.exit(); return; }
- if (args.nameexists) { for (var i in data.users) { const u = data.users[i]; if (u.name == args.nameexists) { console.log(u._id); process.exit(); return; } } process.exit(); return; }
-
- console.log('id, name, email\r\n---------------');
- for (var i in data.users) {
- const u = data.users[i];
- var t = "\"" + u._id.split('/')[2] + "\", \"" + u.name + "\"";
- if (u.email != null) { t += ", \"" + u.email + "\""; }
- console.log(t);
- }
- }
- process.exit();
- break;
- }
- case 'nodes': {
- if (settings.cmd == 'deviceinfo') {
- settings.nodes = (data.result) ? null : data;
- if (--settings.deviceinfocount == 0) { displayDeviceInfo(settings.sysinfo, settings.lastconnect, settings.networking, settings.nodes); process.exit(); }
- }
- if ((settings.cmd == 'listdevices') && (data.responseid == 'meshctrl')) {
- if ((data.result != null) && (data.result != 'ok')) {
- console.log(data.result);
- } else {
- // Filter devices based on device id.
- if (args.filterid) {
- var filteridSplit = args.filterid.split(','), filters = [];
- for (var i in filteridSplit) {
- var f = filteridSplit[i].trim();
- var g = f.split('/'); // If there is any / in the id, just grab the last part.
- if (g.length > 0) { f = g[g.length - 1]; }
- if (f != '') { filters.push(f); }
- }
- if (filters.length > 0) {
- for (var mid in data.nodes) {
- var filteredNodes = [];
- for (var nid in data.nodes[mid]) {
- var n = data.nodes[mid][nid], match = false;
- for (var f in filters) { if (n._id.indexOf(filters[f]) >= 0) { match = true; } }
- if (match) { filteredNodes.push(n); }
- }
- data.nodes[mid] = filteredNodes;
- }
- }
- }
-
- // Filter devices based on filter string
- if (args.filter != null) {
- for (var meshid in data.nodes) {
- for (var d in data.nodes[meshid]) { data.nodes[meshid][d].meshid = meshid; }
- data.nodes[meshid] = parseSearchOrInput(data.nodes[meshid], args.filter.toString().toLowerCase());
- }
- }
-
- if (args.csv) {
- // Return a flat list
- var nodecount = 0;
- for (var i in data.nodes) {
- var devicesInMesh = data.nodes[i];
- for (var j in devicesInMesh) {
- var n = devicesInMesh[j];
- nodecount++;
- if (settings.xmeshes && settings.xmeshes[i]) {
- console.log('\"' + settings.xmeshes[i]._id.split('/')[2] + '\",\"' + settings.xmeshes[i].name.split('\"').join('') + '\",\"' + n._id.split('/')[2] + '\",\"' + n.name.split('\"').join('') + '\",' + (n.icon ? n.icon : 0) + ',' + (n.conn ? n.conn : 0) + ',' + (n.pwr ? n.pwr : 0));
- } else {
- console.log('\"\",\"\",\"' + n._id.split('/')[2] + '\",\"' + n.name.split('\"').join('') + '\",' + (n.icon ? n.icon : 0) + ',' + (n.conn ? n.conn : 0) + ',' + (n.pwr ? n.pwr : 0));
- }
- }
- }
- if (nodecount == 0) { console.log('None'); }
- } else if (args.count) {
- // Return how many devices are in this group
- var nodes = [];
- for (var i in data.nodes) { var devicesInMesh = data.nodes[i]; for (var j in devicesInMesh) { nodes.push(devicesInMesh[j]); } }
- console.log(nodes.length);
- } else if (args.json) {
- // Return all devices in JSON format
- var nodes = [];
-
- for (var i in data.nodes) {
- const devicesInMesh = data.nodes[i];
- for (var j in devicesInMesh) {
- devicesInMesh[j].meshid = i; // Add device group id
- if (settings.xmeshes && settings.xmeshes[i] && settings.xmeshes[i].name) { devicesInMesh[j].groupname = settings.xmeshes[i].name; } // Add device group name
- nodes.push(devicesInMesh[j]);
- }
- }
- console.log(JSON.stringify(nodes, ' ', 2));
- } else {
- // Display the list of nodes in text format
- var nodecount = 0;
- for (var i in data.nodes) {
- var devicesInMesh = data.nodes[i];
- if (devicesInMesh.length > 0) {
- if (settings.xmeshes && settings.xmeshes[i] && settings.xmeshes[i].name) { console.log('\r\nDevice group: \"' + settings.xmeshes[i].name.split('\"').join('') + '\"'); }
- console.log('id, name, icon, conn, pwr\r\n-------------------------');
- for (var j in devicesInMesh) {
- var n = devicesInMesh[j];
- nodecount++;
- console.log('\"' + n._id.split('/')[2] + '\", \"' + n.name.split('\"').join('') + '\", ' + (n.icon ? n.icon : 0) + ', ' + (n.conn ? n.conn : 0) + ', ' + (n.pwr ? n.pwr : 0));
- }
- }
- }
- if (nodecount == 0) { console.log('None'); }
- }
- }
- process.exit();
- }
- if ((settings.cmd == 'groupmessage') && (data.responseid == 'meshctrl')) {
- if ((data.nodes != null)) {
- for (var i in data.nodes) {
- for (let index = 0; index < data.nodes[i].length; index++) {
- const element = data.nodes[i][index];
- ws.send(JSON.stringify({ action: 'msg', type: 'messagebox', nodeid: element._id, title: args.title ? args.title : "MeshCentral", msg: args.msg, timeout: args.timeout ? args.timeout : 120000 }));
- }
- }
- }
-
- setTimeout(function(){ console.log('ok'); process.exit(); }, 1000);
- }
- if ((settings.cmd == 'grouptoast') && (data.responseid == 'meshctrl')) {
- if (data.nodes != null) {
- for (var i in data.nodes) {
- var nodes = [];
- for (let index = 0; index < data.nodes[i].length; index++) {
- const element = data.nodes[i][index];
- nodes.push(element._id);
- }
- ws.send(JSON.stringify({ action: 'toast', nodeids: nodes, title: args.title ? args.title : "MeshCentral", msg: args.msg, responseid: 'meshctrl' }));
- }
- }
- }
- break;
- }
- case 'meshes': { // LISTDEVICEGROUPS
- if (settings.cmd == 'listdevices') {
- // Store the list of device groups for later use
- settings.xmeshes = {}
- for (var i in data.meshes) { settings.xmeshes[data.meshes[i]._id] = data.meshes[i]; }
- } else if (settings.cmd == 'listdevicegroups') {
- if (args.json) {
- // If asked, add the MeshID hex encoding to the JSON.
- if (args.hex) { for (var i in data.meshes) { data.meshes[i]._idhex = '0x' + Buffer.from(data.meshes[i]._id.split('/')[2].replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase(); } }
- console.log(JSON.stringify(data.meshes, ' ', 2));
- } else {
- if (args.idexists) { for (var i in data.meshes) { const u = data.meshes[i]; if ((u._id == args.idexists) || (u._id.split('/')[2] == args.idexists)) { console.log('1'); process.exit(); return; } } console.log('0'); process.exit(); return; }
- if (args.nameexists) { for (var i in data.meshes) { const u = data.meshes[i]; if (u.name == args.nameexists) { console.log(u._id); process.exit(); return; } } process.exit(); return; }
-
- console.log('id, name\r\n---------------');
- for (var i in data.meshes) {
- const m = data.meshes[i];
- var mid = m._id.split('/')[2];
- if (args.hex) { mid = '0x' + Buffer.from(mid.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase(); }
- var t = "\"" + mid + "\", \"" + m.name + "\"";
- console.log(t);
- }
- }
- process.exit();
- } else if (settings.cmd == 'listusersofdevicegroup') {
- for (var i in data.meshes) {
- const m = data.meshes[i];
- var mid = m._id.split('/')[2];
- if (mid == args.id) {
- if (args.json) {
- console.log(JSON.stringify(m.links, ' ', 2));
- } else {
- console.log('userid, rights\r\n---------------');
- for (var l in m.links) {
- var rights = m.links[l].rights;
- var rightsstr = [];
- if (rights == 4294967295) { rightsstr = ['FullAdministrator']; } else {
- if (rights & 1) { rightsstr.push('EditMesh'); }
- if (rights & 2) { rightsstr.push('ManageUsers'); }
- if (rights & 4) { rightsstr.push('ManageComputers'); }
- if (rights & 8) { rightsstr.push('RemoteControl'); }
- if (rights & 16) { rightsstr.push('AgentConsole'); }
- if (rights & 32) { rightsstr.push('ServerFiles'); }
- if (rights & 64) { rightsstr.push('WakeDevice'); }
- if (rights & 128) { rightsstr.push('SetNotes'); }
- if (rights & 256) { rightsstr.push('RemoteViewOnly'); }
- if (rights & 512) { rightsstr.push('NoTerminal'); }
- if (rights & 1024) { rightsstr.push('NoFiles'); }
- if (rights & 2048) { rightsstr.push('NoAMT'); }
- if (rights & 4096) { rightsstr.push('DesktopLimitedInput'); }
- }
- console.log(l.split('/')[2] + ', ' + rightsstr.join(', '));
- }
- }
- process.exit();
- return;
- }
- }
- console.log('Group id not found');
- process.exit();
- }
- break;
- }
- case 'close': {
- if (data.cause == 'noauth') {
- if (data.msg == 'tokenrequired') {
- console.log('Authentication token required, use --token [number].');
- } else if (data.msg == 'nokey') {
- console.log('URL key is invalid or missing, please specify ?key=xxx in url');
- } else {
- if ((args.loginkeyfile != null) || (args.loginkey != null)) {
- console.log('Invalid login, check the login key and that this computer has the correct time.');
- } else {
- console.log('Invalid login.');
- }
- }
- }
- process.exit();
- break;
- }
- case 'createLoginToken': {
- if (data.result != null) {
- console.log(data.result);
- process.exit();
- } else {
- if (args.json) {
- console.log(data);
- } else {
- console.log("New login token created.");
- if (data.name) { console.log("Token name: " + data.name); }
- if (data.created) { console.log("Created: " + new Date(data.created).toLocaleString()); }
- if (data.expire) { console.log("Expire: " + new Date(data.expire).toLocaleString()); }
- if (data.tokenUser) { console.log("Username: " + data.tokenUser); }
- if (data.tokenPass) { console.log("Password: " + data.tokenPass); }
- }
- }
- process.exit();
- break;
- }
- case 'loginTokens': {
- if (args.json) {
- console.log(data.loginTokens);
- } else {
- console.log("Name Username Expire");
- console.log("-------------------------------------------------------------------------------------");
- if (data.loginTokens.length == 0) {
- console.log("No login tokens");
- } else {
- for (var i in data.loginTokens) {
- var t = data.loginTokens[i];
- var e = (t.expire == 0) ? "Unlimited" : new Date(t.expire).toLocaleString();
- console.log(padString(t.name, 28) + padString(t.tokenUser, 28) + e);
- }
- }
- }
- process.exit();
- break;
- }
- case 'getDeviceDetails': {
- console.log(data.data);
- process.exit();
- }
- case 'report': {
- console.log('group,' + data.data.columns.flatMap(c => c.id).join(','));
- Object.keys(data.data.groups).forEach(gk => {
- data.data.groups[gk].entries.forEach(e => {
- console.log(gk + ',' + Object.values(e).join(','));
- });
- });
- process.exit();
- }
- default: { break; }
- }
- //console.log('Data', data);
- //setTimeout(function timeout() { ws.send(Date.now()); }, 500);
- });
-}
-
-// String padding function
-
-function padString(str, pad) {
- var xpad = ' ';
- if (str.length >= pad) return str; return str + xpad.substring(0, pad - str.length)
-}
-
-function parseSearchAndInput(nodes, x) {
- var s = x.split(' ' + "and" + ' '), r = null;
- for (var i in s) {
- var r2 = getDevicesThatMatchFilter(nodes, s[i]);
- if (r == null) { r = r2; } else { var r3 = []; for (var j in r2) { if (r.indexOf(r2[j]) >= 0) { r3.push(r2[j]); } } r = r3; }
- }
- return r;
-}
-
-function parseSearchOrInput(nodes, x) {
- var s = x.split(' ' + "or" + ' '), r = null;
- for (var i in s) { var r2 = parseSearchAndInput(nodes, s[i]); if (r == null) { r = r2; } else { for (var j in r2) { if (r.indexOf(r2[j] >= 0)) { r.push(r2[j]); } } } }
- return r;
-}
-
-function getDevicesThatMatchFilter(nodes, x) {
- var r = [];
- var userSearch = null, ipSearch = null, groupSearch = null, tagSearch = null, agentTagSearch = null, wscSearch = null, osSearch = null, amtSearch = null, descSearch = null;
- if (x.startsWith("user:".toLowerCase())) { userSearch = x.substring("user:".length); }
- else if (x.startsWith("u:".toLowerCase())) { userSearch = x.substring("u:".length); }
- else if (x.startsWith("ip:".toLowerCase())) { ipSearch = x.substring("ip:".length); }
- else if (x.startsWith("group:".toLowerCase())) { groupSearch = x.substring("group:".length); }
- else if (x.startsWith("g:".toLowerCase())) { groupSearch = x.substring("g:".length); }
- else if (x.startsWith("tag:".toLowerCase())) { tagSearch = x.substring("tag:".length); }
- else if (x.startsWith("t:".toLowerCase())) { tagSearch = x.substring("t:".length); }
- else if (x.startsWith("atag:".toLowerCase())) { agentTagSearch = x.substring("atag:".length); }
- else if (x.startsWith("a:".toLowerCase())) { agentTagSearch = x.substring("a:".length); }
- else if (x.startsWith("os:".toLowerCase())) { osSearch = x.substring("os:".length); }
- else if (x.startsWith("amt:".toLowerCase())) { amtSearch = x.substring("amt:".length); }
- else if (x.startsWith("desc:".toLowerCase())) { descSearch = x.substring("desc:".length); }
- else if (x == 'wsc:ok') { wscSearch = 1; }
- else if (x == 'wsc:noav') { wscSearch = 2; }
- else if (x == 'wsc:noupdate') { wscSearch = 3; }
- else if (x == 'wsc:nofirewall') { wscSearch = 4; }
- else if (x == 'wsc:any') { wscSearch = 5; }
-
- if (x == '') {
- // No search
- for (var d in nodes) { r.push(nodes[d]); }
- } else if (ipSearch != null) {
- // IP address search
- for (var d in nodes) { if ((nodes[d].ip != null) && (nodes[d].ip.indexOf(ipSearch) >= 0)) { r.push(nodes[d]); } }
- } else if (groupSearch != null) {
- // Group filter
- if (settings.xmeshes) { for (var d in nodes) { if (settings.xmeshes[nodes[d].meshid] && settings.xmeshes[nodes[d].meshid].name.toLowerCase().indexOf(groupSearch) >= 0) { r.push(nodes[d]); } } }
- } else if (tagSearch != null) {
- // Tag filter
- for (var d in nodes) {
- if ((nodes[d].tags == null) && (tagSearch == '')) { r.push(nodes[d]); }
- else if (nodes[d].tags != null) { for (var j in nodes[d].tags) { if (nodes[d].tags[j].toLowerCase() == tagSearch) { r.push(nodes[d]); break; } } }
- }
- } else if (agentTagSearch != null) {
- // Agent Tag filter
- for (var d in nodes) {
- if ((((nodes[d].agent != null) && (nodes[d].agent.tag == null)) && (agentTagSearch == '')) || ((nodes[d].agent != null) && (nodes[d].agent.tag != null) && (nodes[d].agent.tag.toLowerCase().indexOf(agentTagSearch) >= 0))) { r.push(nodes[d]); };
- }
- } else if (userSearch != null) {
- // User search
- for (var d in nodes) {
- if (nodes[d].users && nodes[d].users.length > 0) { for (var i in nodes[d].users) { if (nodes[d].users[i].toLowerCase().indexOf(userSearch) >= 0) { r.push(nodes[d]); } } }
- }
- } else if (osSearch != null) {
- // OS search
- for (var d in nodes) { if ((nodes[d].osdesc != null) && (nodes[d].osdesc.toLowerCase().indexOf(osSearch) >= 0)) { r.push(nodes[d]); }; }
- } else if (amtSearch != null) {
- // Intel AMT search
- for (var d in nodes) { if ((nodes[d].intelamt != null) && ((amtSearch == '') || (nodes[d].intelamt.state == amtSearch))) { r.push(nodes[d]); } }
- } else if (descSearch != null) {
- // Device description search
- for (var d in nodes) { if ((nodes[d].desc != null) && (nodes[d].desc != '') && ((descSearch == '') || (nodes[d].desc.toLowerCase().indexOf(descSearch) >= 0))) { r.push(nodes[d]); } }
- } else if (wscSearch != null) {
- // Windows Security Center
- for (var d in nodes) {
- if (nodes[d].wsc) {
- if ((wscSearch == 1) && (nodes[d].wsc.antiVirus == 'OK') && (nodes[d].wsc.autoUpdate == 'OK') && (nodes[d].wsc.firewall == 'OK')) { r.push(nodes[d]); }
- else if (((wscSearch == 2) || (wscSearch == 5)) && (nodes[d].wsc.antiVirus != 'OK')) { r.push(nodes[d]); }
- else if (((wscSearch == 3) || (wscSearch == 5)) && (nodes[d].wsc.autoUpdate != 'OK')) { r.push(nodes[d]); }
- else if (((wscSearch == 4) || (wscSearch == 5)) && (nodes[d].wsc.firewall != 'OK')) { r.push(nodes[d]); }
- }
- }
- } else if (x == '*') {
- // Star filter
- for (var d in nodes) { if (stars[nodes[d]._id] == 1) { r.push(nodes[d]); } }
- } else {
- // Device name search
- try {
- var rs = x.split(/\s+/).join('|'), rx = new RegExp(rs); // In some cases (like +), this can throw an exception.
- for (var d in nodes) {
- //if (showRealNames) {
- //if (nodes[d].rnamel != null && rx.test(nodes[d].rnamel.toLowerCase())) { r.push(nodes[d]); }
- //} else {
- if (rx.test(nodes[d].name.toLowerCase())) { r.push(nodes[d]); }
- //}
- }
- } catch (ex) { for (var d in nodes) { r.push(nodes[d]); } }
- }
-
- return r;
-}
-
-
-// Connect tunnel to a remote agent
-function connectTunnel(url) {
- // Setup WebSocket options
- var options = { rejectUnauthorized: false, checkServerIdentity: onVerifyServer }
-
- // Setup the HTTP proxy if needed
- if (args.proxy != null) { const HttpsProxyAgent = require('https-proxy-agent'); options.agent = new HttpsProxyAgent(require('url').parse(args.proxy)); }
-
- // Connect the WebSocket
- console.log('Connecting...');
- const WebSocket = require('ws');
- settings.tunnelwsstate = 0;
- settings.tunnelws = new WebSocket(url, options);
- settings.tunnelws.on('open', function () { console.log('Waiting for Agent...'); }); // Wait for agent connection
- settings.tunnelws.on('close', function () { console.log('Connection Closed.'); process.exit(); });
- settings.tunnelws.on('error', function (err) { console.log(err); process.exit(); });
-
- if (settings.cmd == 'shell') {
- // This code does all of the work for a shell command
- settings.tunnelws.on('message', function (rawdata) {
- var data = rawdata.toString();
- if (settings.tunnelwsstate == 1) {
- // If the incoming text looks exactly like a control command, ignore it.
- if ((typeof data == 'string') && (data.startsWith('{"ctrlChannel":"102938","type":"'))) {
- var ctrlCmd = null;
- try { ctrlCmd = JSON.parse(data); } catch (ex) { }
- if ((ctrlCmd != null) && (ctrlCmd.ctrlChannel == '102938') && (ctrlCmd.type != null)) return; // This is a control command, like ping/pong. Ignore it.
- }
- process.stdout.write(data);
- } else if (settings.tunnelwsstate == 0) {
- if (data == 'c') { console.log('Connected.'); } else if (data == 'cr') { console.log('Connected, session is being recorded.'); } else return;
- // Send terminal size
- var termSize = null;
- if (typeof process.stdout.getWindowSize == 'function') { termSize = process.stdout.getWindowSize(); }
- if (termSize != null) { settings.tunnelws.send(JSON.stringify({ ctrlChannel: '102938', type: 'options', cols: termSize[0], rows: termSize[1] })); }
- settings.tunnelwsstate = 1;
- settings.tunnelws.send('1'); // Terminal
- process.stdin.setEncoding('utf8');
- process.stdin.setRawMode(true);
- process.stdout.setEncoding('utf8');
- process.stdin.unpipe(process.stdout);
- process.stdout.unpipe(process.stdin);
- process.stdin.on('data', function (data) { settings.tunnelws.send(Buffer.from(data)); });
- //process.stdin.on('readable', function () { var chunk; while ((chunk = process.stdin.read()) !== null) { settings.tunnelws.send(Buffer.from(chunk)); } });
- process.stdin.on('end', function () { process.exit(); });
- process.stdout.on('resize', function () {
- var termSize = null;
- if (typeof process.stdout.getWindowSize == 'function') { termSize = process.stdout.getWindowSize(); }
- if (termSize != null) { settings.tunnelws.send(JSON.stringify({ ctrlChannel: '102938', type: 'termsize', cols: termSize[0], rows: termSize[1] })); }
- });
- }
- });
- } else if (settings.cmd == 'upload') {
- // This code does all of the work for a file upload
- // node meshctrl upload --id oL4Y6Eg0qjnpHFrp1AxfxnBPenbDGnDSkC@HSOnAheIyd51pKhqSCUgJZakzwfKl --file readme.md --target c:\
- settings.tunnelws.on('message', function (rawdata) {
- if (settings.tunnelwsstate == 1) {
- var cmd = null;
- try { cmd = JSON.parse(rawdata.toString()); } catch (ex) { return; }
- if (cmd.reqid == 'up') {
- if ((cmd.action == 'uploadack') || (cmd.action == 'uploadstart')) {
- settings.inFlight--;
- if (settings.uploadFile == null) { if (settings.inFlight == 0) { process.exit(); } return; } // If the file is closed and there is no more in-flight data, exit.
- var loops = (cmd.action == 'uploadstart') ? 16 : 1; // If this is the first data to be sent, hot start now. We are going to have 16 blocks of data in-flight.
- for (var i = 0; i < loops; i++) {
- if (settings.uploadFile == null) continue;
- var buf = Buffer.alloc(65565);
- var len = require('fs').readSync(settings.uploadFile, buf, 1, 65564, settings.uploadPtr);
- var start = 1;
- settings.uploadPtr += len;
- if (len > 0) {
- if ((buf[1] == 0) || (buf[1] == 123)) { start = 0; buf[0] = 0; len++; } // If the buffer starts with 0 or 123, we must add an extra 0 at the start of the buffer
- settings.inFlight++;
- settings.tunnelws.send(buf.slice(start, start + len));
- } else {
- console.log('Upload done, ' + settings.uploadPtr + ' bytes sent.');
- if (settings.uploadFile != null) { require('fs').closeSync(settings.uploadFile); delete settings.uploadFile; }
- if (settings.inFlight == 0) { process.exit(); return; } // File is closed, if there is no more in-flight data, exit.
- }
- }
-
- } else if (cmd.action == 'uploaderror') {
- if (settings.uploadFile != null) { require('fs').closeSync(settings.uploadFile); }
- console.log('Upload error.');
- process.exit();
- }
- }
- } else if (settings.tunnelwsstate == 0) {
- var data = rawdata.toString();
- if (data == 'c') { console.log('Connected.'); } else if (data == 'cr') { console.log('Connected, session is being recorded.'); } else return;
- settings.tunnelwsstate = 1;
- settings.tunnelws.send('5'); // Files
- settings.uploadSize = require('fs').statSync(args.file).size;
- settings.uploadFile = require('fs').openSync(args.file, 'r');
- settings.uploadPtr = 0;
- settings.inFlight = 1;
- console.log('Uploading...');
- settings.tunnelws.send(JSON.stringify({ action: 'upload', reqid: 'up', path: args.target, name: require('path').basename(args.file), size: settings.uploadSize }));
- }
- });
- } else if (settings.cmd == 'download') {
- // This code does all of the work for a file download
- // node meshctrl download --id oL4Y6Eg0qjnpHFrp1AxfxnBPenbDGnDSkC@HSOnAheIyd51pKhqSCUgJZakzwfKl --file c:\temp\MC-8Languages.png --target c:\temp\bob.png
- settings.tunnelws.on('message', function (rawdata) {
- if (settings.tunnelwsstate == 1) {
- if ((rawdata.length > 0) && (rawdata.toString()[0] != '{')) {
- // This is binary data, this test is ok because 4 first bytes is a control value.
- if ((rawdata.length > 4) && (settings.downloadFile != null)) { settings.downloadSize += (rawdata.length - 4); require('fs').writeSync(settings.downloadFile, rawdata, 4, rawdata.length - 4); }
- if ((rawdata[3] & 1) != 0) { // Check end flag
- // File is done, close everything.
- if (settings.downloadFile != null) { require('fs').closeSync(settings.downloadFile); }
- console.log('Download completed, ' + settings.downloadSize + ' bytes written.');
- process.exit();
- } else {
- settings.tunnelws.send(JSON.stringify({ action: 'download', sub: 'ack', id: args.file })); // Send the ACK
- }
- } else {
- // This is text data
- var cmd = null;
- try { cmd = JSON.parse(rawdata.toString()); } catch (ex) { return; }
- if (cmd.action == 'download') {
- if (cmd.id != args.file) return;
- if (cmd.sub == 'start') {
- if ((args.target.endsWith('\\')) || (args.target.endsWith('/'))) { args.target += path.parse(args.file).name; }
- try { settings.downloadFile = require('fs').openSync(args.target, 'w'); } catch (ex) { console.log("Unable to create file: " + args.target); process.exit(); return; }
- settings.downloadSize = 0;
- settings.tunnelws.send(JSON.stringify({ action: 'download', sub: 'startack', id: args.file }));
- console.log('Download started: ' + args.target);
- } else if (cmd.sub == 'cancel') {
- if (settings.downloadFile != null) { require('fs').closeSync(settings.downloadFile); }
- console.log('Download canceled.');
- process.exit();
- }
- }
- }
- } else if (settings.tunnelwsstate == 0) {
- var data = rawdata.toString();
- if (data == 'c') { console.log('Connected.'); } else if (data == 'cr') { console.log('Connected, session is being recorded.'); } else return;
- settings.tunnelwsstate = 1;
- settings.tunnelws.send('5'); // Files
- settings.tunnelws.send(JSON.stringify({ action: 'download', sub: 'start', id: args.file, path: args.file }));
- }
- });
- }
-}
-
-// Encode an object as a cookie using a key using AES-GCM. (key must be 32 bytes or more)
-function encodeCookie(o, key) {
- try {
- if (key == null) { return null; }
- o.time = Math.floor(Date.now() / 1000); // Add the cookie creation time
- const iv = Buffer.from(crypto.randomBytes(12), 'binary'), cipher = crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv);
- const crypted = Buffer.concat([cipher.update(JSON.stringify(o), 'utf8'), cipher.final()]);
- return Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
- } catch (e) { return null; }
-}
-
-// Generate a random Intel AMT password
-function checkAmtPassword(p) { return (p.length > 7) && (/\d/.test(p)) && (/[a-z]/.test(p)) && (/[A-Z]/.test(p)) && (/\W/.test(p)); }
-function getRandomAmtPassword() { var p; do { p = Buffer.from(crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); } while (checkAmtPassword(p) == false); return p; }
-function getRandomHex(count) { return Buffer.from(crypto.randomBytes(count), 'binary').toString('hex'); }
-function format(format) { var args = Array.prototype.slice.call(arguments, 1); return format.replace(/{(\d+)}/g, function (match, number) { return typeof args[number] != 'undefined' ? args[number] : match; }); };
-function winRemoveSingleQuotes(str) { if (process.platform != 'win32') return str; else return str.split('\'').join(''); }
-
-function csvFormatArray(x) {
- var y = [];
- for (var i in x) { if ((x[i] == null) || (x[i] == '')) { y.push(''); } else { y.push('"' + x[i].split('"').join('') + '"'); } }
- return y.join(',');
-}
-
-function displayDeviceInfo(sysinfo, lastconnect, network, nodes) {
- //console.log('displayDeviceInfo', sysinfo, lastconnect, network, nodes);
-
- // Fetch the node information
- var node = null;;
- if (sysinfo != null && (sysinfo.node != null)) {
- // Node information came with system information
- node = sysinfo.node;
- } else {
- // This device does not have system information, get node information from the nodes list.
- for (var m in nodes.nodes) {
- for (var n in nodes.nodes[m]) {
- if (nodes.nodes[m][n]._id.indexOf(args.id) >= 0) { node = nodes.nodes[m][n]; }
- }
- }
- }
- if ((sysinfo == null && lastconnect == null && network == null) || (node == null)) {
- console.log("Invalid device id");
- process.exit(); return;
- }
-
- var info = {};
-
- //if (network != null) { sysinfo.netif = network.netif; }
- if (lastconnect != null) { node.lastconnect = lastconnect.time; node.lastaddr = lastconnect.addr; }
- if (args.raw) { console.log(JSON.stringify(sysinfo, ' ', 2)); return; }
-
- // General
- var output = {}, outputCount = 0;
- if (node.name) { output["Server Name"] = node.name; outputCount++; }
- if (node.rname) { output["Computer Name"] = node.rname; outputCount++; }
- if (node.host != null) { output["Hostname"] = node.host; outputCount++; }
- if (node.ip != null) { output["IP Address"] = node.ip; outputCount++; }
- if (node.desc != null) { output["Description"] = node.desc; outputCount++; }
- if (node.icon != null) { output["Icon"] = node.icon; outputCount++; }
- if (node.tags) { output["Tags"] = node.tags; outputCount++; }
- if (node.av) {
- var av = [];
- for (var i in node.av) {
- if (typeof node.av[i]['product'] == 'string') {
- var n = node.av[i]['product'];
- if (node.av[i]['updated'] === true) { n += ', updated'; }
- if (node.av[i]['updated'] === false) { n += ', not updated'; }
- if (node.av[i]['enabled'] === true) { n += ', enabled'; }
- if (node.av[i]['enabled'] === false) { n += ', disabled'; }
- av.push(n);
- }
- }
- output["AntiVirus"] = av; outputCount++;
- }
- if (typeof node.wsc == 'object') {
- output["WindowsSecurityCenter"] = node.wsc; outputCount++;
- }
- if (outputCount > 0) { info["General"] = output; }
-
- // Operating System
- var hardware = null;
- if ((sysinfo != null) && (sysinfo.hardware != null)) { hardware = sysinfo.hardware; }
- if ((hardware && hardware.windows && hardware.windows.osinfo) || node.osdesc) {
- var output = {}, outputCount = 0;
- if (node.rname) { output["Name"] = node.rname; outputCount++; }
- if (node.osdesc) { output["Version"] = node.osdesc; outputCount++; }
- if (hardware && hardware.windows && hardware.windows.osinfo) { var m = hardware.windows.osinfo; if (m.OSArchitecture) { output["Architecture"] = m.OSArchitecture; outputCount++; } }
- if (outputCount > 0) { info["Operating System"] = output; }
- }
-
- // MeshAgent
- if (node.agent) {
- var output = {}, outputCount = 0;
- var agentsStr = ["Unknown", "Windows 32bit console", "Windows 64bit console", "Windows 32bit service", "Windows 64bit service", "Linux 32bit", "Linux 64bit", "MIPS", "XENx86", "Android", "Linux ARM", "macOS x86-32bit", "Android x86", "PogoPlug ARM", "Android", "Linux Poky x86-32bit", "macOS x86-64bit", "ChromeOS", "Linux Poky x86-64bit", "Linux NoKVM x86-32bit", "Linux NoKVM x86-64bit", "Windows MinCore console", "Windows MinCore service", "NodeJS", "ARM-Linaro", "ARMv6l / ARMv7l", "ARMv8 64bit", "ARMv6l / ARMv7l / NoKVM", "MIPS24KC (OpenWRT)", "Apple Silicon", "FreeBSD x86-64", "Unknown", "Linux ARM 64 bit (glibc/2.24 NOKVM)", "Alpine Linux x86 64 Bit (MUSL)", "Assistant (Windows)", "Armada370 - ARM32/HF (libc/2.26)", "OpenWRT x86-64", "OpenBSD x86-64", "Unknown", "Unknown", "MIPSEL24KC (OpenWRT)", "ARMADA/CORTEX-A53/MUSL (OpenWRT)", "Windows ARM 64bit console", "Windows ARM 64bit service", "ARMVIRT32 (OpenWRT)", "RISC-V x86-64"];
- if ((node.agent != null) && (node.agent.id != null) && (node.agent.ver != null)) {
- var str = '';
- if (node.agent.id <= agentsStr.length) { str = agentsStr[node.agent.id]; } else { str = agentsStr[0]; }
- if (node.agent.ver != 0) { str += ' v' + node.agent.ver; }
- output["Mesh Agent"] = str; outputCount++;
- }
- if ((node.conn & 1) != 0) {
- output["Last agent connection"] = "Connected now"; outputCount++;
- } else {
- if (node.lastconnect) { output["Last agent connection"] = new Date(node.lastconnect).toLocaleString(); outputCount++; }
- }
- if (node.lastaddr) {
- var splitip = node.lastaddr.split(':');
- if (splitip.length > 2) {
- output["Last agent address"] = node.lastaddr; outputCount++; // IPv6
- } else {
- output["Last agent address"] = splitip[0]; outputCount++; // IPv4
- }
- }
- if ((node.agent != null) && (node.agent.tag != null)) {
- output["Tag"] = node.agent.tag; outputCount++;
- }
- if (outputCount > 0) { info["Mesh Agent"] = output; }
- }
-
- // Networking
- if (network.netif != null) {
- var output = {}, outputCount = 0, minfo = {};
- for (var i in network.netif) {
- var m = network.netif[i], moutput = {}, moutputCount = 0;
- if (m.desc) { moutput["Description"] = m.desc; moutputCount++; }
- if (m.mac) {
- if (m.gatewaymac) {
- moutput["MAC Layer"] = format("MAC: {0}, Gateway: {1}", m.mac, m.gatewaymac); moutputCount++;
- } else {
- moutput["MAC Layer"] = format("MAC: {0}", m.mac); moutputCount++;
- }
- }
- if (m.v4addr && (m.v4addr != '0.0.0.0')) {
- if (m.v4gateway && m.v4mask) {
- moutput["IPv4 Layer"] = format("IP: {0}, Mask: {1}, Gateway: {2}", m.v4addr, m.v4mask, m.v4gateway); moutputCount++;
- } else {
- moutput["IPv4 Layer"] = format("IP: {0}", m.v4addr); moutputCount++;
- }
- }
- if (moutputCount > 0) { minfo[m.name + (m.dnssuffix ? (', ' + m.dnssuffix) : '')] = moutput; info["Networking"] = minfo; }
- }
- }
-
- if (network.netif2 != null) {
- var minfo = {};
- for (var i in network.netif2) {
- var m = network.netif2[i], moutput = {}, moutputCount = 0;
-
- if (Array.isArray(m) == false ||
- m.length < 1 ||
- m[0] == null ||
- ((typeof m[0].mac == 'string') && (m[0].mac.startsWith('00:00:00:00')))
- )
- continue;
-
- var ifTitle = '' + i;
- if (m[0].fqdn != null && m[0].fqdn != '') ifTitle += ', ' + m[0].fqdn;
-
- if (typeof m[0].mac == 'string') {
- if (m[0].gatewaymac) {
- moutput['MAC Layer'] = format("MAC: {0}, Gateway: {1}", m[0].mac, m[0].gatewaymac);
- } else {
- moutput['MAC Layer'] = format("MAC: {0}", m[0].mac);
- }
- moutputCount++;
- }
-
- moutput['IPv4 Layer'] = '';
- moutput['IPv6 Layer'] = '';
- for (var j = 0; j < m.length; j++) {
- var iplayer = m[j];
- if (iplayer.family == 'IPv4' || iplayer.family == 'IPv6') {
- if (iplayer.gateway && iplayer.netmask) {
- moutput[iplayer.family + ' Layer'] += format("IP: {0}, Mask: {1}, Gateway: {2} ", iplayer.address, iplayer.netmask, iplayer.gateway);
- moutputCount++;
- } else {
- if (iplayer.address) {
- moutput[iplayer.family + ' Layer'] += format("IP: {0} ", iplayer.address);
- moutputCount++;
- }
- }
- }
- }
- if (moutput['IPv4 Layer'] == '') delete moutput['IPv4 Layer'];
- if (moutput['IPv6 Layer'] == '') delete moutput['IPv6 Layer'];
- if (moutputCount > 0) {
- minfo[ifTitle] = moutput;
- info["Networking"] = minfo;
- }
- }
- }
-
- // Intel AMT
- if (node.intelamt != null) {
- var output = {}, outputCount = 0;
- output["Version"] = (node.intelamt.ver) ? ('v' + node.intelamt.ver) : ('' + "Unknown" + ''); outputCount++;
- var provisioningStates = { 0: "Not Activated (Pre)", 1: "Not Activated (In)", 2: "Activated" };
- var provisioningMode = '';
- if ((node.intelamt.state == 2) && node.intelamt.flags) { if (node.intelamt.flags & 2) { provisioningMode = (', ' + "Client Control Mode (CCM)"); } else if (node.intelamt.flags & 4) { provisioningMode = (', ' + "Admin Control Mode (ACM)"); } }
- output["Provisioning State"] = ((node.intelamt.state) ? (provisioningStates[node.intelamt.state]) : ('' + "Unknown" + '')) + provisioningMode; outputCount++;
- output["Security"] = (node.intelamt.tls == 1) ? "Secured using TLS" : "TLS is not setup"; outputCount++;
- output["Admin Credentials"] = (node.intelamt.user == null || node.intelamt.user == '') ? "Not Known" : "Known"; outputCount++;
- if (outputCount > 0) { info["Intel Active Management Technology (Intel AMT)"] = output; }
- }
-
- if (hardware != null) {
- if (hardware.identifiers) {
- var output = {}, outputCount = 0, ident = hardware.identifiers;
- // BIOS
- if (ident.bios_vendor) { output["Vendor"] = ident.bios_vendor; outputCount++; }
- if (ident.bios_version) { output["Version"] = ident.bios_version; outputCount++; }
- if (outputCount > 0) { info["BIOS"] = output; }
- output = {}, outputCount = 0;
-
- // Motherboard
- if (ident.board_vendor) { output["Vendor"] = ident.board_vendor; outputCount++; }
- if (ident.board_name) { output["Name"] = ident.board_name; outputCount++; }
- if (ident.board_serial && (ident.board_serial != '')) { output["Serial"] = ident.board_serial; outputCount++; }
- if (ident.board_version) { output["Version"] = ident.board_version; }
- if (ident.product_uuid) { output["Identifier"] = ident.product_uuid; }
- if (ident.cpu_name) { output["CPU"] = ident.cpu_name; }
- if (ident.gpu_name) { for (var i in ident.gpu_name) { output["GPU" + (parseInt(i) + 1)] = ident.gpu_name[i]; } }
- if (outputCount > 0) { info["Motherboard"] = output; }
- }
-
- // Memory
- if (hardware.windows) {
- if (hardware.windows.memory) {
- var output = {}, outputCount = 0, minfo = {};
- hardware.windows.memory.sort(function (a, b) { if (a.BankLabel > b.BankLabel) return 1; if (a.BankLabel < b.BankLabel) return -1; return 0; });
- for (var i in hardware.windows.memory) {
- var m = hardware.windows.memory[i], moutput = {}, moutputCount = 0;
- if (m.Capacity) { moutput["Capacity/Speed"] = (m.Capacity / 1024 / 1024) + " Mb, " + m.Speed + " Mhz"; moutputCount++; }
- if (m.PartNumber) { moutput["Part Number"] = ((m.Manufacturer && m.Manufacturer != 'Undefined') ? (m.Manufacturer + ', ') : '') + m.PartNumber; moutputCount++; }
- if (moutputCount > 0) { minfo[m.BankLabel] = moutput; info["Memory"] = minfo; }
- }
- }
- }
-
- // Storage
- if (hardware.identifiers && ident.storage_devices) {
- var output = {}, outputCount = 0, minfo = {};
- // Sort Storage
- ident.storage_devices.sort(function (a, b) { if (a.Caption > b.Caption) return 1; if (a.Caption < b.Caption) return -1; return 0; });
- for (var i in ident.storage_devices) {
- var m = ident.storage_devices[i], moutput = {};
- if (m.Size) {
- if (m.Model && (m.Model != m.Caption)) { moutput["Model"] = m.Model; outputCount++; }
- if ((typeof m.Size == 'string') && (parseInt(m.Size) == m.Size)) { m.Size = parseInt(m.Size); }
- if (typeof m.Size == 'number') { moutput["Capacity"] = Math.floor(m.Size / 1024 / 1024) + 'Mb'; outputCount++; }
- if (typeof m.Size == 'string') { moutput["Capacity"] = m.Size; outputCount++; }
- if (moutputCount > 0) { minfo[m.Caption] = moutput; info["Storage"] = minfo; }
- }
- }
- }
- }
-
- // Display everything
- if (args.json) {
- console.log(JSON.stringify(info, ' ', 2));
- } else {
- for (var i in info) {
- console.log('--- ' + i + ' ---');
- for (var j in info[i]) {
- if ((typeof info[i][j] == 'string') || (typeof info[i][j] == 'number')) {
- console.log(' ' + j + ': ' + info[i][j]);
- } else {
- console.log(' ' + j + ':');
- for (var k in info[i][j]) {
- console.log(' ' + k + ': ' + info[i][j][k]);
- }
- }
- }
- }
- }
-}
-
-// Read the Mesh Agent error log and index it.
-function indexAgentErrorLog() {
- // Index the messages
- const lines = require('fs').readFileSync('../meshcentral-data/agenterrorlogs.txt', { encoding: 'utf8', flag: 'r' }).split('\r\n');
- var errorIndex = {}; // "msg" --> [ { lineNumber, elemenetNumber } ]
- for (var i = 0; i < lines.length; i++) {
- const line = lines[i];
- if (line.length > 88) {
- var nodeid = line.substring(0, 70);
- var fetchTime = parseInt(line.substring(72, 85));
- var data = JSON.parse(line.substring(87));
- if ((data != null) && (data.action == 'errorlog') && (Array.isArray(data.log))) {
- for (var j = 0; j < data.log.length; j++) {
- var entry = data.log[j];
- if ((entry != null) && (typeof entry.t == 'number') && (typeof entry.m == 'string')) {
- const msg = entry.m;
- if (errorIndex[msg] == null) { errorIndex[msg] = []; }
- errorIndex[msg].push({ l: i, e: j });
- }
- }
- }
- }
- }
-
- // Sort the messages by frequency
- var errorIndexCount = []; // [ { m: "msg", c: count } ]
- for (var i in errorIndex) { errorIndexCount.push({ m: i, c: errorIndex[i].length }); }
- errorIndexCount = errorIndexCount.sort(function (a, b) { return b.c - a.c })
-
- // Display the results
- for (var i = 0; i < errorIndexCount.length; i++) {
- const m = errorIndexCount[i].m;
- if ((m.indexOf('STUCK') >= 0) || (m.indexOf('FATAL') >= 0)) { console.log(errorIndexCount[i].c, m); }
- }
-}
+#!/usr/bin/env node
+
+/**
+* @description MeshCentral command line tool
+* @author Ylian Saint-Hilaire
+* @copyright Intel Corporation 2018-2022
+* @license Apache-2.0
+* @version v0.0.1
+*/
+
+// Make sure we have the dependency modules
+try { require('minimist'); } catch (ex) { console.log('Missing module "minimist", type "npm install minimist" to install it.'); return; }
+try { require('ws'); } catch (ex) { console.log('Missing module "ws", type "npm install ws" to install it.'); return; }
+
+var settings = {};
+const crypto = require('crypto');
+const args = require('minimist')(process.argv.slice(2));
+const path = require('path');
+const possibleCommands = ['edituser', 'listusers', 'listusersessions', 'listdevicegroups', 'listdevices', 'listusersofdevicegroup', 'listevents', 'logintokens', 'serverinfo', 'userinfo', 'adduser', 'removeuser', 'adddevicegroup', 'removedevicegroup', 'editdevicegroup', 'broadcast', 'showevents', 'addusertodevicegroup', 'removeuserfromdevicegroup', 'addusertodevice', 'removeuserfromdevice', 'sendinviteemail', 'generateinvitelink', 'config', 'movetodevicegroup', 'deviceinfo', 'removedevice', 'editdevice', 'addlocaldevice', 'addamtdevice', 'addusergroup', 'listusergroups', 'removeusergroup', 'runcommand', 'shell', 'upload', 'download', 'deviceopenurl', 'devicemessage', 'devicetoast', 'addtousergroup', 'removefromusergroup', 'removeallusersfromusergroup', 'devicesharing', 'devicepower', 'indexagenterrorlog', 'agentdownload', 'report', 'grouptoast', 'groupmessage', 'webrelay'];
+if (args.proxy != null) { try { require('https-proxy-agent'); } catch (ex) { console.log('Missing module "https-proxy-agent", type "npm install https-proxy-agent" to install it.'); return; } }
+
+if (args['_'].length == 0) {
+ console.log("MeshCtrl performs command line actions on a MeshCentral server.");
+ console.log("Information at: https://meshcentral.com");
+ console.log("No action specified, use MeshCtrl like this:\r\n\r\n meshctrl [action] [arguments]\r\n");
+ console.log("Supported actions:");
+ console.log(" Help [action] - Get help on an action.");
+ console.log(" ServerInfo - Show server information.");
+ console.log(" UserInfo - Show user information.");
+ console.log(" ListUsers - List user accounts.");
+ console.log(" ListUserSessions - List online users.");
+ console.log(" ListUserGroups - List user groups.");
+ console.log(" ListDevices - List devices.");
+ console.log(" ListDeviceGroups - List device groups.");
+ console.log(" ListUsersOfDeviceGroup - List the users in a device group.");
+ console.log(" ListEvents - List server events.");
+ console.log(" LoginTokens - List, create and remove login tokens.");
+ console.log(" DeviceInfo - Show information about a device.");
+ console.log(" AddLocalDevice - Add a local device.");
+ console.log(" AddAmtDevice - Add a AMT device.");
+ console.log(" EditDevice - Make changes to a device.");
+ console.log(" RemoveDevice - Delete a device.");
+ console.log(" Config - Perform operation on config.json file.");
+ console.log(" AddUser - Create a new user account.");
+ console.log(" EditUser - Change a user account.");
+ console.log(" RemoveUser - Delete a user account.");
+ console.log(" AddUserGroup - Create a new user group.");
+ console.log(" RemoveUserGroup - Delete a user group.");
+ console.log(" AddToUserGroup - Add a user, device or device group to a user group.");
+ console.log(" RemoveFromUserGroup - Remove a user, device or device group from a user group.");
+ console.log(" RemoveAllUsersFromUserGroup - Remove all users from a user group.");
+ console.log(" AddDeviceGroup - Create a new device group.");
+ console.log(" RemoveDeviceGroup - Delete a device group.");
+ console.log(" EditDeviceGroup - Change a device group values.");
+ console.log(" MoveToDeviceGroup - Move a device to a different device group.");
+ console.log(" AddUserToDeviceGroup - Add a user to a device group.");
+ console.log(" RemoveUserFromDeviceGroup - Remove a user from a device group.");
+ console.log(" AddUserToDevice - Add a user to a device.");
+ console.log(" RemoveUserFromDevice - Remove a user from a device.");
+ console.log(" SendInviteEmail - Send an agent install invitation email.");
+ console.log(" GenerateInviteLink - Create an invitation link.");
+ console.log(" Broadcast - Display a message to all online users.");
+ console.log(" ShowEvents - Display real-time server events in JSON format.");
+ console.log(" RunCommand - Run a shell command on a remote device.");
+ console.log(" Shell - Access command shell of a remote device.");
+ console.log(" Upload - Upload a file to a remote device.");
+ console.log(" Download - Download a file from a remote device.");
+ console.log(" WebRelay - Creates a HTTP/HTTPS webrelay link for a remote device.");
+ console.log(" DeviceOpenUrl - Open a URL on a remote device.");
+ console.log(" DeviceMessage - Open a message box on a remote device.");
+ console.log(" DeviceToast - Display a toast notification on a remote device.");
+ console.log(" GroupMessage - Open a message box on remote devices in a specific device group.");
+ console.log(" GroupToast - Display a toast notification on remote devices in a specific device group.");
+ console.log(" DevicePower - Perform wake/sleep/reset/off operations on remote devices.");
+ console.log(" DeviceSharing - View, add and remove sharing links for a given device.");
+ console.log(" AgentDownload - Download an agent of a specific type for a device group.");
+ console.log(" Report - Create and show a CSV report.");
+ console.log("\r\nSupported login arguments:");
+ console.log(" --url [wss://server] - Server url, wss://localhost:443 is default.");
+ console.log(" - Use wss://localhost:443?key=xxx if login key is required.");
+ console.log(" --loginuser [username] - Login username, admin is default.");
+ console.log(" --loginpass [password] - Login password OR Leave blank to enter password at prompt");
+ console.log(" --token [number] - 2nd factor authentication token.");
+ console.log(" --loginkey [hex] - Server login key in hex.");
+ console.log(" --loginkeyfile [file] - File containing server login key in hex.");
+ console.log(" --logindomain [domainid] - Domain id, default is empty, only used with loginkey.");
+ console.log(" --proxy [http://proxy:123] - Specify an HTTP proxy.");
+ return;
+} else {
+ settings.cmd = args['_'][0].toLowerCase();
+ if ((possibleCommands.indexOf(settings.cmd) == -1) && (settings.cmd != 'help')) { console.log("Invalid command. Possible commands are: " + possibleCommands.join(', ') + '.'); return; }
+ //console.log(settings.cmd);
+
+ var ok = false;
+ switch (settings.cmd) {
+ case 'config': { performConfigOperations(args); return; }
+ case 'indexagenterrorlog': { indexAgentErrorLog(); return; }
+ case 'serverinfo': { ok = true; break; }
+ case 'userinfo': { ok = true; break; }
+ case 'listusers': { ok = true; break; }
+ case 'listusersessions': { ok = true; break; }
+ case 'listusergroups': { ok = true; break; }
+ case 'listdevicegroups': { ok = true; break; }
+ case 'listdevices': { ok = true; break; }
+ case 'listevents': { ok = true; break; }
+ case 'logintokens': { ok = true; break; }
+ case 'listusersofdevicegroup':
+ case 'deviceinfo':
+ case 'removedevice':
+ case 'editdevice': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else { ok = true; }
+ break;
+ }
+ case 'addlocaldevice': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else if (args.devicename == null) { console.log(winRemoveSingleQuotes("Missing devicename, use --devicename [devicename]")); }
+ else if (args.hostname == null) { console.log(winRemoveSingleQuotes("Missing hostname, use --hostname [hostname]")); }
+ else { ok = true; }
+ break;
+ }
+ case 'addamtdevice': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else if (args.devicename == null) { console.log(winRemoveSingleQuotes("Missing devicename, use --devicename [devicename]")); }
+ else if (args.hostname == null) { console.log(winRemoveSingleQuotes("Missing hostname, use --hostname [hostname]")); }
+ else if (args.user == null) { console.log(winRemoveSingleQuotes("Missing user, use --user [user]")); }
+ else if (args.pass == null) { console.log(winRemoveSingleQuotes("Missing pass, use --pass [pass]")); }
+ else { ok = true; }
+ break;
+ }
+ case 'addusertodevicegroup': {
+ if ((args.id == null) && (args.group == null)) { console.log(winRemoveSingleQuotes("Device group identifier missing, use --id '[groupid]' or --group [groupname]")); }
+ else if (args.userid == null) { console.log("Add user to group missing useid, use --userid [userid]"); }
+ else { ok = true; }
+ break;
+ }
+ case 'removeuserfromdevicegroup': {
+ if ((args.id == null) && (args.group == null)) { console.log(winRemoveSingleQuotes("Device group identifier missing, use --id '[groupid]' or --group [groupname]")); }
+ else if (args.userid == null) { console.log("Remove user from group missing useid, use --userid [userid]"); }
+ else { ok = true; }
+ break;
+ }
+ case 'addusertodevice': {
+ if (args.userid == null) { console.log("Add user to device missing userid, use --userid [userid]"); }
+ else if (args.id == null) { console.log(winRemoveSingleQuotes("Add user to device missing device id, use --id '[deviceid]'")); }
+ else { ok = true; }
+ break;
+ }
+ case 'removeuserfromdevice': {
+ if (args.userid == null) { console.log("Remove user from device missing userid, use --userid [userid]"); }
+ else if (args.id == null) { console.log(winRemoveSingleQuotes("Remove user from device missing device id, use --id '[deviceid]'")); }
+ else { ok = true; }
+ break;
+ }
+ case 'adddevicegroup': {
+ if (args.name == null) { console.log("Message group name, use --name [name]"); }
+ else { ok = true; }
+ break;
+ }
+ case 'editdevicegroup':
+ case 'removedevicegroup': {
+ if ((args.id == null) && (args.group == null)) { console.log(winRemoveSingleQuotes("Device group identifier missing, use --id '[groupid]' or --group [groupname]")); }
+ else { ok = true; }
+ break;
+ }
+ case 'movetodevicegroup': {
+ if ((args.id == null) && (args.group == null)) { console.log(winRemoveSingleQuotes("Device group identifier missing, use --id '[groupid]' or --group [groupname]")); }
+ else if (args.devid == null) { console.log(winRemoveSingleQuotes("Device identifier missing, use --devid '[deviceid]'")); }
+ else { ok = true; }
+ break;
+ }
+ case 'broadcast': {
+ if (args.msg == null) { console.log("Message missing, use --msg [message]"); }
+ else { ok = true; }
+ break;
+ }
+ case 'showevents': {
+ ok = true;
+ break;
+ }
+ case 'adduser': {
+ if (args.user == null) { console.log("New account name missing, use --user [name]"); }
+ else if ((args.pass == null) && (args.randompass == null)) { console.log("New account password missing, use --pass [password] or --randompass"); }
+ else { ok = true; }
+ break;
+ }
+ case 'edituser': {
+ if (args.userid == null) { console.log("Edit account user missing, use --userid [id]"); }
+ else { ok = true; }
+ break;
+ }
+ case 'removeuser': {
+ if (args.userid == null) { console.log("Remove account userid missing, use --userid [id]"); }
+ else { ok = true; }
+ break;
+ }
+ case 'addusergroup': {
+ if (args.name == null) { console.log("New user group name missing, use --name [name]"); }
+ else { ok = true; }
+ break;
+ }
+ case 'removeusergroup': {
+ if (args.groupid == null) { console.log(winRemoveSingleQuotes("Remove user group id missing, use --groupid '[id]'")); }
+ else { ok = true; }
+ break;
+ }
+ case 'addtousergroup': {
+ if (args.groupid == null) { console.log(winRemoveSingleQuotes("Group id missing, use --groupid '[id]'")); }
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing identifier to add, use --id [id]")); }
+ else { ok = true; }
+ break;
+ }
+ case 'removefromusergroup': {
+ if (args.groupid == null) { console.log(winRemoveSingleQuotes("Group id missing, use --groupid '[id]'")); }
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing identifier to remove, use --id [id]")); }
+ else { ok = true; }
+ break;
+ }
+ case 'removeallusersfromusergroup': {
+ if (args.groupid == null) { console.log(winRemoveSingleQuotes("Group id missing, use --groupid '[id]'")); }
+ else { ok = true; }
+ break;
+ }
+ case 'sendinviteemail': {
+ if ((args.id == null) && (args.group == null)) { console.log("Device group identifier missing, use --id '[groupid]' or --group [groupname]"); }
+ else if (args.email == null) { console.log("Device email is missing, use --email [email]"); }
+ else { ok = true; }
+ break;
+ }
+ case 'generateinvitelink': {
+ if ((args.id == null) && (args.group == null)) { console.log("Device group identifier missing, use --id '[groupid]' or --group [groupname]"); }
+ else if (args.hours == null) { console.log("Invitation validity period missing, use --hours [hours]"); }
+ else { ok = true; }
+ break;
+ }
+ case 'runcommand': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else if (args.run == null) { console.log("Missing run, use --run \"command\""); }
+ else { ok = true; }
+ break;
+ }
+ case 'shell': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else { ok = true; }
+ break;
+ }
+ case 'devicepower': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else { ok = true; }
+ break;
+ }
+ case 'devicesharing': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else if ((args.daily != null) && (args.weekly != null)) { console.log(winRemoveSingleQuotes("Can't specify both --daily and --weekly at the same time.")); }
+ else { ok = true; }
+ break;
+ }
+ case 'agentdownload': {
+ if (args.type == null) { console.log(winRemoveSingleQuotes("Missing device type, use --type [agenttype]")); }
+ else if ((parseInt(args.type) == null) || isNaN(parseInt(args.type)) || (parseInt(args.type) < 1) || (parseInt(args.type) > 11000)) { console.log(winRemoveSingleQuotes("Invalid agent type, must be a number.")); }
+ else if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[meshid]'")); }
+ else if ((typeof args.id != 'string') || (args.id.length != 64)) { console.log(winRemoveSingleQuotes("Invalid meshid.")); }
+ else { ok = true; }
+ break;
+ }
+ case 'upload': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else if (args.file == null) { console.log("Local file missing, use --file [file] specify the file to upload"); }
+ else if (args.target == null) { console.log("Remote target path missing, use --target [path] to specify the remote location"); }
+ else if (require('fs').existsSync(args.file) == false) { console.log("Local file does not exists, check --file"); }
+ else { ok = true; }
+ break;
+ }
+ case 'download': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else if (args.file == null) { console.log("Remote file missing, use --file [file] specify the remote file to download"); }
+ else if (args.target == null) { console.log("Target path missing, use --target [path] to specify the local download location"); }
+ else { ok = true; }
+ break;
+ }
+ case 'webrelay': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else if (args.type == null) { console.log(winRemoveSingleQuotes("Missing protocol type, use --type [http,https]")); }
+ else { ok = true; }
+ break;
+ }
+ case 'deviceopenurl': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else if (args.openurl == null) { console.log("Remote URL, use --openurl [url] specify the link to open."); }
+ else { ok = true; }
+ break;
+ }
+ case 'devicemessage': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else if (args.msg == null) { console.log("Remote message, use --msg \"[message]\" specify a remote message."); }
+ else { ok = true; }
+ break;
+ }
+ case 'devicetoast': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else if (args.msg == null) { console.log("Remote message, use --msg \"[message]\" specify a remote message."); }
+ else { ok = true; }
+ break;
+ }
+ case 'groupmessage': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device group id, use --id '[devicegroupid]'")); }
+ else if (args.msg == null) { console.log("Remote message, use --msg \"[message]\" specify a remote message."); }
+ else { ok = true; }
+ break;
+ }
+ case 'grouptoast': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device group id, use --id '[devicegroupid]'")); }
+ else if (args.msg == null) { console.log("Remote message, use --msg \"[message]\" specify a remote message."); }
+ else { ok = true; }
+ break;
+ }
+ case 'report': {
+ if (args.type == null) { console.log(winRemoveSingleQuotes("Missing report type, use --type '[reporttype]'")); }
+ else { ok = true; }
+ break;
+ }
+ case 'help': {
+ if (args['_'].length < 2) {
+ console.log("Get help on an action. Type:\r\n\r\n help [action]\r\n\r\nPossible actions are: " + possibleCommands.join(', ') + '.');
+ } else {
+ switch (args['_'][1].toLowerCase()) {
+ case 'config': {
+ displayConfigHelp();
+ break;
+ }
+ case 'sendinviteemail': {
+ console.log("Send invitation email with instructions on how to install the mesh agent for a specific device group. Example usage:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl SendInviteEmail --id 'groupid' --message \"msg\" --email user@sample.com"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl SendInviteEmail --group \"My Computers\" --name \"Jack\" --email user@sample.com"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [groupid] - Device group identifier (or --group).");
+ } else {
+ console.log(" --id '[groupid]' - Device group identifier (or --group).");
+ }
+ console.log(" --group [groupname] - Device group name (or --id).");
+ console.log(" --email [email] - Email address.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --name (name) - Name of recipient to be included in the email.");
+ console.log(" --message (msg) - Message to be included in the email.");
+ break;
+ }
+ case 'generateinvitelink': {
+ console.log("Generate a agent invitation URL for a given group. Example usage:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl GenerateInviteLink --id 'groupid' --hours 24"));
+ console.log(" MeshCtrl GenerateInviteLink --group \"My Computers\" --hours 0");
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [groupid] - Device group identifier (or --group).");
+ } else {
+ console.log(" --id '[groupid]' - Device group identifier (or --group).");
+ }
+ console.log(" --group [groupname] - Device group name (or --id).");
+ console.log(" --hours [hours] - Validity period in hours or 0 for infinite.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --flags [mode] - Mode flag for link type (0 = both, 1 = interactive only, 2 = background only)");
+ break;
+ }
+ case 'showevents': {
+ console.log("Show the server's event stream for this user account. Example usage:\r\n");
+ console.log(" MeshCtrl ShowEvents");
+ console.log(" MeshCtrl ShowEvents --filter nodeconnect");
+ console.log(" MeshCtrl ShowEvents --filter uicustomevent,changenode");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --filter [actions] - Show only specified actions.");
+ break;
+ }
+ case 'serverinfo': {
+ console.log("Get information on the MeshCentral server, Example usages:\r\n");
+ console.log(" MeshCtrl ServerInfo --loginuser myaccountname --loginpass mypassword");
+ console.log(" MeshCtrl ServerInfo --loginuser myaccountname --loginkeyfile key.txt");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --json - Show result as JSON.");
+ break;
+ }
+ case 'userinfo': {
+ console.log("Get account information for the login account, Example usages:\r\n");
+ console.log(" MeshCtrl UserInfo --loginuser myaccountname --loginpass mypassword");
+ console.log(" MeshCtrl UserInfo --loginuser myaccountname --loginkeyfile key.txt");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --json - Show result as JSON.");
+ break;
+ }
+ case 'listusers': {
+ console.log("List the account on the MeshCentral server, Example usages:\r\n");
+ console.log(" MeshCtrl ListUsers");
+ console.log(" MeshCtrl ListUsers --json");
+ console.log(" MeshCtrl ListUsers --nameexists \"bob\"");
+ console.log(" MeshCtrl ListUsers --filter 2fa");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --idexists [id] - Return 1 if id exists, 0 if not.");
+ console.log(" --nameexists [name] - Return id if name exists.");
+ console.log(" --filter [filter1,...] - Filter user names: 2FA, NO2FA.");
+ console.log(" --json - Show result as JSON.");
+ break;
+ }
+ case 'listusersessions': {
+ console.log("List active user sessions on the MeshCentral server, Example usages:\r\n");
+ console.log(" MeshCtrl ListUserSessions");
+ console.log(" MeshCtrl ListUserSessions --json");
+ break;
+ }
+ case 'listusergroups': {
+ console.log("List user groups on the MeshCentral server, Example usages:\r\n");
+ console.log(" MeshCtrl ListUserGroups");
+ console.log(" MeshCtrl ListUserGroups --json");
+ break;
+ }
+ case 'listdevicegroups': {
+ console.log("List the device groups for this account. Example usages:\r\n");
+ console.log(" MeshCtrl ListDeviceGroups ");
+ console.log(" MeshCtrl ListDeviceGroups --json");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --idexists [id] - Return 1 if id exists, 0 if not.");
+ console.log(" --nameexists [name] - Return id if name exists.");
+ console.log(" --emailexists [email] - Return id if email exists.");
+ console.log(" --hex - Display meshid in hex format.");
+ console.log(" --json - Show result as JSON.");
+ break;
+ }
+ case 'listdevices': {
+ console.log("List devices. Example usages:\r\n");
+ console.log(" MeshCtrl ListDevices");
+ console.log(winRemoveSingleQuotes(" MeshCtrl ListDevices -id '[groupid]' --json"));
+ console.log("\r\nOptional arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [groupid] - Filter by group identifier (or --group).");
+ } else {
+ console.log(" --id '[groupid]' - Filter by group identifier (or --group).");
+ }
+ console.log(" --group [groupname] - Filter by group name (or --id).");
+ console.log(" --count - Only return the device count.");
+ console.log(" --json - Show result as JSON.");
+ console.log(" --csv - Show result as comma separated values.");
+ console.log(" --filter \"[filter]\" - Filter devices using a filter string.");
+ console.log(" \"x\" - Devices with \"x\" in the name.");
+ console.log(" \"user:x or u:x\" - Devices with \"x\" in the name of currently logged in user.");
+ console.log(" \"ip:x\" - Devices \"x\" IP address.");
+ console.log(" \"group:x or g:x\" - Devices with \"x\" in device group name.");
+ console.log(" \"tag:x or t:x\" - Devices with \"x\" in device tag.");
+ console.log(" \"atag:x or a:x\" - Devices with \"x\" in device agent tag.");
+ console.log(" \"os:x\" - Devices with \"x\" in the device OS description.");
+ console.log(" \"amt:x\" - Devices with Intel AMT provisioning state (0, 1, 2).");
+ console.log(" \"desc:x\" - Devices with \"x\" in device description.");
+ console.log(" \"wsc:ok\" - Devices with Windows Security Center ok.");
+ console.log(" \"wsc:noav\" - Devices with Windows Security Center with anti-virus problem.");
+ console.log(" \"wsc:noupdate\" - Devices with Windows Security Center with update problem.");
+ console.log(" \"wsc:nofirewall\" - Devices with Windows Security Center with firewall problem.");
+ console.log(" \"wsc:any\" - Devices with Windows Security Center with any problem.");
+ console.log(" \"a and b\" - Match both conditions with precedence over OR. For example: \"lab and g:home\".");
+ console.log(" \"a or b\" - Math one of the conditions, for example: \"lab or g:home\".");
+ console.log(" --filterid [id,id...] - Show only results for devices with included id.");
+ console.log(" --details - Show all device details.");
+ break;
+ }
+ case 'listusersofdevicegroup': {
+ console.log("List users that have permissions for a given device group. Example usage:\r\n");
+ console.log(" MeshCtrl ListUserOfDeviceGroup ");
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [groupid] - Device group identifier.");
+ } else {
+ console.log(" --id '[groupid]' - Device group identifier.");
+ }
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --json - Show result as JSON.");
+ break;
+ }
+ case 'listevents': {
+ console.log("List server events optionally filtered by user or device. Example usage:\r\n");
+ console.log(" MeshCtrl ListEvents ");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --userid [name] - User account identifier.");
+ console.log(" --id [deviceid] - The device identifier.");
+ console.log(" --limit [number] - Maximum number of events to list.");
+ console.log(" --raw - Output raw data in JSON format.");
+ console.log(" --json - Give results in JSON format.");
+ break;
+ }
+ case 'logintokens': {
+ console.log("List account login tokens and allow addition and removal. Example usage:\r\n");
+ console.log(" MeshCtrl LoginTokens ");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --remove [name] - Remove a login token.");
+ console.log(" --add [name] - Add a login token.");
+ console.log(" --expire [minutes] - When adding a token, minutes until expire.");
+ console.log(" --json - Show login tokens in JSON format.");
+ break;
+ }
+ case 'adduser': {
+ console.log("Add a new user account. Example usages:\r\n");
+ console.log(" MeshCtrl AddUser --user newaccountname --pass newpassword");
+ console.log(" MeshCtrl AddUser --user newaccountname --randompass --rights full");
+ console.log("\r\nRequired arguments:\r\n");
+ console.log(" --user [name] - New account name.");
+ console.log(" --pass [password] - New account password.");
+ console.log(" --randompass - Create account with a random password.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --domain [domain] - Account domain, only for cross-domain admins.");
+ console.log(" --email [email] - New account email address.");
+ console.log(" --emailverified - New account email is verified.");
+ console.log(" --resetpass - Request password reset on next login.");
+ console.log(" --realname [name] - Set the real name for this account.");
+ console.log(" --phone [number] - Set the account phone number.");
+ console.log(" --rights [none|full|a,b,c] - Comma separated list of server permissions. Possible values:");
+ console.log(" manageusers,serverbackup,serverrestore,serverupdate,fileaccess,locked,nonewgroups,notools,usergroups,recordings,locksettings,allevents,nonewdevices");
+ break;
+ }
+ case 'edituser': {
+ console.log("Edit a user account, Example usages:\r\n");
+ console.log(" MeshCtrl EditUser --userid user --rights locked,locksettings");
+ console.log(" MeshCtrl EditUser --userid user --realname Jones");
+ console.log("\r\nRequired arguments:\r\n");
+ console.log(" --userid [name] - User account identifier.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --domain [domain] - Account domain, only for cross-domain admins.");
+ console.log(" --email [email] - Account email address.");
+ console.log(" --emailverified - Account email is verified.");
+ console.log(" --resetpass - Request password reset on next login.");
+ console.log(" --realname [name] - Set the real name for this account.");
+ console.log(" --phone [number] - Set the account phone number.");
+ console.log(" --rights [none|full|a,b,c] - Comma separated list of server permissions. Possible values:");
+ console.log(" manageusers,serverbackup,serverrestore,serverupdate,fileaccess,locked,nonewgroups,notools,usergroups,recordings,locksettings,allevents,nonewdevices");
+ break;
+ }
+ case 'removeuser': {
+ console.log("Delete a user account, Example usages:\r\n");
+ console.log(" MeshCtrl RemoveUser --userid accountid");
+ console.log("\r\nRequired arguments:\r\n");
+ console.log(" --userid [id] - Account identifier.");
+ break;
+ }
+ case 'addusergroup': {
+ console.log("Create a new user group, Example usages:\r\n");
+ console.log(" MeshCtrl AddUserGroup --name \"Test Group\"");
+ console.log("\r\nRequired arguments:\r\n");
+ console.log(" --name [name] - Name of the user group.");
+ break;
+ }
+ case 'removeusergroup': {
+ console.log("Remove a user group, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl RemoveUserGroup --groupid 'ugrp//abcdf'"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --groupid [groupid] - User group identifier.");
+ } else {
+ console.log(" --groupid '[groupid]' - User group identifier.");
+ }
+ break;
+ }
+ case 'addtousergroup': {
+ console.log("Add a user, device or device group to a user group, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl AddToUserGroup --id 'user//abcdef' --groupid 'ugrp//abcdf'"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl AddToUserGroup --id 'node//abcdef' --groupid 'ugrp//abcdf' --rights [rights]"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl AddToUserGroup --id 'mesh//abcdef' --groupid 'ugrp//abcdf' --rights [rights]"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [id] - Identifier to add.");
+ console.log(" --groupid [groupid] - User group identifier.");
+ } else {
+ console.log(" --id '[id]' - Identifier to add.");
+ console.log(" --groupid '[groupid]' - User group identifier.");
+ }
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --rights [number] - Rights granted for adding device or device group.");
+ console.log(" - 4294967295 for full admin or the sum of the following numbers.");
+ console.log(" 1 = Edit Device Group 2 = Manage Users ");
+ console.log(" 4 = Manage Computers 8 = Remote Control ");
+ console.log(" 16 = Agent Console 32 = Server Files ");
+ console.log(" 64 = Wake Device 128 = Set Notes ");
+ console.log(" 256 = Remote View Only 512 = No Terminal ");
+ console.log(" 1024 = No Files 2048 = No Intel AMT ");
+ console.log(" 4096 = Desktop Limited Input 8192 = Limit Events ");
+ console.log(" 16384 = Chat / Notify 32768 = Uninstall Agent ");
+ console.log(" 65536 = No Remote Desktop 131072 = Remote Commands ");
+ console.log(" 262144 = Reset / Power off ");
+ break;
+ }
+ case 'removefromusergroup': {
+ console.log("Remove a user, device or device group from a user group, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl RemoveUserFromUserGroup --userid 'user//abcdef' --groupid 'ugrp//abcdf'"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl RemoveUserFromUserGroup --userid 'node//abcdef' --groupid 'ugrp//abcdf'"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl RemoveUserFromUserGroup --userid 'mesh//abcdef' --groupid 'ugrp//abcdf'"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [userid] - Identifier to remove.");
+ console.log(" --groupid [groupid] - User group identifier.");
+ } else {
+ console.log(" --id '[userid]' - Identifier to remove.");
+ console.log(" --groupid '[groupid]' - User group identifier.");
+ }
+ break;
+ }
+ case 'removeallusersfromusergroup': {
+ console.log("Remove all users from a user group, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl RemoveAllUsersFromUserGroup --groupid 'ugrp//abcdf'"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --groupid [groupid] - User group identifier.");
+ } else {
+ console.log(" --groupid '[groupid]' - User group identifier.");
+ }
+ break;
+ }
+ case 'adddevicegroup': {
+ console.log("Add a device group, Example usages:\r\n");
+ console.log(" MeshCtrl AddDeviceGroup --name newgroupname");
+ console.log(" MeshCtrl AddDeviceGroup --name newgroupname --desc description --amtonly");
+ console.log(" MeshCtrl AddDeviceGroup --name newgroupname --features 1 --consent 7");
+ console.log("\r\nRequired arguments:\r\n");
+ console.log(" --name [name] - Name of the new group.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --desc [description] - New group description.");
+ console.log(" --amtonly - New group is agent-less, Intel AMT only.");
+ console.log(" --agentless - New group is agent-less only.");
+ console.log(" --features [number] - Set device group features, sum of numbers below.");
+ console.log(" 1 = Auto-Remove 2 = Hostname Sync");
+ console.log(" 4 = Record Sessions");
+ console.log(" --consent [number] - Set device group user consent, sum of numbers below.");
+ console.log(" 1 = Desktop notify user 2 = Terminal notify user ");
+ console.log(" 4 = Files notify user 8 = Desktop prompt user ");
+ console.log(" 16 = Terminal prompt user 32 = Files prompt user ");
+ console.log(" 64 = Desktop Toolbar ");
+ break;
+ }
+ case 'removedevicegroup': {
+ console.log("Remove a device group, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl RemoveDeviceGroup --id 'groupid'"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [groupid] - Device group identifier (or --group).");
+ } else {
+ console.log(" --id '[groupid]' - Device group identifier (or --group).");
+ }
+ console.log(" --group [groupname] - Device group name (or --id).");
+ break;
+ }
+ case 'editdevicegroup': {
+ console.log("Edit a device group, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl EditDeviceGroup --id 'groupid' --name \"New Name\""));
+ console.log(winRemoveSingleQuotes(" MeshCtrl EditDeviceGroup --id 'groupid' --desc \"Description\" --consent 63"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl EditDeviceGroup --id 'groupid' --invitecodes \"code1,code2\" --backgroundonly"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [groupid] - Device group identifier (or --group).");
+ } else {
+ console.log(" --id '[groupid]' - Device group identifier (or --group).");
+ }
+ console.log(" --group [groupname] - Device group name (or --id).");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --name [name] - Set new device group name.");
+ console.log(" --desc [description] - Set new device group description, blank to clear.");
+ console.log(" --flags [number] - Set device group flags, sum of the values below, 0 for none.");
+ console.log(" 1 = Auto remove device on disconnect.");
+ console.log(" 2 = Sync hostname.");
+ console.log(" --consent [number] - Set device group consent options, sum of the values below, 0 for none.");
+ console.log(" 1 = Desktop notify user.");
+ console.log(" 2 = Terminal notify user.");
+ console.log(" 4 = Files notify user.");
+ console.log(" 8 = Desktop prompt for user consent.");
+ console.log(" 16 = Terminal prompt for user consent.");
+ console.log(" 32 = Files prompt for user consent.");
+ console.log(" 64 = Desktop show connection toolbar.");
+ console.log(" --invitecodes [aa,bb] - Comma separated list of invite codes, blank to clear.");
+ console.log(" --backgroundonly - When used with invitecodes, set agent to only install in background.");
+ console.log(" --interactiveonly - When used with invitecodes, set agent to only run on demand.");
+ break;
+ }
+ case 'movetodevicegroup': {
+ console.log("Move a device to a new device group, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl MoveToDeviceGroup --devid 'deviceid' --id 'groupid'"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [groupid] - Device group identifier (or --group).");
+ } else {
+ console.log(" --id '[groupid]' - Device group identifier (or --group).");
+ }
+ console.log(" --group [groupname] - Device group name (or --id).");
+ if (process.platform == 'win32') {
+ console.log(" --devid [deviceid] - Device identifier.");
+ } else {
+ console.log(" --devid '[deviceid]' - Device identifier.");
+ }
+ break;
+ }
+ case 'addusertodevicegroup': {
+ console.log("Add a user to a device group, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl AddUserToDeviceGroup --id 'groupid' --userid userid --fullrights"));
+ console.log(" MeshCtrl AddUserToDeviceGroup --group groupname --userid userid --editgroup --manageusers");
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [groupid] - Device group identifier (or --group).");
+ } else {
+ console.log(" --id '[groupid]' - Device group identifier (or --group).");
+ }
+ console.log(" --group [groupname] - Device group name (or --id).");
+ console.log(" --userid [userid] - The user identifier.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --fullrights - Allow full rights over this device group.");
+ console.log(" --editgroup - Allow the user to edit group information.");
+ console.log(" --manageusers - Allow the user to add/remove users.");
+ console.log(" --managedevices - Allow the user to edit device information.");
+ console.log(" --remotecontrol - Allow device remote control operations.");
+ console.log(" --agentconsole - Allow agent console operations.");
+ console.log(" --serverfiles - Allow access to group server files.");
+ console.log(" --wakedevices - Allow device wake operation.");
+ console.log(" --notes - Allow editing of device notes.");
+ console.log(" --desktopviewonly - Restrict user to view-only remote desktop.");
+ console.log(" --limiteddesktop - Limit remote desktop keys.");
+ console.log(" --noterminal - Hide the terminal tab from this user.");
+ console.log(" --nofiles - Hide the files tab from this user.");
+ console.log(" --noamt - Hide the Intel AMT tab from this user.");
+ console.log(" --limitedevents - User can only see his own events.");
+ console.log(" --chatnotify - Allow chat and notification options.");
+ console.log(" --uninstall - Allow remote uninstall of the agent.");
+ if (args.limiteddesktop) { meshrights |= 4096; }
+ if (args.limitedevents) { meshrights |= 8192; }
+ if (args.chatnotify) { meshrights |= 16384; }
+ if (args.uninstall) { meshrights |= 32768; }
+
+ break;
+ }
+ case 'removeuserfromdevicegroup': {
+ console.log("Remove a user from a device group, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl RemoveuserFromDeviceGroup --id 'groupid' --userid userid"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [groupid] - Device group identifier (or --group).");
+ } else {
+ console.log(" --id '[groupid]' - Device group identifier (or --group).");
+ }
+ console.log(" --group [groupname] - Device group name (or --id).");
+ console.log(" --userid [userid] - The user identifier.");
+ break;
+ }
+ case 'addusertodevice': {
+ console.log("Add a user to a device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl AddUserToDevice --id 'deviceid' --userid userid --fullrights"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl AddUserToDevice --id 'deviceid' --userid userid --remotecontrol"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log(" --userid [userid] - The user identifier.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --fullrights - Allow full rights over this device.");
+ console.log(" --remotecontrol - Allow device remote control operations.");
+ console.log(" --agentconsole - Allow agent console operations.");
+ console.log(" --serverfiles - Allow access to group server files.");
+ console.log(" --wakedevices - Allow device wake operation.");
+ console.log(" --notes - Allow editing of device notes.");
+ console.log(" --desktopviewonly - Restrict user to view-only remote desktop.");
+ console.log(" --limiteddesktop - Limit remote desktop keys.");
+ console.log(" --noterminal - Hide the terminal tab from this user.");
+ console.log(" --nofiles - Hide the files tab from this user.");
+ console.log(" --noamt - Hide the Intel AMT tab from this user.");
+ console.log(" --limitedevents - User can only see his own events.");
+ console.log(" --chatnotify - Allow chat and notification options.");
+ console.log(" --uninstall - Allow remote uninstall of the agent.");
+ break;
+ }
+ case 'removeuserfromdevice': {
+ console.log("Remove a user from a device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl RemoveuserFromDeviceGroup --id 'deviceid' --userid userid"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log(" --userid [userid] - The user identifier.");
+ break;
+ }
+ case 'broadcast': {
+ console.log("Display a message to one or all logged in users, Example usages:\r\n");
+ console.log(" MeshCtrl Broadcast --msg \"This is a test\"");
+ console.log("\r\nRequired arguments:\r\n");
+ console.log(" --msg [message] - Message to display.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --user [userid] - Send the message to the specified user.");
+ break;
+ }
+ case 'deviceinfo': {
+ console.log("Display information about a device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceInfo --id 'deviceid'"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceInfo --id 'deviceid' --json"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --raw - Output raw data in JSON format.");
+ console.log(" --json - Give results in JSON format.");
+ break;
+ }
+ case 'removedevice': {
+ console.log("Delete a device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl RemoveDevice --id 'deviceid'"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ break;
+ }
+ case 'addlocaldevice': {
+ console.log("Add a Local Device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl AddLocalDevice --id 'meshid' --devicename 'devicename' --hostname 'hostname'"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl AddLocalDevice --id 'meshid' --devicename 'devicename' --hostname 'hostname' --type 6"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [meshid] - The mesh identifier.");
+ console.log(" --devicename [devicename] - The device name.");
+ console.log(" --hostname [hostname] - The devices hostname or ip address.");
+ } else {
+ console.log(" --id '[meshid]' - The mesh identifier.");
+ console.log(" --devicename '[devicename]' - The device name.");
+ console.log(" --hostname '[hostname]' - The devices hostname or ip address.");
+ }
+
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --type [TypeNumber] - With the following choices:");
+ console.log(" type 4 - Default, Windows (RDP)");
+ console.log(" type 6 - Linux (SSH/SCP/VNC)");
+ console.log(" type 29 - macOS (SSH/SCP/VNC)");
+ break;
+ }
+ case 'addamtdevice': {
+ console.log("Add an Intel AMT Device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl AddAmtDevice --id 'meshid' --devicename 'devicename' --hostname 'hostname --user 'admin' --pass 'admin'"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl AddAmtDevice --id 'meshid' --devicename 'devicename' --hostname 'hostname --user 'admin' --pass 'admin' --notls"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [meshid] - The mesh identifier.");
+ console.log(" --devicename [devicename] - The device name.");
+ console.log(" --hostname [hostname] - The devices hostname or ip address.");
+ console.log(" --user [user] - The devices AMT username.");
+ console.log(" --pass [pass] - The devices AMT password.");
+ console.log("")
+ } else {
+ console.log(" --id '[meshid]' - The mesh identifier.");
+ console.log(" --devicename '[devicename]' - The device name.");
+ console.log(" --hostname '[hostname]' - The devices hostname or ip address.");
+ console.log(" --user '[user]' - The devices AMT username.");
+ console.log(" --pass '[pass]' - The devices AMT password.");
+ }
+ console.log("\r\nOptional arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --notls - Use No TLS Security.");
+ } else {
+ console.log(" --notls - Use No TLS Security.");
+ }
+ break;
+ }
+ case 'editdevice': {
+ console.log("Change information about a device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl EditDevice --id 'deviceid' --name 'device1'"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log("\r\nOptional arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --name [name] - Change device name.");
+ console.log(" --desc [description] - Change device description.");
+ console.log(" --tags [tag1,tags2] - Change device tags.");
+ } else {
+ console.log(" --name '[name]' - Change device name.");
+ console.log(" --desc '[description]' - Change device description.");
+ console.log(" --tags '[tag1,tags2]' - Change device tags.");
+ }
+ console.log(" --icon [number] - Change the device icon (1 to 8).");
+ console.log(" --consent [flags] - Sum of the following numbers:");
+ console.log(" 1 = Desktop notify 2 = Terminal notify");
+ console.log(" 4 = Files notify 8 = Desktop prompt");
+ console.log(" 16 = Terminal prompt 32 = Files prompt");
+ console.log(" 64 = Desktop privacy bar");
+ break;
+ }
+ case 'runcommand': {
+ console.log("Run a shell command on a remote device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl RunCommand --id 'deviceid' --run \"command\""));
+ console.log(winRemoveSingleQuotes(" MeshCtrl RunCommand --id 'deviceid' --run \"command\" --powershell"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl RunCommand --id 'deviceid' --run \"command\" --reply"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log(" --run \"[command]\" - Shell command to execute on the remote device.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --powershell - Run in Windows PowerShell.");
+ console.log(" --runasuser - Attempt to run the command as logged in user.");
+ console.log(" --runasuseronly - Only run the command as the logged in user.");
+ console.log(" --reply - Return with the output from running the command.");
+ break;
+ }
+ case 'shell': {
+ console.log("Access a command shell on a remote device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl Shell --id 'deviceid'"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl Shell --id 'deviceid' --powershell"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --powershell - Run a Windows PowerShell.");
+ break;
+ }
+ case 'devicepower': {
+ console.log("Perform power operations on remote devices, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl DevicePower --wake --id 'deviceid'"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl DevicePower --sleep --id 'deviceid'"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl DevicePower --reset --id 'deviceid'"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl DevicePower --off --id 'deviceid1,deviceid2'"));
+ console.log("\r\nNote that some power operations may take up to a minute to execute.\r\n");
+ console.log("Required arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid1,deviceid2] - Device identifiers.");
+ } else {
+ console.log(" --id '[deviceid1,deviceid2]' - Device identifiers.");
+ }
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --wake - Attempt to wake up the remote device.");
+ console.log(" --reset - Attempt to remote the remote device.");
+ console.log(" --sleep - Attempt to place the remote device in low power mode.");
+ console.log(" --off - Attempt to power off the remote device.");
+ console.log(" --amtoff - Attempt to power off the remote device using Intel AMT.");
+ console.log(" --amton - Attempt to power on the remote device using Intel AMT.");
+ console.log(" --amtreset - Attempt to reset the remote device using Intel AMT.");
+ break;
+ }
+ case 'devicesharing': {
+ var tzoffset = (new Date()).getTimezoneOffset() * 60000; // Offset in milliseconds
+ var localISOTime = (new Date(Date.now() - tzoffset)).toISOString().slice(0, -5);
+ console.log("List sharing links for a specified device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid'"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --remove abcdef"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --add Guest --start " + localISOTime + " --duration 30"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --add Guest --start " + localISOTime + " --duration 30 --daily"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --add Guest --type desktop,terminal --consent prompt"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --add Guest --type http --port 80"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --remove [shareid] - Remove a device sharing link.");
+ console.log(" --add [guestname] - Add a device sharing link.");
+ console.log(" --type [desktop,terminal,files,http,https] - Type of sharing to add, can be combined. default is desktop.");
+ console.log(" --viewonly - Make desktop sharing view only.");
+ console.log(" --consent [notify,prompt,none] - Consent flags, default is notify.");
+ console.log(" --start [yyyy-mm-ddThh:mm:ss] - Start time, default is now.");
+ console.log(" --end [yyyy-mm-ddThh:mm:ss] - End time.");
+ console.log(" --duration [minutes] - Duration of the share, default is 60 minutes.");
+ console.log(" --daily - Add recurring daily device share.");
+ console.log(" --weekly - Add recurring weekly device share.");
+ console.log(" --port [portnumber] - Set alternative port for http or https, default is 80 for http and 443 for https.");
+ break;
+ }
+ case 'agentdownload': {
+ console.log("Download an agent of a specific type for a given device group, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl AgentDownload --id 'groupid' --type 3"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl AgentDownload --id 'groupid' --type 3 --installflags 1"));
+ console.log("\r\nRequired arguments:\r\n");
+ console.log(" --type [ArchitectureNumber] - Agent architecture number.");
+ if (process.platform == 'win32') {
+ console.log(" --id [groupid] - The device group identifier.");
+ } else {
+ console.log(" --id '[groupid]' - The device group identifier.");
+ }
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --installflags [InstallFlagsNumber] - With the following choices:");
+ console.log(" installflags 0 - Default, Interactive & Background, offers connect button & install/uninstall");
+ console.log(" installflags 1 - Interactive only, offers only connect button, not install/uninstall");
+ console.log(" installflags 2 - Background only, offers only install/uninstall, not connect");
+ break;
+ }
+ case 'upload': {
+ console.log("Upload a local file to a remote device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl Upload --id 'deviceid' --file sample.txt --target c:\\"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl Upload --id 'deviceid' --file sample.txt --target /tmp"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log(" --file [localfile] - The local file to upload.");
+ console.log(" --target [remotepath] - The remote path to upload the file to.");
+ break;
+ }
+ case 'download': {
+ console.log("Download a file from a remote device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl Download --id 'deviceid' --file C:\\sample.txt --target c:\\temp"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl Download --id 'deviceid' --file /tmp/sample.txt --target /tmp"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log(" --file [remotefile] - The remote file to download.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --target [localpath] - The local path to download the file to.");
+ break;
+ }
+ case 'webrelay': {
+ console.log("Generate a webrelay URL to access a HTTP/HTTPS service on a remote device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl WebRelay --id 'deviceid' --type http --port 80"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl WebRelay --id 'deviceid' --type https --port 443"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log(" --type [http,https] - Type of relay from remote device, http or https.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --port [portnumber] - Set alternative port for http or https, default is 80 for http and 443 for https.");
+ break;
+ }
+ case 'deviceopenurl': {
+ console.log("Open a web page on a remote device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceOpenUrl --id 'deviceid' --openurl http://meshcentral.com"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log(" --openurl [url] - Link to the web page.");
+ break;
+ }
+ case 'devicemessage': {
+ console.log("Display a message on the remote device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceMessage --id 'deviceid' --msg \"message\""));
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceMessage --id 'deviceid' --msg \"message\" --title \"title\""));
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceMessage --id 'deviceid' --msg \"message\" --title \"title\" --timeout 120000"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log(" --msg [message] - The message to display.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --title [title] - Messagebox title, default is \"MeshCentral\".");
+ console.log(" --timeout [miliseconds] - After timeout messagebox vanishes, 0 keeps messagebox open until closed manually, default is 120000 (2 Minutes).");
+ break;
+ }
+ case 'devicetoast': {
+ console.log("Display a toast message on the remote device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceToast --id 'deviceid' --msg \"message\""));
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceToast --id 'deviceid' --msg \"message\" --title \"title\""));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log(" --msg [message] - The message to display.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --title [title] - Toast title, default is \"MeshCentral\".");
+ break;
+ }
+ case 'groupmessage': {
+ console.log("Open a message box on remote devices in a specific device group, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl GroupMessage --id 'devicegroupid' --msg \"message\""));
+ console.log(winRemoveSingleQuotes(" MeshCtrl GroupMessage --id 'devicegroupid' --msg \"message\" --title \"title\""));
+ console.log(winRemoveSingleQuotes(" MeshCtrl GroupMessage --id 'devicegroupid' --msg \"message\" --title \"title\" --timeout 120000"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [devicegroupid] - The device identifier.");
+ } else {
+ console.log(" --id '[devicegroupid]' - The device identifier.");
+ }
+ console.log(" --msg [message] - The message to display.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --title [title] - Messagebox title, default is \"MeshCentral\".");
+ console.log(" --timeout [miliseconds] - After timeout messagebox vanishes, 0 keeps messagebox open until closed manually, default is 120000 (2 Minutes).");
+ break;
+ }
+ case 'grouptoast': {
+ console.log("Display a toast notification on remote devices in a specific device group, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl GroupToast --id 'devicegroupid' --msg \"message\""));
+ console.log(winRemoveSingleQuotes(" MeshCtrl GroupToast --id 'devicegroupid' --msg \"message\" --title \"title\""));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [devicegroupid] - The device identifier.");
+ } else {
+ console.log(" --id '[devicegroupid]' - The device identifier.");
+ }
+ console.log(" --msg [message] - The message to display.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --title [title] - Toast title, default is \"MeshCentral\".");
+ break;
+ }
+ case 'report': {
+ console.log("Generate a CSV report, Example usages:\r\n");
+ console.log(" MeshCtrl Report --type sessions --devicegroup mesh//...");
+ console.log(" MeshCtrl Report --type traffic --json");
+ console.log(" MeshCtrl Report --type logins --groupby day");
+ console.log(" MeshCtrl Report --type db");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --start [yyyy-mm-ddThh:mm:ss] - Filter the results starting at that date. Defaults to last 24h and last week when used with --groupby day. Usable with sessions, traffic and logins");
+ console.log(" --end [yyyy-mm-ddThh:mm:ss] - Filter the results ending at that date. Defaults to now. Usable with sessions, traffic and logins");
+ console.log(" --groupby [name] - How to group results. Options: user, day, device. Defaults to user. User and day usable in sessions and logins, device usable in sessions.");
+ console.log(" --devicegroup [devicegroupid] - Filter the results by device group. Usable in sessions");
+ console.log(" --showtraffic - Add traffic data in sessions report");
+ break;
+ }
+ default: {
+ console.log("Get help on an action. Type:\r\n\r\n help [action]\r\n\r\nPossible actions are: " + possibleCommands.join(', ') + '.');
+ }
+ }
+ }
+ break;
+ }
+ }
+
+ if (ok) {
+ if(args.loginpass===true){
+ const readline = require('readline');
+ const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ terminal: false
+ });
+ process.stdout.write('Enter your password: ');
+ const stdin = process.openStdin();
+ stdin.setRawMode(true); // Set raw mode to prevent echoing of characters
+ stdin.resume();
+ args.loginpass = '';
+ process.stdin.on('data', (char) => {
+ char = char + '';
+ switch (char) {
+ case '\n':
+ case '\r':
+ case '\u0004': // They've finished entering their password
+ stdin.setRawMode(false);
+ stdin.pause();
+ process.stdout.clearLine(); process.stdout.cursorTo(0);
+ rl.close();
+ serverConnect();
+ break;
+ case '\u0003': // Ctrl+C
+ process.stdout.write('\n');
+ process.exit();
+ break;
+ default: // Mask the password with "*"
+ args.loginpass += char;
+ process.stdout.clearLine(); process.stdout.cursorTo(0);
+ process.stdout.write('Enter your password: ' + '*'.repeat(args.loginpass.length));
+ break;
+ }
+ });
+ }else{
+ serverConnect();
+ }
+ }
+}
+
+function displayConfigHelp() {
+ console.log("Perform operations on the config.json file. Example usage:\r\n");
+ console.log(" MeshCtrl config --show");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --show - Display the config.json file.");
+ console.log(" --listdomains - Display non-default domains.");
+ console.log(" --adddomain [domain] - Add a domain.");
+ console.log(" --removedomain [domain] - Remove a domain.");
+ console.log(" --settodomain [domain] - Set values to the domain.");
+ console.log(" --removefromdomain [domain] - Remove values from the domain.");
+ console.log("\r\nWith adddomain, removedomain, settodomain and removefromdomain you can add the key and value pair. For example:\r\n");
+ console.log(" --adddomain \"MyDomain\" --title \"My Server Name\" --newAccounts false");
+ console.log(" --settodomain \"MyDomain\" --title \"My Server Name\"");
+ console.log(" --removefromdomain \"MyDomain\" --title");
+}
+
+function performConfigOperations(args) {
+ var domainValues = ['title', 'title2', 'titlepicture', 'trustedcert', 'welcomepicture', 'welcometext', 'userquota', 'meshquota', 'newaccounts', 'usernameisemail', 'newaccountemaildomains', 'newaccountspass', 'newaccountsrights', 'geolocation', 'lockagentdownload', 'userconsentflags', 'Usersessionidletimeout', 'auth', 'ldapoptions', 'ldapusername', 'ldapuserbinarykey', 'ldapuseremail', 'footer', 'certurl', 'loginKey', 'userallowedip', 'agentallowedip', 'agentnoproxy', 'agentconfig', 'orphanagentuser', 'httpheaders', 'yubikey', 'passwordrequirements', 'limits', 'amtacmactivation', 'redirects', 'sessionrecording', 'hide', 'preventduplicatedevices'];
+ var domainObjectValues = ['ldapoptions', 'httpheaders', 'yubikey', 'passwordrequirements', 'limits', 'amtacmactivation', 'redirects', 'sessionrecording'];
+ var domainArrayValues = ['newaccountemaildomains', 'newaccountsrights', 'loginkey', 'agentconfig'];
+ var configChange = false;
+ var fs = require('fs');
+ var path = require('path');
+ var configFile = 'config.json';
+ var didSomething = 0;
+ if (fs.existsSync(configFile) == false) { configFile = path.join('meshcentral-data', 'config.json'); }
+ if (fs.existsSync(configFile) == false) { configFile = path.join(__dirname, 'config.json'); }
+ if (fs.existsSync(configFile) == false) { configFile = path.join(__dirname, 'meshcentral-data', 'config.json'); }
+ if (fs.existsSync(configFile) == false) { configFile = path.join(__dirname, '..', 'meshcentral-data', 'config.json'); }
+ if (fs.existsSync(configFile) == false) { configFile = path.join(__dirname, '..', '..', 'meshcentral-data', 'config.json'); }
+ if (fs.existsSync(configFile) == false) { console.log("Unable to find config.json."); return; }
+ var config = null;
+ try { config = fs.readFileSync(configFile).toString('utf8'); } catch (ex) { console.log("Error: Unable to read config.json"); return; }
+ try { config = JSON.parse(fs.readFileSync(configFile)); } catch (e) { console.log('ERROR: Unable to parse ' + configFile + '.'); return null; }
+ console.log("!!!READ CONFIG");
+ console.log(JSON.parse(fs.readFileSync(configFile)));
+ if (args.adddomain != null) {
+ didSomething++;
+ if (config.domains == null) { config.domains = {}; }
+ if (config.domains[args.adddomain] != null) { console.log("Error: Domain \"" + args.adddomain + "\" already exists"); }
+ else {
+ configChange = true;
+ config.domains[args.adddomain] = {};
+ for (var i in args) {
+ if (domainValues.indexOf(i.toLowerCase()) >= 0) {
+ if (args[i] == 'true') { args[i] = true; } else if (args[i] == 'false') { args[i] = false; } else if (parseInt(args[i]) == args[i]) { args[i] = parseInt(args[i]); }
+ config.domains[args.adddomain][i] = args[i];
+ configChange = true;
+ }
+ }
+ }
+ }
+ if (args.removedomain != null) {
+ didSomething++;
+ if (config.domains == null) { config.domains = {}; }
+ if (config.domains[args.removedomain] == null) { console.log("Error: Domain \"" + args.removedomain + "\" does not exist"); }
+ else { delete config.domains[args.removedomain]; configChange = true; }
+ }
+ if (args.settodomain != null) {
+ didSomething++;
+ if (config.domains == null) { config.domains = {}; }
+ if (args.settodomain == true) { args.settodomain = ''; }
+ if (config.domains[args.settodomain] == null) { console.log("Error: Domain \"" + args.settodomain + "\" does not exist"); }
+ else {
+ for (var i in args) {
+ if ((i == '_') || (i == 'settodomain')) continue;
+ if (domainValues.indexOf(i.toLowerCase()) >= 0) {
+ var isObj = (domainObjectValues.indexOf(i.toLowerCase()) >= 0);
+ var isArr = (domainArrayValues.indexOf(i.toLowerCase()) >= 0);
+ if ((isObj == false) && (isArr == false)) {
+ // Simple value set
+ if (args[i] == '') { delete config.domains[args.settodomain][i]; configChange = true; } else {
+ if (args[i] == 'true') { args[i] = true; } else if (args[i] == 'false') { args[i] = false; } else if (parseInt(args[i]) == args[i]) { args[i] = parseInt(args[i]); }
+ config.domains[args.settodomain][i] = args[i];
+ configChange = true;
+ }
+ } else if (isObj || isArr) {
+ // Set an object/array value
+ if (args[i] == '') { delete config.domains[args.settodomain][i]; configChange = true; } else {
+ var x = null;
+ try { x = JSON.parse(args[i]); } catch (ex) { }
+ if ((x == null) || (typeof x != 'object')) { console.log("Unable to parse JSON for " + i + "."); } else {
+ if (isArr && Array.isArray(x) == false) {
+ console.log("Value " + i + " must be an array.");
+ } else if (!isArr && Array.isArray(x) == true) {
+ console.log("Value " + i + " must be an object.");
+ } else {
+ config.domains[args.settodomain][i] = x;
+ configChange = true;
+ }
+ }
+ }
+ }
+ } else {
+ console.log('Invalid configuration value: ' + i);
+ }
+ }
+ }
+ }
+ if (args.removefromdomain != null) {
+ didSomething++;
+ if (config.domains == null) { config.domains = {}; }
+ if (config.domains[args.removefromdomain] == null) { console.log("Error: Domain \"" + args.removefromdomain + "\" does not exist"); }
+ else { for (var i in args) { if (domainValues.indexOf(i.toLowerCase()) >= 0) { delete config.domains[args.removefromdomain][i]; configChange = true; } } }
+ }
+ if (configChange) {
+ try { fs.writeFileSync(configFile, JSON.stringify(config, null, 2)); } catch (ex) { console.log("Error: Unable to read config.json"); return; }
+ }
+ if (args.show == 1) {
+ console.log(JSON.stringify(config, null, 2)); return;
+ } else if (args.listdomains == 1) {
+ if (config.domains == null) {
+ console.log('No domains found.'); return;
+ } else {
+ // Show the list of active domains, skip the default one.
+ for (var i in config.domains) { if ((i != '') && (i[0] != '_')) { console.log(i); } } return;
+ }
+ } else {
+ if (didSomething == 0) {
+ displayConfigHelp();
+ } else {
+ console.log("Done.");
+ }
+ }
+}
+
+function onVerifyServer(clientName, certs) { return null; }
+
+function serverConnect() {
+ const WebSocket = require('ws');
+
+ var url = 'wss://localhost/control.ashx';
+ if (args.url) {
+ url = args.url;
+ if (url.length < 5) { console.log("Invalid url."); process.exit(); return; }
+ if ((url.startsWith('wss://') == false) && (url.startsWith('ws://') == false)) { console.log("Invalid url."); process.exit(); return; }
+ var i = url.indexOf('?key='), loginKey = null;
+ if (i >= 0) { loginKey = url.substring(i + 5); url = url.substring(0, i); }
+ if (url.endsWith('/') == false) { url += '/'; }
+ url += 'control.ashx';
+ if (loginKey != null) { url += '?key=' + loginKey; }
+ }
+
+ // TODO: checkServerIdentity does not work???
+ var options = { rejectUnauthorized: false, checkServerIdentity: onVerifyServer }
+
+ // Setup the HTTP proxy if needed
+ if (args.proxy != null) {
+ const HttpsProxyAgent = require('https-proxy-agent');
+ options.agent = new HttpsProxyAgent(require('url').parse(args.proxy));
+ }
+
+ // Password authentication
+ if (args.loginpass != null) {
+ var username = 'admin';
+ if (args.loginuser != null) { username = args.loginuser; }
+ var token = '';
+ if (args.token != null) { token = ',' + Buffer.from('' + args.token).toString('base64'); }
+ options.headers = { 'x-meshauth': Buffer.from('' + username).toString('base64') + ',' + Buffer.from('' + args.loginpass).toString('base64') + token }
+ }
+
+ // Cookie authentication
+ var ckey = null, loginCookie = null;
+ if (args.loginkey != null) {
+ // User key passed in as argument hex
+ if (args.loginkey.length != 160) { loginCookie = args.loginkey; }
+ ckey = Buffer.from(args.loginkey, 'hex');
+ if (ckey.length != 80) { ckey = null; loginCookie = args.loginkey; }
+ } else if (args.loginkeyfile != null) {
+ // Load key from hex file
+ var fs = require('fs');
+ try {
+ var keydata = fs.readFileSync(args.loginkeyfile, 'utf8').split(' ').join('').split('\r').join('').split('\n').join('');
+ ckey = Buffer.from(keydata, 'hex');
+ if (ckey.length != 80) { ckey = null; loginCookie = args.loginkey; }
+ } catch (ex) { console.log(ex.message); process.exit(); return; }
+ }
+
+ settings.xxurl = url;
+ if (ckey != null) {
+ var domainid = '', username = 'admin';
+ if (args.logindomain != null) { domainid = args.logindomain; }
+ if (args.loginuser != null) { username = args.loginuser; }
+ url += (url.indexOf('?key=') >= 0 ? '&auth=' : '?auth=') + encodeCookie({ userid: 'user/' + domainid + '/' + username, domainid: domainid }, ckey);
+ } else {
+ if (args.logindomain != null) { console.log("--logindomain can only be used along with --loginkey."); process.exit(); return; }
+ if (loginCookie != null) { url += (url.indexOf('?key=') >= 0 ? '&auth=' : '?auth=') + loginCookie; }
+ }
+
+ const ws = new WebSocket(url, options);
+ //console.log('Connecting to ' + url);
+
+ ws.on('open', function open() {
+ //console.log('Connected.');
+ switch (settings.cmd) {
+ case 'serverinfo': { break; }
+ case 'userinfo': { break; }
+ case 'listusers': { ws.send(JSON.stringify({ action: 'users', responseid: 'meshctrl' })); break; }
+ case 'listusersessions': { ws.send(JSON.stringify({ action: 'wssessioncount', responseid: 'meshctrl' })); break; }
+ case 'removeallusersfromusergroup':
+ case 'listusergroups': { ws.send(JSON.stringify({ action: 'usergroups', responseid: 'meshctrl' })); break; }
+ case 'listdevicegroups': { ws.send(JSON.stringify({ action: 'meshes', responseid: 'meshctrl' })); break; }
+ case 'listusersofdevicegroup': { ws.send(JSON.stringify({ action: 'meshes', responseid: 'meshctrl' })); break; }
+ case 'listdevices': {
+ if (args.details) {
+ // Get list of devices with lots of details
+ ws.send(JSON.stringify({ action: 'getDeviceDetails', type: (args.csv) ? 'csv' : 'json' }));
+ } else if (args.group) {
+ ws.send(JSON.stringify({ action: 'nodes', meshname: args.group, responseid: 'meshctrl' }));
+ } else if (args.id) {
+ ws.send(JSON.stringify({ action: 'nodes', meshid: args.id, responseid: 'meshctrl' }));
+ } else {
+ ws.send(JSON.stringify({ action: 'meshes' }));
+ ws.send(JSON.stringify({ action: 'nodes', responseid: 'meshctrl' }));
+ }
+ break;
+ }
+ case 'listevents': {
+ limit = null;
+ if (args.limit) { limit = parseInt(args.limit); }
+ if ((typeof limit != 'number') || (limit < 1)) { limit = null; }
+
+ var cmd = null;
+ if (args.userid) {
+ cmd = { action: 'events', user: args.userid, responseid: 'meshctrl' };
+ } else if (args.id) {
+ cmd = { action: 'events', nodeid: args.id, responseid: 'meshctrl' };
+ } else {
+ cmd = { action: 'events', responseid: 'meshctrl' };
+ }
+ if (typeof limit == 'number') { cmd.limit = limit; }
+ ws.send(JSON.stringify(cmd));
+ break;
+ }
+ case 'logintokens': {
+ if (args.add) {
+ var cmd = { action: 'createLoginToken', name: args.add, expire: 0, responseid: 'meshctrl' };
+ if (args.expire) { cmd.expire = parseInt(args.expire); }
+ ws.send(JSON.stringify(cmd));
+ } else {
+ var cmd = { action: 'loginTokens', responseid: 'meshctrl' };
+ if (args.remove) { cmd.remove = [args.remove]; }
+ ws.send(JSON.stringify(cmd));
+ }
+ break;
+ }
+ case 'adduser': {
+ var siteadmin = getSiteAdminRights(args);
+ if (args.randompass) { args.pass = getRandomAmtPassword(); }
+ var op = { action: 'adduser', username: args.user, pass: args.pass, responseid: 'meshctrl' };
+ if (args.email) { op.email = args.email; if (args.emailverified) { op.emailVerified = true; } }
+ if (args.resetpass) { op.resetNextLogin = true; }
+ if (siteadmin != -1) { op.siteadmin = siteadmin; }
+ if (args.domain) { op.domain = args.domain; }
+ if (args.phone === true) { op.phone = ''; }
+ if (typeof args.phone == 'string') { op.phone = args.phone; }
+ if (typeof args.realname == 'string') { op.realname = args.realname; }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'edituser': {
+ var userid = args.userid;
+ if ((args.domain != null) && (userid.indexOf('/') < 0)) { userid = 'user/' + args.domain + '/' + userid; }
+ var siteadmin = getSiteAdminRights(args);
+ var op = { action: 'edituser', userid: userid, responseid: 'meshctrl' };
+ if (args.email) { op.email = args.email; if (args.emailverified) { op.emailVerified = true; } }
+ if (args.resetpass) { op.resetNextLogin = true; }
+ if (siteadmin != -1) { op.siteadmin = siteadmin; }
+ if (args.domain) { op.domain = args.domain; }
+ if (args.phone === true) { op.phone = ''; }
+ if (typeof args.phone == 'string') { op.phone = args.phone; }
+ if (typeof args.realname == 'string') { op.realname = args.realname; }
+ if (args.realname === true) { op.realname = ''; }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'removeuser': {
+ var userid = args.userid;
+ if ((args.domain != null) && (userid.indexOf('/') < 0)) { userid = 'user/' + args.domain + '/' + userid; }
+ ws.send(JSON.stringify({ action: 'deleteuser', userid: userid, responseid: 'meshctrl' }));
+ break;
+ }
+ case 'addusergroup': {
+ var op = { action: 'createusergroup', name: args.name, desc: args.desc, responseid: 'meshctrl' };
+ if (args.domain) { op.domain = args.domain; }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'removeusergroup': {
+ var ugrpid = args.groupid;
+ if ((args.domain != null) && (userid.indexOf('/') < 0)) { ugrpid = 'ugrp/' + args.domain + '/' + ugrpid; }
+ ws.send(JSON.stringify({ action: 'deleteusergroup', ugrpid: ugrpid, responseid: 'meshctrl' }));
+ break;
+ }
+ case 'addtousergroup': {
+ var ugrpid = args.groupid;
+ if ((args.domain != null) && (userid.indexOf('/') < 0)) { ugrpid = 'ugrp/' + args.domain + '/' + ugrpid; }
+
+ // Add a user to a user group
+ if (args.userid != null) {
+ var userid = args.userid;
+ if ((args.domain != null) && (userid.indexOf('/') < 0)) { userid = 'user/' + args.domain + '/' + userid; }
+ ws.send(JSON.stringify({ action: 'addusertousergroup', ugrpid: ugrpid, usernames: [userid.split('/')[2]], responseid: 'meshctrl' }));
+ break;
+ }
+
+ if ((args.id != null) && (args.id.startsWith('user/'))) {
+ ws.send(JSON.stringify({ action: 'addusertousergroup', ugrpid: ugrpid, usernames: [args.id.split('/')[2]], responseid: 'meshctrl' }));
+ break;
+ }
+
+ var rights = 0;
+ if (args.rights != null) { rights = parseInt(args.rights); }
+
+ // Add a device group to a user group
+ if (args.meshid != null) {
+ var meshid = args.meshid;
+ if ((args.domain != null) && (userid.indexOf('/') < 0)) { meshid = 'mesh/' + args.domain + '/' + meshid; }
+ ws.send(JSON.stringify({ action: 'addmeshuser', meshid: meshid, userid: ugrpid, meshadmin: rights, responseid: 'meshctrl' }));
+ break;
+ }
+
+ if ((args.id != null) && (args.id.startsWith('mesh/'))) {
+ ws.send(JSON.stringify({ action: 'addmeshuser', meshid: args.id, userid: ugrpid, meshadmin: rights, responseid: 'meshctrl' }));
+ break;
+ }
+
+ // Add a device to a user group
+ if (args.nodeid != null) {
+ var nodeid = args.nodeid;
+ if ((args.domain != null) && (userid.indexOf('/') < 0)) { nodeid = 'node/' + args.domain + '/' + nodeid; }
+ ws.send(JSON.stringify({ action: 'adddeviceuser', nodeid: nodeid, userids: [ugrpid], rights: rights, responseid: 'meshctrl' }));
+ break;
+ }
+
+ if ((args.id != null) && (args.id.startsWith('node/'))) {
+ ws.send(JSON.stringify({ action: 'adddeviceuser', nodeid: args.id, userids: [ugrpid], rights: rights, responseid: 'meshctrl' }));
+ break;
+ }
+
+ break;
+ }
+ case 'removefromusergroup': {
+ var ugrpid = args.groupid;
+ if ((args.domain != null) && (userid.indexOf('/') < 0)) { ugrpid = 'ugrp/' + args.domain + '/' + ugrpid; }
+
+ // Remove a user from a user group
+ if (args.userid != null) {
+ var userid = args.userid;
+ if ((args.domain != null) && (userid.indexOf('/') < 0)) { userid = 'user/' + args.domain + '/' + userid; }
+ ws.send(JSON.stringify({ action: 'removeuserfromusergroup', ugrpid: ugrpid, userid: userid, responseid: 'meshctrl' }));
+ break;
+ }
+
+ if ((args.id != null) && (args.id.startsWith('user/'))) {
+ ws.send(JSON.stringify({ action: 'removeuserfromusergroup', ugrpid: ugrpid, userid: args.id, responseid: 'meshctrl' }));
+ break;
+ }
+
+ // Remove a device group from a user group
+ if (args.meshid != null) {
+ var meshid = args.meshid;
+ if ((args.domain != null) && (userid.indexOf('/') < 0)) { meshid = 'mesh/' + args.domain + '/' + meshid; }
+ ws.send(JSON.stringify({ action: 'removemeshuser', meshid: meshid, userid: ugrpid, responseid: 'meshctrl' }));
+ break;
+ }
+
+ if ((args.id != null) && (args.id.startsWith('mesh/'))) {
+ ws.send(JSON.stringify({ action: 'removemeshuser', meshid: args.id, userid: ugrpid, responseid: 'meshctrl' }));
+ break;
+ }
+
+ // Remove a device from a user group
+ if (args.nodeid != null) {
+ var nodeid = args.nodeid;
+ if ((args.domain != null) && (userid.indexOf('/') < 0)) { nodeid = 'node/' + args.domain + '/' + nodeid; }
+ ws.send(JSON.stringify({ action: 'adddeviceuser', nodeid: nodeid, userids: [ugrpid], rights: 0, responseid: 'meshctrl', remove: true }));
+ break;
+ }
+
+ if ((args.id != null) && (args.id.startsWith('node/'))) {
+ ws.send(JSON.stringify({ action: 'adddeviceuser', nodeid: args.id, userids: [ugrpid], rights: 0, responseid: 'meshctrl', remove: true }));
+ break;
+ }
+
+ break;
+ }
+ case 'adddevicegroup': {
+ var op = { action: 'createmesh', meshname: args.name, meshtype: 2, responseid: 'meshctrl' };
+ if (args.desc) { op.desc = args.desc; }
+ if (args.amtonly) { op.meshtype = 1; }
+ if (args.agentless) { op.meshtype = 3; }
+ if (args.features) { op.flags = parseInt(args.features); }
+ if (args.consent) { op.consent = parseInt(args.consent); }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'removedevicegroup': {
+ var op = { action: 'deletemesh', responseid: 'meshctrl' };
+ if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'addamtdevice': {
+ var op = { action: 'addamtdevice', amttls: 1, responseid: 'meshctrl' };
+ if (args.id) { op.meshid = args.id; }
+ if ((typeof args.devicename == 'string') && (args.devicename != '')) { op.devicename = args.devicename; }
+ if ((typeof args.hostname == 'string') && (args.hostname != '')) { op.hostname = args.hostname; }
+ if ((typeof args.user == 'string') && (args.user != '')) { op.amtusername = args.user; }
+ if ((typeof args.pass == 'string') && (args.pass != '')) { op.amtpassword = args.pass; }
+ if (args.notls) { op.amttls = 0; }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'addlocaldevice': {
+ var op = { action: 'addlocaldevice', type: 4, responseid: 'meshctrl' };
+ if (args.id) { op.meshid = args.id; }
+ if ((typeof args.devicename == 'string') && (args.devicename != '')) { op.devicename = args.devicename; }
+ if ((typeof args.hostname == 'string') && (args.hostname != '')) { op.hostname = args.hostname; }
+ if (args.type) {
+ if ((typeof parseInt(args.type) != 'number') || isNaN(parseInt(args.type))) { console.log("Invalid type."); process.exit(1); return; }
+ op.type = args.type;
+ }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'editdevicegroup': {
+ var op = { action: 'editmesh', responseid: 'meshctrl' };
+ if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshidname = args.group; }
+ if ((typeof args.name == 'string') && (args.name != '')) { op.meshname = args.name; }
+ if (args.desc === true) { op.desc = ""; } else if (typeof args.desc == 'string') { op.desc = args.desc; }
+ if (args.invitecodes === true) { op.invite = "*"; } else if (typeof args.invitecodes == 'string') {
+ var invitecodes = args.invitecodes.split(','), invitecodes2 = [];
+ for (var i in invitecodes) { if (invitecodes[i].length > 0) { invitecodes2.push(invitecodes[i]); } }
+ if (invitecodes2.length > 0) {
+ op.invite = { codes: invitecodes2, flags: 0 };
+ if (args.backgroundonly === true) { op.invite.flags = 2; }
+ else if (args.interactiveonly === true) { op.invite.flags = 1; }
+ }
+ }
+ if (args.flags != null) {
+ var flags = parseInt(args.flags);
+ if (typeof flags == 'number') { op.flags = flags; }
+ }
+ if (args.consent != null) {
+ var consent = parseInt(args.consent);
+ if (typeof consent == 'number') { op.consent = consent; }
+ }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'movetodevicegroup': {
+ var op = { action: 'changeDeviceMesh', responseid: 'meshctrl', nodeids: [args.devid] };
+ if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'addusertodevicegroup': {
+ var meshrights = 0;
+ if (args.fullrights) { meshrights = 0xFFFFFFFF; }
+ if (args.editgroup) { meshrights |= 1; }
+ if (args.manageusers) { meshrights |= 2; }
+ if (args.managedevices) { meshrights |= 4; }
+ if (args.remotecontrol) { meshrights |= 8; }
+ if (args.agentconsole) { meshrights |= 16; }
+ if (args.serverfiles) { meshrights |= 32; }
+ if (args.wakedevices) { meshrights |= 64; }
+ if (args.notes) { meshrights |= 128; }
+ if (args.desktopviewonly) { meshrights |= 256; }
+ if (args.noterminal) { meshrights |= 512; }
+ if (args.nofiles) { meshrights |= 1024; }
+ if (args.noamt) { meshrights |= 2048; }
+ if (args.limiteddesktop) { meshrights |= 4096; }
+ if (args.limitedevents) { meshrights |= 8192; }
+ if (args.chatnotify) { meshrights |= 16384; }
+ if (args.uninstall) { meshrights |= 32768; }
+ var op = { action: 'addmeshuser', usernames: [args.userid], meshadmin: meshrights, responseid: 'meshctrl' };
+ if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'removeuserfromdevicegroup': {
+ var op = { action: 'removemeshuser', userid: args.userid, responseid: 'meshctrl' };
+ if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'addusertodevice': {
+ var meshrights = 0;
+ if (args.fullrights) { meshrights = (8 + 16 + 32 + 64 + 128 + 16384 + 32768); }
+ if (args.remotecontrol) { meshrights |= 8; }
+ if (args.agentconsole) { meshrights |= 16; }
+ if (args.serverfiles) { meshrights |= 32; }
+ if (args.wakedevices) { meshrights |= 64; }
+ if (args.notes) { meshrights |= 128; }
+ if (args.desktopviewonly) { meshrights |= 256; }
+ if (args.noterminal) { meshrights |= 512; }
+ if (args.nofiles) { meshrights |= 1024; }
+ if (args.noamt) { meshrights |= 2048; }
+ if (args.limiteddesktop) { meshrights |= 4096; }
+ if (args.limitedevents) { meshrights |= 8192; }
+ if (args.chatnotify) { meshrights |= 16384; }
+ if (args.uninstall) { meshrights |= 32768; }
+ var op = { action: 'adddeviceuser', nodeid: args.id, usernames: [args.userid], rights: meshrights, responseid: 'meshctrl' };
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'removeuserfromdevice': {
+ var op = { action: 'adddeviceuser', nodeid: args.id, usernames: [args.userid], rights: 0, remove: true, responseid: 'meshctrl' };
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'sendinviteemail': {
+ var op = { action: 'inviteAgent', email: args.email, name: '', os: '0', responseid: 'meshctrl' }
+ if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
+ if (args.name) { op.name = args.name; }
+ if (args.message) { op.msg = args.message; }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'generateinvitelink': {
+ var op = { action: 'createInviteLink', expire: args.hours, flags: 0, responseid: 'meshctrl' }
+ if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
+ if (args.flags) { op.flags = args.flags; }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'broadcast': {
+ var op = { action: 'userbroadcast', msg: args.msg, responseid: 'meshctrl' };
+ if (args.user) { op.userid = args.user; }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'showevents': {
+ console.log('Connected. Press ctrl-c to end.');
+ break;
+ }
+ case 'deviceinfo': {
+ settings.deviceinfocount = 4;
+ ws.send(JSON.stringify({ action: 'nodes' }));
+ ws.send(JSON.stringify({ action: 'getnetworkinfo', nodeid: args.id, responseid: 'meshctrl' }));
+ ws.send(JSON.stringify({ action: 'lastconnect', nodeid: args.id, responseid: 'meshctrl' }));
+ ws.send(JSON.stringify({ action: 'getsysinfo', nodeid: args.id, nodeinfo: true, responseid: 'meshctrl' }));
+ break;
+ }
+ case 'removedevice': {
+ var op = { action: 'removedevices', nodeids: [ args.id ], responseid: 'meshctrl' };
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'editdevice': {
+ var op = { action: 'changedevice', nodeid: args.id, responseid: 'meshctrl' };
+ if (typeof args.name == 'string') { op.name = args.name; }
+ if (typeof args.name == 'number') { op.name = '' + args.name; }
+ if (args.desc) { if (args.desc === true) { op.desc = ''; } else if (typeof args.desc == 'string') { op.desc = args.desc; } else if (typeof args.desc == 'number') { op.desc = '' + args.desc; } }
+ if (args.tags) { if (args.tags === true) { op.tags = ''; } else if (typeof args.tags == 'string') { op.tags = args.tags.split(','); } else if (typeof args.tags == 'number') { op.tags = '' + args.tags; } }
+ if (args.icon) { op.icon = parseInt(args.icon); if ((typeof op.icon != 'number') || isNaN(op.icon) || (op.icon < 1) || (op.icon > 8)) { console.log("Icon must be between 1 and 8."); process.exit(1); return; } }
+ if (args.consent) { op.consent = parseInt(args.consent); if ((typeof op.consent != 'number') || isNaN(op.consent) || (op.consent < 1)) { console.log("Invalid consent flags."); process.exit(1); return; } }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'runcommand': {
+ var runAsUser = 0;
+ if (args.runasuser) { runAsUser = 1; } else if (args.runasuseronly) { runAsUser = 2; }
+ var reply = false;
+ if (args.reply) { reply = true; }
+ ws.send(JSON.stringify({ action: 'runcommands', nodeids: [args.id], type: ((args.powershell) ? 2 : 0), cmds: args.run, responseid: 'meshctrl', runAsUser: runAsUser, reply: reply }));
+ break;
+ }
+ case 'shell':
+ case 'upload':
+ case 'download': {
+ ws.send("{\"action\":\"authcookie\"}");
+ break;
+ }
+ case 'devicepower': {
+ var nodes = args.id.split(',');
+ if (args.wake) {
+ // Wake operation
+ ws.send(JSON.stringify({ action: 'wakedevices', nodeids: nodes, responseid: 'meshctrl' }));
+ } else if (args.off) {
+ // Power off operation
+ ws.send(JSON.stringify({ action: 'poweraction', nodeids: nodes, actiontype: 2, responseid: 'meshctrl' }));
+ } else if (args.reset) {
+ // Reset operation
+ ws.send(JSON.stringify({ action: 'poweraction', nodeids: nodes, actiontype: 3, responseid: 'meshctrl' }));
+ } else if (args.sleep) {
+ // Sleep operation
+ ws.send(JSON.stringify({ action: 'poweraction', nodeids: nodes, actiontype: 4, responseid: 'meshctrl' }));
+ } else if (args.amton) {
+ // Intel AMT Power on operation
+ ws.send(JSON.stringify({ action: 'poweraction', nodeids: nodes, actiontype: 302, responseid: 'meshctrl' }));
+ } else if (args.amtoff) {
+ // Intel AMT Power off operation
+ ws.send(JSON.stringify({ action: 'poweraction', nodeids: nodes, actiontype: 308, responseid: 'meshctrl' }));
+ } else if (args.amtreset) {
+ // Intel AMT Power reset operation
+ ws.send(JSON.stringify({ action: 'poweraction', nodeids: nodes, actiontype: 310, responseid: 'meshctrl' }));
+ } else {
+ console.log('No power operation specified.');
+ process.exit(1);
+ }
+ break;
+ }
+ case 'agentdownload': {
+ // Download an agent
+ var u = settings.xxurl.replace('wss://', 'https://').replace('/control.ashx', '/meshagents');
+ if (u.indexOf('?') > 0) { u += '&'; } else { u += '?'; }
+ u += 'id=' + args.type + '&meshid=' + args.id;
+ if (args.installflags) {
+ if ((typeof parseInt(args.installflags) != 'number') || isNaN(parseInt(args.installflags)) || (parseInt(args.installflags) < 0) || (parseInt(args.installflags) > 2)) { console.log("Invalid Installflags."); process.exit(1); return; }
+ u += '&installflags=' + args.installflags;
+ }
+ const options = { rejectUnauthorized: false, checkServerIdentity: onVerifyServer }
+ const fs = require('fs');
+ const https = require('https');
+ var downloadSize = 0;
+ const req = https.request(u, options, function (res) {
+ if (res.statusCode != 200) {
+ console.log('Download error, statusCode: ' + res.statusCode);
+ process.exit(1);
+ } else {
+ // Agent the agent filename
+ var agentFileName = 'meshagent';
+ if ((res.headers) && (res.headers['content-disposition'] != null)) {
+ var i = res.headers['content-disposition'].indexOf('filename=\"');
+ if (i >= 0) {
+ agentFileName = res.headers['content-disposition'].substring(i + 10);
+ i = agentFileName.indexOf('\"');
+ if (i >= 0) { agentFileName = agentFileName.substring(0, i); }
+ }
+ }
+ // Check if this file already exists
+ if (fs.existsSync(agentFileName)) { console.log('File \"' + agentFileName + '\" already exists.'); process.exit(1); }
+ var fd = fs.openSync(agentFileName, 'w'); // Open the file for writing
+ res.on('data', function (d) {
+ downloadSize += d.length;
+ fs.writeSync(fd, d); // Save to file
+ });
+ res.on('end', function (d) {
+ fs.closeSync(fd); // Close file
+ console.log('Downloaded ' + downloadSize + ' byte(s) to \"' + agentFileName + '\"');
+ process.exit(1);
+ });
+ }
+ })
+ req.on('error', function (error) { console.error(error); process.exit(1); })
+ req.end()
+ break;
+ }
+ case 'webrelay': {
+ var protocol = null;
+ if (args.type != null) {
+ if (args.type == 'http') {
+ protocol = 1;
+ } else if (args.type == 'https') {
+ protocol = 2;
+ } else {
+ console.log("Unknown protocol type: " + args.type); process.exit(1);
+ }
+ }
+ var port = null;
+ if (typeof args.port == 'number') {
+ if ((args.port < 1) || (args.port > 65535)) { console.log("Port number must be between 1 and 65535."); process.exit(1); }
+ port = args.port;
+ } else if (protocol == 1) {
+ port = 80;
+ } else if (protocol == 2) {
+ port = 443;
+ }
+ ws.send(JSON.stringify({ action: 'webrelay', nodeid: args.id, port: port, appid: protocol, responseid: 'meshctrl' }));
+ break;
+ }
+ case 'devicesharing': {
+ if (args.add) {
+ if (args.add.length == 0) { console.log("Invalid guest name."); process.exit(1); }
+
+ // Sharing type, desktop or terminal
+ var p = 0;
+ if (args.type != null) {
+ var shareTypes = args.type.toLowerCase().split(',');
+ for (var i in shareTypes) { if ((shareTypes[i] != 'terminal') && (shareTypes[i] != 'desktop') && (shareTypes[i] != 'files') && (shareTypes[i] != 'http') && (shareTypes[i] != 'https')) { console.log("Unknown sharing type: " + shareTypes[i]); process.exit(1); } }
+ if (shareTypes.indexOf('terminal') >= 0) { p |= 1; }
+ if (shareTypes.indexOf('desktop') >= 0) { p |= 2; }
+ if (shareTypes.indexOf('files') >= 0) { p |= 4; }
+ if (shareTypes.indexOf('http') >= 0) { p |= 8; }
+ if (shareTypes.indexOf('https') >= 0) { p |= 16; }
+ }
+ if (p == 0) { p = 2; } // Desktop
+
+ // Sharing view only
+ var viewOnly = false;
+ if (args.viewonly) { viewOnly = true; }
+
+ // User consent
+ var consent = 0;
+ if (args.consent == null) {
+ if ((p & 1) != 0) { consent = 0x0002; } // Terminal notify
+ if ((p & 2) != 0) { consent = 0x0001; } // Desktop notify
+ if ((p & 4) != 0) { consent = 0x0004; } // Files notify
+ } else {
+ if (typeof args.consent == 'string') {
+ var flagStrs = args.consent.split(',');
+ for (var i in flagStrs) {
+ var flagStr = flagStrs[i].toLowerCase();
+ if (flagStr == 'none') { consent = 0; }
+ else if (flagStr == 'notify') {
+ if ((p & 1) != 0) { consent |= 0x0002; } // Terminal notify
+ if ((p & 2) != 0) { consent |= 0x0001; } // Desktop notify
+ if ((p & 4) != 0) { consent |= 0x0004; } // Files notify
+ } else if (flagStr == 'prompt') {
+ if ((p & 1) != 0) { consent |= 0x0010; } // Terminal prompt
+ if ((p & 2) != 0) { consent |= 0x0008; } // Desktop prompt
+ if ((p & 4) != 0) { consent |= 0x0020; } // Files prompt
+ } else if (flagStr == 'bar') {
+ if ((p & 2) != 0) { consent |= 0x0040; } // Desktop toolbar
+ } else { console.log("Unknown consent type."); process.exit(1); return; }
+ }
+ }
+ }
+
+ var port = null;
+ // Set Port Number if http or https
+ if ((p & 8) || (p & 16)) {
+ if (typeof args.port == 'number') {
+ if ((args.port < 1) || (args.port > 65535)) { console.log("Port number must be between 1 and 65535."); process.exit(1); }
+ port = args.port;
+ } else if ((p & 8)) {
+ port = 80;
+ } else if ((p & 16)) {
+ port = 443;
+ }
+ }
+
+ // Start and end time
+ var start = null, end = null;
+ if (args.start) { start = Math.floor(Date.parse(args.start) / 1000); end = start + (60 * 60); }
+ if (args.end) { if (start == null) { start = Math.floor(Date.now() / 1000) } end = Math.floor(Date.parse(args.end) / 1000); if (end <= start) { console.log("End time must be ahead of start time."); process.exit(1); return; } }
+ if (args.duration) { if (start == null) { start = Math.floor(Date.now() / 1000) } end = start + parseInt(args.duration * 60); }
+
+ // Recurring
+ var recurring = 0;
+ if (args.daily) { recurring = 1; } else if (args.weekly) { recurring = 2; }
+ if (recurring > 0) {
+ if (args.end != null) { console.log("End time can't be specified for recurring shares, use --duration only."); process.exit(1); return; }
+ if (args.duration == null) { args.duration = 60; } else { args.duration = parseInt(args.duration); }
+ if (start == null) { start = Math.floor(Date.now() / 1000) }
+ if ((typeof args.duration != 'number') || (args.duration < 1)) { console.log("Invalid duration value."); process.exit(1); return; }
+
+ // Recurring sharing
+ ws.send(JSON.stringify({ action: 'createDeviceShareLink', nodeid: args.id, guestname: args.add, p: p, consent: consent, start: start, expire: args.duration, recurring: recurring, viewOnly: viewOnly, port: port, responseid: 'meshctrl' }));
+ } else {
+ if ((start == null) && (end == null)) {
+ // Unlimited sharing
+ ws.send(JSON.stringify({ action: 'createDeviceShareLink', nodeid: args.id, guestname: args.add, p: p, consent: consent, expire: 0, viewOnly: viewOnly, port: port, responseid: 'meshctrl' }));
+ } else {
+ // Time limited sharing
+ ws.send(JSON.stringify({ action: 'createDeviceShareLink', nodeid: args.id, guestname: args.add, p: p, consent: consent, start: start, end: end, viewOnly: viewOnly, port: port, responseid: 'meshctrl' }));
+ }
+ }
+ } else if (args.remove) {
+ ws.send(JSON.stringify({ action: 'removeDeviceShare', nodeid: args.id, publicid: args.remove, responseid: 'meshctrl' }));
+ } else {
+ ws.send(JSON.stringify({ action: 'deviceShares', nodeid: args.id, responseid: 'meshctrl' }));
+ }
+ break;
+ }
+ case 'deviceopenurl': {
+ ws.send(JSON.stringify({ action: 'msg', type: 'openUrl', nodeid: args.id, url: args.openurl, responseid: 'meshctrl' }));
+ break;
+ }
+ case 'devicemessage': {
+ ws.send(JSON.stringify({ action: 'msg', type: 'messagebox', nodeid: args.id, title: args.title ? args.title : "MeshCentral", msg: args.msg, timeout: args.timeout ? args.timeout : 120000, responseid: 'meshctrl' }));
+ break;
+ }
+ case 'devicetoast': {
+ ws.send(JSON.stringify({ action: 'toast', nodeids: [args.id], title: args.title ? args.title : "MeshCentral", msg: args.msg, responseid: 'meshctrl' }));
+ break;
+ }
+ case 'groupmessage': {
+ ws.send(JSON.stringify({ action: 'nodes', meshid: args.id, responseid: 'meshctrl' }));
+ break;
+ }
+ case 'grouptoast': {
+ ws.send(JSON.stringify({ action: 'nodes', meshid: args.id, responseid: 'meshctrl' }));
+ break;
+ }
+ case 'report': {
+ var reporttype = 1;
+ switch(args.type) {
+ case 'traffic':
+ reporttype = 2;
+ break;
+ case 'logins':
+ reporttype = 3;
+ break;
+ case 'db':
+ reporttype = 4;
+ break;
+ }
+
+ var reportgroupby = 1;
+ if(args.groupby){
+ reportgroupby = args.groupby === 'device' ? 2 : args.groupby === 'day' ? 3: 1;
+ }
+
+ var start = null, end = null;
+ if (args.start) {
+ start = Math.floor(Date.parse(args.start) / 1000);
+ } else {
+ start = reportgroupby === 3 ? Math.round(new Date().getTime() / 1000) - (168 * 3600) : Math.round(new Date().getTime() / 1000) - (24 * 3600);
+ }
+ if (args.end) {
+ end = Math.floor(Date.parse(args.end) / 1000);
+ } else {
+ end = Math.round(new Date().getTime() / 1000);
+ }
+ if (end <= start) { console.log("End time must be ahead of start time."); process.exit(1); return; }
+
+ ws.send(JSON.stringify({ action: 'report', type: reporttype, groupBy: reportgroupby, devGroup: args.devicegroup || null, start, end, tz: Intl.DateTimeFormat().resolvedOptions().timeZone, tf: new Date().getTimezoneOffset(), showTraffic: args.hasOwnProperty('showtraffic'), l: 'en', responseid: 'meshctrl' }));
+ break;
+ }
+ }
+ });
+
+ function getSiteAdminRights(args) {
+ var siteadmin = -1;
+ if (typeof args.rights == 'number') {
+ siteadmin = args.rights;
+ } else if (typeof args.rights == 'string') {
+ siteadmin = 0;
+ var srights = args.rights.toLowerCase().split(',');
+ if (srights.indexOf('full') != -1) { siteadmin = 0xFFFFFFFF; }
+ if (srights.indexOf('none') != -1) { siteadmin = 0x00000000; }
+ if (srights.indexOf('backup') != -1 || srights.indexOf('serverbackup') != -1) { siteadmin |= 0x00000001; }
+ if (srights.indexOf('manageusers') != -1) { siteadmin |= 0x00000002; }
+ if (srights.indexOf('restore') != -1 || srights.indexOf('serverrestore') != -1) { siteadmin |= 0x00000004; }
+ if (srights.indexOf('fileaccess') != -1) { siteadmin |= 0x00000008; }
+ if (srights.indexOf('update') != -1 || srights.indexOf('serverupdate') != -1) { siteadmin |= 0x00000010; }
+ if (srights.indexOf('locked') != -1) { siteadmin |= 0x00000020; }
+ if (srights.indexOf('nonewgroups') != -1) { siteadmin |= 0x00000040; }
+ if (srights.indexOf('notools') != -1) { siteadmin |= 0x00000080; }
+ if (srights.indexOf('usergroups') != -1) { siteadmin |= 0x00000100; }
+ if (srights.indexOf('recordings') != -1) { siteadmin |= 0x00000200; }
+ if (srights.indexOf('locksettings') != -1) { siteadmin |= 0x00000400; }
+ if (srights.indexOf('allevents') != -1) { siteadmin |= 0x00000800; }
+ if (srights.indexOf('nonewdevices') != -1) { siteadmin |= 0x00001000; }
+ }
+
+ if (args.siteadmin) { siteadmin = 0xFFFFFFFF; }
+ if (args.manageusers) { if (siteadmin == -1) { siteadmin = 0; } siteadmin |= 2; }
+ if (args.fileaccess) { if (siteadmin == -1) { siteadmin = 0; } siteadmin |= 8; }
+ if (args.serverupdate) { if (siteadmin == -1) { siteadmin = 0; } siteadmin |= 16; }
+ if (args.locked) { if (siteadmin == -1) { siteadmin = 0; } siteadmin |= 32; }
+ if (args.nonewgroups) { if (siteadmin == -1) { siteadmin = 0; } siteadmin |= 64; }
+ if (args.notools) { if (siteadmin == -1) { siteadmin = 0; } siteadmin |= 128; }
+ return siteadmin;
+ }
+
+ ws.on('close', function () { process.exit(); });
+ ws.on('error', function (err) {
+ if (err.code == 'ENOTFOUND') { console.log('Unable to resolve ' + url); }
+ else if (err.code == 'ECONNREFUSED') { console.log('Unable to connect to ' + url); }
+ else { console.log('Unable to connect to ' + url); }
+ process.exit();
+ });
+
+ ws.on('message', function incoming(rawdata) {
+ var data = null;
+ try { data = JSON.parse(rawdata); } catch (ex) { }
+ if (data == null) { console.log('Unable to parse data: ' + rawdata); }
+ if (settings.cmd == 'showevents') {
+ if (args.filter == null) {
+ // Display all events
+ console.log(JSON.stringify(data, null, 2));
+ } else {
+ // Display select events
+ var filters = args.filter.split(',');
+ if (typeof data.event == 'object') {
+ if (filters.indexOf(data.event.action) >= 0) { console.log(JSON.stringify(data, null, 2) + '\r\n'); }
+ } else {
+ if (filters.indexOf(data.action) >= 0) { console.log(JSON.stringify(data, null, 2) + '\r\n'); }
+ }
+ }
+ return;
+ }
+ switch (data.action) {
+ case 'serverinfo': { // SERVERINFO
+ settings.currentDomain = data.serverinfo.domain;
+ if (settings.cmd == 'serverinfo') {
+ if (args.json) {
+ console.log(JSON.stringify(data.serverinfo, ' ', 2));
+ } else {
+ for (var i in data.serverinfo) { console.log(i + ':', data.serverinfo[i]); }
+ }
+ process.exit();
+ }
+ break;
+ }
+ case 'events': {
+ if (settings.cmd == 'listevents') {
+ if (args.raw) {
+ // RAW JSON
+ console.log(JSON.stringify(data.events));
+ } else if (args.json) {
+ // Formatted JSON
+ console.log(JSON.stringify(data.events, null, 2));
+ } else {
+ if ((args.id == null) && (args.userid == null)) {
+ // CSV format
+ console.log("time,type,action,nodeid,userid,msg");
+ for (var i in data.events) {
+ var x = [];
+ x.push(data.events[i].time);
+ x.push(data.events[i].etype);
+ x.push(data.events[i].action);
+ x.push(data.events[i].nodeid);
+ x.push(data.events[i].userid);
+ x.push(data.events[i].msg);
+ console.log(csvFormatArray(x));
+ }
+ } else if (args.id != null) {
+ // CSV format
+ console.log("time,type,action,userid,msg");
+ for (var i in data.events) {
+ var x = [];
+ x.push(data.events[i].time);
+ x.push(data.events[i].etype);
+ x.push(data.events[i].action);
+ x.push(data.events[i].userid);
+ x.push(data.events[i].msg);
+ console.log(csvFormatArray(x));
+ }
+ } else if (args.userid != null) {
+ // CSV format
+ console.log("time,type,action,nodeid,msg");
+ for (var i in data.events) {
+ var x = [];
+ x.push(data.events[i].time);
+ x.push(data.events[i].etype);
+ x.push(data.events[i].action);
+ x.push(data.events[i].nodeid);
+ x.push(data.events[i].msg);
+ console.log(csvFormatArray(x));
+ }
+ }
+ }
+ process.exit();
+ }
+ break;
+ }
+ case 'authcookie': { // SHELL, UPLOAD, DOWNLOAD
+ if ((settings.cmd == 'shell') || (settings.cmd == 'upload') || (settings.cmd == 'download')) {
+ var protocol = 1; // Terminal
+ if ((settings.cmd == 'upload') || (settings.cmd == 'download')) { protocol = 5; } // Files
+ if ((args.id.split('/') != 3) && (settings.currentDomain != null)) { args.id = 'node/' + settings.currentDomain + '/' + args.id; }
+ var id = getRandomHex(6);
+ ws.send(JSON.stringify({ action: 'msg', nodeid: args.id, type: 'tunnel', usage: 1, value: '*/meshrelay.ashx?p=' + protocol + '&nodeid=' + args.id + '&id=' + id + '&rauth=' + data.rcookie, responseid: 'meshctrl' }));
+ connectTunnel(url.replace('/control.ashx', '/meshrelay.ashx?browser=1&p=' + protocol + '&nodeid=' + encodeURIComponent(args.id) + '&id=' + id + '&auth=' + data.cookie));
+ }
+ break;
+ }
+ case 'deviceShares': { // DEVICESHARING
+ if (data.result != null) {
+ console.log(data.result);
+ } else {
+ if ((data.deviceShares == null) || (data.deviceShares.length == 0)) {
+ console.log('No device sharing links for this device.');
+ } else {
+ if (args.json) {
+ console.log(data.deviceShares);
+ } else {
+ for (var i in data.deviceShares) {
+ var share = data.deviceShares[i];
+ var shareType = [];
+ if ((share.p & 1) != 0) { shareType.push("Terminal"); }
+ if ((share.p & 2) != 0) { if (share.viewOnly) { shareType.push("View Only Desktop"); } else { shareType.push("Desktop"); } }
+ if ((share.p & 4) != 0) { shareType.push("Files"); }
+ shareType = shareType.join(' + ');
+ if (shareType == '') { shareType = "Unknown"; }
+ var consent = [];
+ if ((share.consent & 0x0001) != 0) { consent.push("Desktop Notify"); }
+ if ((share.consent & 0x0008) != 0) { consent.push("Desktop Prompt"); }
+ if ((share.consent & 0x0040) != 0) { consent.push("Desktop Connection Toolbar"); }
+ if ((share.consent & 0x0002) != 0) { consent.push("Terminal Notify"); }
+ if ((share.consent & 0x0010) != 0) { consent.push("Terminal Prompt"); }
+ if ((share.consent & 0x0004) != 0) { consent.push("Files Notify"); }
+ if ((share.consent & 0x0020) != 0) { consent.push("Files Prompt"); }
+ console.log('----------');
+ console.log('Identifier: ' + share.publicid);
+ console.log('Type: ' + shareType);
+ console.log('UserId: ' + share.userid);
+ console.log('Guest Name: ' + share.guestName);
+ console.log('User Consent: ' + consent.join(', '));
+ if (share.startTime) { console.log('Start Time: ' + new Date(share.startTime).toLocaleString()); }
+ if (share.expireTime) { console.log('Expire Time: ' + new Date(share.expireTime).toLocaleString()); }
+ if (share.duration) { console.log('Duration: ' + share.duration + ' minute' + ((share.duration > 1) ? 's' : '')); }
+ if (share.recurring == 1) { console.log('Recurring: ' + 'Daily'); }
+ if (share.recurring == 2) { console.log('Recurring: ' + 'Weekly'); }
+ console.log('URL: ' + share.url);
+ }
+ }
+ }
+ }
+ process.exit();
+ break;
+ }
+ case 'userinfo': { // USERINFO
+ if (settings.cmd == 'userinfo') {
+ if (args.json) {
+ console.log(JSON.stringify(data.userinfo, ' ', 2));
+ } else {
+ for (var i in data.userinfo) { console.log(i + ':', data.userinfo[i]); }
+ }
+ process.exit();
+ }
+ break;
+ }
+ case 'getsysinfo': { // DEVICEINFO
+ if (settings.cmd == 'deviceinfo') {
+ settings.sysinfo = (data.result) ? null : data;
+ if (--settings.deviceinfocount == 0) { displayDeviceInfo(settings.sysinfo, settings.lastconnect, settings.networking, settings.nodes); process.exit(); }
+ }
+ break;
+ }
+ case 'lastconnect': {
+ if (settings.cmd == 'deviceinfo') {
+ settings.lastconnect = (data.result) ? null : data;
+ if (--settings.deviceinfocount == 0) { displayDeviceInfo(settings.sysinfo, settings.lastconnect, settings.networking, settings.nodes); process.exit(); }
+ }
+ break;
+ }
+ case 'getnetworkinfo': {
+ if (settings.cmd == 'deviceinfo') {
+ settings.networking = (data.result) ? null : data;
+ if (--settings.deviceinfocount == 0) { displayDeviceInfo(settings.sysinfo, settings.lastconnect, settings.networking, settings.nodes); process.exit(); }
+ }
+ break;
+ }
+ case 'msg': // SHELL
+ case 'toast': // TOAST
+ case 'adduser': // ADDUSER
+ case 'edituser': // EDITUSER
+ case 'addamtdevice': // ADDAMTDEVICE
+ case 'addlocaldevice': // ADDLOCALDEVICE
+ case 'removedevices': // REMOVEDEVICE
+ case 'changedevice': // EDITDEVICE
+ case 'deleteuser': // REMOVEUSER
+ case 'createmesh': // ADDDEVICEGROUP
+ case 'deletemesh': // REMOVEDEVICEGROUP
+ case 'editmesh': // EDITDEVICEGROUP
+ case 'wakedevices':
+ case 'changeDeviceMesh':
+ case 'addmeshuser': //
+ case 'removemeshuser': //
+ case 'wakedevices': //
+ case 'inviteAgent': //
+ case 'adddeviceuser': //
+ case 'createusergroup': //
+ case 'deleteusergroup': //
+ case 'runcommands':
+ case 'poweraction':
+ case 'addusertousergroup':
+ case 'removeuserfromusergroup':
+ case 'removeDeviceShare':
+ case 'userbroadcast': { // BROADCAST
+ if (((settings.cmd == 'shell') || (settings.cmd == 'upload') || (settings.cmd == 'download')) && (data.result == 'OK')) return;
+ if ((data.type == 'runcommands') && (settings.cmd != 'runcommand')) return;
+ if ((settings.multiresponse != null) && (settings.multiresponse > 1)) { settings.multiresponse--; break; }
+ if (data.responseid == 'meshctrl') {
+ if (data.meshid) { console.log(data.result, data.meshid); }
+ else if (data.userid) { console.log(data.result, data.userid); }
+ else console.log(data.result);
+ process.exit();
+ }
+ break;
+ }
+ case 'createDeviceShareLink':
+ case 'webrelay':
+ if (data.result == 'OK') {
+ if (data.publicid) { console.log('ID: ' + data.publicid); }
+ console.log('URL: ' + data.url);
+ } else {
+ console.log(data.result);
+ }
+ process.exit();
+ break;
+ case 'createInviteLink':
+ if (data.responseid == 'meshctrl') {
+ if (data.url) { console.log(data.url); }
+ else console.log(data.result);
+ process.exit();
+ }
+ break;
+ case 'wssessioncount': { // LIST USER SESSIONS
+ if (args.json) {
+ console.log(JSON.stringify(data.wssessions, ' ', 2));
+ } else {
+ for (var i in data.wssessions) { console.log(i + ', ' + ((data.wssessions[i] > 1) ? (data.wssessions[i] + ' sessions.') : ("1 session."))); }
+ }
+ process.exit();
+ break;
+ }
+ case 'usergroups': { // LIST USER GROUPS
+ if (settings.cmd == 'listusergroups') {
+ if (args.json) {
+ console.log(JSON.stringify(data.ugroups, ' ', 2));
+ } else {
+ for (var i in data.ugroups) {
+ var x = i + ', ' + data.ugroups[i].name;
+ if (data.ugroups[i].desc && (data.ugroups[i].desc != '')) { x += ', ' + data.ugroups[i].desc; }
+ console.log(x);
+ var mesh = [], user = [], node = [];
+ if (data.ugroups[i].links != null) { for (var j in data.ugroups[i].links) { if (j.startsWith('mesh/')) { mesh.push(j); } if (j.startsWith('user/')) { user.push(j); } if (j.startsWith('node/')) { node.push(j); } } }
+ console.log(' Users:');
+ if (user.length > 0) { for (var j in user) { console.log(' ' + user[j]); } } else { console.log(' (None)'); }
+ console.log(' Device Groups:');
+ if (mesh.length > 0) { for (var j in mesh) { console.log(' ' + mesh[j] + ', ' + data.ugroups[i].links[mesh[j]].rights); } } else { console.log(' (None)'); }
+ console.log(' Devices:');
+ if (node.length > 0) { for (var j in node) { console.log(' ' + node[j] + ', ' + data.ugroups[i].links[node[j]].rights); } } else { console.log(' (None)'); }
+ }
+ }
+ process.exit();
+ } else if (settings.cmd == 'removeallusersfromusergroup') {
+ var ugrpid = args.groupid, exit = false;
+ if ((args.domain != null) && (userid.indexOf('/') < 0)) { ugrpid = 'ugrp/' + args.domain + '/' + ugrpid; }
+ var ugroup = data.ugroups[ugrpid];
+ if (ugroup == null) {
+ console.log('User group not found.');
+ exit = true;
+ } else {
+ var usercount = 0;
+ if (ugroup.links) {
+ for (var i in ugroup.links) {
+ if (i.startsWith('user/')) {
+ usercount++;
+ ws.send(JSON.stringify({ action: 'removeuserfromusergroup', ugrpid: ugrpid, userid: i, responseid: 'meshctrl' }));
+ console.log('Removing ' + i);
+ }
+ }
+ }
+ if (usercount == 0) { console.log('No users in this user group.'); exit = true; } else { settings.multiresponse = usercount; }
+ }
+ if (exit) { process.exit(); }
+ }
+ break;
+ }
+ case 'users': { // LISTUSERS
+ if (data.result) { console.log(data.result); process.exit(); return; }
+ if (args.filter) {
+ // Filter the list of users
+ var filters = args.filter.toLowerCase().split(',');
+ var filteredusers = [];
+ for (var i in data.users) {
+ var ok = false;
+ if ((filters.indexOf('2fa') >= 0) && ((data.users[i].otphkeys != null) || (data.users[i].otpkeys != null) || (data.users[i].otpsecret != null))) { ok = true; }
+ if ((filters.indexOf('no2fa') >= 0) && ((data.users[i].otphkeys == null) && (data.users[i].otpkeys == null) && (data.users[i].otpsecret == null))) { ok = true; }
+ if (ok == true) { filteredusers.push(data.users[i]); }
+ }
+ data.users = filteredusers;
+ }
+ if (args.json) {
+ console.log(JSON.stringify(data.users, ' ', 2));
+ } else {
+ if (args.idexists) { for (var i in data.users) { const u = data.users[i]; if ((u._id == args.idexists) || (u._id.split('/')[2] == args.idexists)) { console.log('1'); process.exit(); return; } } console.log('0'); process.exit(); return; }
+ if (args.nameexists) { for (var i in data.users) { const u = data.users[i]; if (u.name == args.nameexists) { console.log(u._id); process.exit(); return; } } process.exit(); return; }
+
+ console.log('id, name, email\r\n---------------');
+ for (var i in data.users) {
+ const u = data.users[i];
+ var t = "\"" + u._id.split('/')[2] + "\", \"" + u.name + "\"";
+ if (u.email != null) { t += ", \"" + u.email + "\""; }
+ console.log(t);
+ }
+ }
+ process.exit();
+ break;
+ }
+ case 'nodes': {
+ if (settings.cmd == 'deviceinfo') {
+ settings.nodes = (data.result) ? null : data;
+ if (--settings.deviceinfocount == 0) { displayDeviceInfo(settings.sysinfo, settings.lastconnect, settings.networking, settings.nodes); process.exit(); }
+ }
+ if ((settings.cmd == 'listdevices') && (data.responseid == 'meshctrl')) {
+ if ((data.result != null) && (data.result != 'ok')) {
+ console.log(data.result);
+ } else {
+ // Filter devices based on device id.
+ if (args.filterid) {
+ var filteridSplit = args.filterid.split(','), filters = [];
+ for (var i in filteridSplit) {
+ var f = filteridSplit[i].trim();
+ var g = f.split('/'); // If there is any / in the id, just grab the last part.
+ if (g.length > 0) { f = g[g.length - 1]; }
+ if (f != '') { filters.push(f); }
+ }
+ if (filters.length > 0) {
+ for (var mid in data.nodes) {
+ var filteredNodes = [];
+ for (var nid in data.nodes[mid]) {
+ var n = data.nodes[mid][nid], match = false;
+ for (var f in filters) { if (n._id.indexOf(filters[f]) >= 0) { match = true; } }
+ if (match) { filteredNodes.push(n); }
+ }
+ data.nodes[mid] = filteredNodes;
+ }
+ }
+ }
+
+ // Filter devices based on filter string
+ if (args.filter != null) {
+ for (var meshid in data.nodes) {
+ for (var d in data.nodes[meshid]) { data.nodes[meshid][d].meshid = meshid; }
+ data.nodes[meshid] = parseSearchOrInput(data.nodes[meshid], args.filter.toString().toLowerCase());
+ }
+ }
+
+ if (args.csv) {
+ // Return a flat list
+ var nodecount = 0;
+ for (var i in data.nodes) {
+ var devicesInMesh = data.nodes[i];
+ for (var j in devicesInMesh) {
+ var n = devicesInMesh[j];
+ nodecount++;
+ if (settings.xmeshes && settings.xmeshes[i]) {
+ console.log('\"' + settings.xmeshes[i]._id.split('/')[2] + '\",\"' + settings.xmeshes[i].name.split('\"').join('') + '\",\"' + n._id.split('/')[2] + '\",\"' + n.name.split('\"').join('') + '\",' + (n.icon ? n.icon : 0) + ',' + (n.conn ? n.conn : 0) + ',' + (n.pwr ? n.pwr : 0));
+ } else {
+ console.log('\"\",\"\",\"' + n._id.split('/')[2] + '\",\"' + n.name.split('\"').join('') + '\",' + (n.icon ? n.icon : 0) + ',' + (n.conn ? n.conn : 0) + ',' + (n.pwr ? n.pwr : 0));
+ }
+ }
+ }
+ if (nodecount == 0) { console.log('None'); }
+ } else if (args.count) {
+ // Return how many devices are in this group
+ var nodes = [];
+ for (var i in data.nodes) { var devicesInMesh = data.nodes[i]; for (var j in devicesInMesh) { nodes.push(devicesInMesh[j]); } }
+ console.log(nodes.length);
+ } else if (args.json) {
+ // Return all devices in JSON format
+ var nodes = [];
+
+ for (var i in data.nodes) {
+ const devicesInMesh = data.nodes[i];
+ for (var j in devicesInMesh) {
+ devicesInMesh[j].meshid = i; // Add device group id
+ if (settings.xmeshes && settings.xmeshes[i] && settings.xmeshes[i].name) { devicesInMesh[j].groupname = settings.xmeshes[i].name; } // Add device group name
+ nodes.push(devicesInMesh[j]);
+ }
+ }
+ console.log(JSON.stringify(nodes, ' ', 2));
+ } else {
+ // Display the list of nodes in text format
+ var nodecount = 0;
+ for (var i in data.nodes) {
+ var devicesInMesh = data.nodes[i];
+ if (devicesInMesh.length > 0) {
+ if (settings.xmeshes && settings.xmeshes[i] && settings.xmeshes[i].name) { console.log('\r\nDevice group: \"' + settings.xmeshes[i].name.split('\"').join('') + '\"'); }
+ console.log('id, name, icon, conn, pwr\r\n-------------------------');
+ for (var j in devicesInMesh) {
+ var n = devicesInMesh[j];
+ nodecount++;
+ console.log('\"' + n._id.split('/')[2] + '\", \"' + n.name.split('\"').join('') + '\", ' + (n.icon ? n.icon : 0) + ', ' + (n.conn ? n.conn : 0) + ', ' + (n.pwr ? n.pwr : 0));
+ }
+ }
+ }
+ if (nodecount == 0) { console.log('None'); }
+ }
+ }
+ process.exit();
+ }
+ if ((settings.cmd == 'groupmessage') && (data.responseid == 'meshctrl')) {
+ if ((data.nodes != null)) {
+ for (var i in data.nodes) {
+ for (let index = 0; index < data.nodes[i].length; index++) {
+ const element = data.nodes[i][index];
+ ws.send(JSON.stringify({ action: 'msg', type: 'messagebox', nodeid: element._id, title: args.title ? args.title : "MeshCentral", msg: args.msg, timeout: args.timeout ? args.timeout : 120000 }));
+ }
+ }
+ }
+
+ setTimeout(function(){ console.log('ok'); process.exit(); }, 1000);
+ }
+ if ((settings.cmd == 'grouptoast') && (data.responseid == 'meshctrl')) {
+ if (data.nodes != null) {
+ for (var i in data.nodes) {
+ var nodes = [];
+ for (let index = 0; index < data.nodes[i].length; index++) {
+ const element = data.nodes[i][index];
+ nodes.push(element._id);
+ }
+ ws.send(JSON.stringify({ action: 'toast', nodeids: nodes, title: args.title ? args.title : "MeshCentral", msg: args.msg, responseid: 'meshctrl' }));
+ }
+ }
+ }
+ break;
+ }
+ case 'meshes': { // LISTDEVICEGROUPS
+ if (settings.cmd == 'listdevices') {
+ // Store the list of device groups for later use
+ settings.xmeshes = {}
+ for (var i in data.meshes) { settings.xmeshes[data.meshes[i]._id] = data.meshes[i]; }
+ } else if (settings.cmd == 'listdevicegroups') {
+ if (args.json) {
+ // If asked, add the MeshID hex encoding to the JSON.
+ if (args.hex) { for (var i in data.meshes) { data.meshes[i]._idhex = '0x' + Buffer.from(data.meshes[i]._id.split('/')[2].replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase(); } }
+ console.log(JSON.stringify(data.meshes, ' ', 2));
+ } else {
+ if (args.idexists) { for (var i in data.meshes) { const u = data.meshes[i]; if ((u._id == args.idexists) || (u._id.split('/')[2] == args.idexists)) { console.log('1'); process.exit(); return; } } console.log('0'); process.exit(); return; }
+ if (args.nameexists) { for (var i in data.meshes) { const u = data.meshes[i]; if (u.name == args.nameexists) { console.log(u._id); process.exit(); return; } } process.exit(); return; }
+
+ console.log('id, name\r\n---------------');
+ for (var i in data.meshes) {
+ const m = data.meshes[i];
+ var mid = m._id.split('/')[2];
+ if (args.hex) { mid = '0x' + Buffer.from(mid.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase(); }
+ var t = "\"" + mid + "\", \"" + m.name + "\"";
+ console.log(t);
+ }
+ }
+ process.exit();
+ } else if (settings.cmd == 'listusersofdevicegroup') {
+ for (var i in data.meshes) {
+ const m = data.meshes[i];
+ var mid = m._id.split('/')[2];
+ if (mid == args.id) {
+ if (args.json) {
+ console.log(JSON.stringify(m.links, ' ', 2));
+ } else {
+ console.log('userid, rights\r\n---------------');
+ for (var l in m.links) {
+ var rights = m.links[l].rights;
+ var rightsstr = [];
+ if (rights == 4294967295) { rightsstr = ['FullAdministrator']; } else {
+ if (rights & 1) { rightsstr.push('EditMesh'); }
+ if (rights & 2) { rightsstr.push('ManageUsers'); }
+ if (rights & 4) { rightsstr.push('ManageComputers'); }
+ if (rights & 8) { rightsstr.push('RemoteControl'); }
+ if (rights & 16) { rightsstr.push('AgentConsole'); }
+ if (rights & 32) { rightsstr.push('ServerFiles'); }
+ if (rights & 64) { rightsstr.push('WakeDevice'); }
+ if (rights & 128) { rightsstr.push('SetNotes'); }
+ if (rights & 256) { rightsstr.push('RemoteViewOnly'); }
+ if (rights & 512) { rightsstr.push('NoTerminal'); }
+ if (rights & 1024) { rightsstr.push('NoFiles'); }
+ if (rights & 2048) { rightsstr.push('NoAMT'); }
+ if (rights & 4096) { rightsstr.push('DesktopLimitedInput'); }
+ }
+ console.log(l.split('/')[2] + ', ' + rightsstr.join(', '));
+ }
+ }
+ process.exit();
+ return;
+ }
+ }
+ console.log('Group id not found');
+ process.exit();
+ }
+ break;
+ }
+ case 'close': {
+ if (data.cause == 'noauth') {
+ if (data.msg == 'tokenrequired') {
+ console.log('Authentication token required, use --token [number].');
+ } else if (data.msg == 'nokey') {
+ console.log('URL key is invalid or missing, please specify ?key=xxx in url');
+ } else {
+ if ((args.loginkeyfile != null) || (args.loginkey != null)) {
+ console.log('Invalid login, check the login key and that this computer has the correct time.');
+ } else {
+ console.log('Invalid login.');
+ }
+ }
+ }
+ process.exit();
+ break;
+ }
+ case 'createLoginToken': {
+ if (data.result != null) {
+ console.log(data.result);
+ process.exit();
+ } else {
+ if (args.json) {
+ console.log(data);
+ } else {
+ console.log("New login token created.");
+ if (data.name) { console.log("Token name: " + data.name); }
+ if (data.created) { console.log("Created: " + new Date(data.created).toLocaleString()); }
+ if (data.expire) { console.log("Expire: " + new Date(data.expire).toLocaleString()); }
+ if (data.tokenUser) { console.log("Username: " + data.tokenUser); }
+ if (data.tokenPass) { console.log("Password: " + data.tokenPass); }
+ }
+ }
+ process.exit();
+ break;
+ }
+ case 'loginTokens': {
+ if (args.json) {
+ console.log(data.loginTokens);
+ } else {
+ console.log("Name Username Expire");
+ console.log("-------------------------------------------------------------------------------------");
+ if (data.loginTokens.length == 0) {
+ console.log("No login tokens");
+ } else {
+ for (var i in data.loginTokens) {
+ var t = data.loginTokens[i];
+ var e = (t.expire == 0) ? "Unlimited" : new Date(t.expire).toLocaleString();
+ console.log(padString(t.name, 28) + padString(t.tokenUser, 28) + e);
+ }
+ }
+ }
+ process.exit();
+ break;
+ }
+ case 'getDeviceDetails': {
+ console.log(data.data);
+ process.exit();
+ }
+ case 'report': {
+ console.log('group,' + data.data.columns.flatMap(c => c.id).join(','));
+ Object.keys(data.data.groups).forEach(gk => {
+ data.data.groups[gk].entries.forEach(e => {
+ console.log(gk + ',' + Object.values(e).join(','));
+ });
+ });
+ process.exit();
+ }
+ default: { break; }
+ }
+ //console.log('Data', data);
+ //setTimeout(function timeout() { ws.send(Date.now()); }, 500);
+ });
+}
+
+// String padding function
+
+function padString(str, pad) {
+ var xpad = ' ';
+ if (str.length >= pad) return str; return str + xpad.substring(0, pad - str.length)
+}
+
+function parseSearchAndInput(nodes, x) {
+ var s = x.split(' ' + "and" + ' '), r = null;
+ for (var i in s) {
+ var r2 = getDevicesThatMatchFilter(nodes, s[i]);
+ if (r == null) { r = r2; } else { var r3 = []; for (var j in r2) { if (r.indexOf(r2[j]) >= 0) { r3.push(r2[j]); } } r = r3; }
+ }
+ return r;
+}
+
+function parseSearchOrInput(nodes, x) {
+ var s = x.split(' ' + "or" + ' '), r = null;
+ for (var i in s) { var r2 = parseSearchAndInput(nodes, s[i]); if (r == null) { r = r2; } else { for (var j in r2) { if (r.indexOf(r2[j] >= 0)) { r.push(r2[j]); } } } }
+ return r;
+}
+
+function getDevicesThatMatchFilter(nodes, x) {
+ var r = [];
+ var userSearch = null, ipSearch = null, groupSearch = null, tagSearch = null, agentTagSearch = null, wscSearch = null, osSearch = null, amtSearch = null, descSearch = null;
+ if (x.startsWith("user:".toLowerCase())) { userSearch = x.substring("user:".length); }
+ else if (x.startsWith("u:".toLowerCase())) { userSearch = x.substring("u:".length); }
+ else if (x.startsWith("ip:".toLowerCase())) { ipSearch = x.substring("ip:".length); }
+ else if (x.startsWith("group:".toLowerCase())) { groupSearch = x.substring("group:".length); }
+ else if (x.startsWith("g:".toLowerCase())) { groupSearch = x.substring("g:".length); }
+ else if (x.startsWith("tag:".toLowerCase())) { tagSearch = x.substring("tag:".length); }
+ else if (x.startsWith("t:".toLowerCase())) { tagSearch = x.substring("t:".length); }
+ else if (x.startsWith("atag:".toLowerCase())) { agentTagSearch = x.substring("atag:".length); }
+ else if (x.startsWith("a:".toLowerCase())) { agentTagSearch = x.substring("a:".length); }
+ else if (x.startsWith("os:".toLowerCase())) { osSearch = x.substring("os:".length); }
+ else if (x.startsWith("amt:".toLowerCase())) { amtSearch = x.substring("amt:".length); }
+ else if (x.startsWith("desc:".toLowerCase())) { descSearch = x.substring("desc:".length); }
+ else if (x == 'wsc:ok') { wscSearch = 1; }
+ else if (x == 'wsc:noav') { wscSearch = 2; }
+ else if (x == 'wsc:noupdate') { wscSearch = 3; }
+ else if (x == 'wsc:nofirewall') { wscSearch = 4; }
+ else if (x == 'wsc:any') { wscSearch = 5; }
+
+ if (x == '') {
+ // No search
+ for (var d in nodes) { r.push(nodes[d]); }
+ } else if (ipSearch != null) {
+ // IP address search
+ for (var d in nodes) { if ((nodes[d].ip != null) && (nodes[d].ip.indexOf(ipSearch) >= 0)) { r.push(nodes[d]); } }
+ } else if (groupSearch != null) {
+ // Group filter
+ if (settings.xmeshes) { for (var d in nodes) { if (settings.xmeshes[nodes[d].meshid] && settings.xmeshes[nodes[d].meshid].name.toLowerCase().indexOf(groupSearch) >= 0) { r.push(nodes[d]); } } }
+ } else if (tagSearch != null) {
+ // Tag filter
+ for (var d in nodes) {
+ if ((nodes[d].tags == null) && (tagSearch == '')) { r.push(nodes[d]); }
+ else if (nodes[d].tags != null) { for (var j in nodes[d].tags) { if (nodes[d].tags[j].toLowerCase() == tagSearch) { r.push(nodes[d]); break; } } }
+ }
+ } else if (agentTagSearch != null) {
+ // Agent Tag filter
+ for (var d in nodes) {
+ if ((((nodes[d].agent != null) && (nodes[d].agent.tag == null)) && (agentTagSearch == '')) || ((nodes[d].agent != null) && (nodes[d].agent.tag != null) && (nodes[d].agent.tag.toLowerCase().indexOf(agentTagSearch) >= 0))) { r.push(nodes[d]); };
+ }
+ } else if (userSearch != null) {
+ // User search
+ for (var d in nodes) {
+ if (nodes[d].users && nodes[d].users.length > 0) { for (var i in nodes[d].users) { if (nodes[d].users[i].toLowerCase().indexOf(userSearch) >= 0) { r.push(nodes[d]); } } }
+ }
+ } else if (osSearch != null) {
+ // OS search
+ for (var d in nodes) { if ((nodes[d].osdesc != null) && (nodes[d].osdesc.toLowerCase().indexOf(osSearch) >= 0)) { r.push(nodes[d]); }; }
+ } else if (amtSearch != null) {
+ // Intel AMT search
+ for (var d in nodes) { if ((nodes[d].intelamt != null) && ((amtSearch == '') || (nodes[d].intelamt.state == amtSearch))) { r.push(nodes[d]); } }
+ } else if (descSearch != null) {
+ // Device description search
+ for (var d in nodes) { if ((nodes[d].desc != null) && (nodes[d].desc != '') && ((descSearch == '') || (nodes[d].desc.toLowerCase().indexOf(descSearch) >= 0))) { r.push(nodes[d]); } }
+ } else if (wscSearch != null) {
+ // Windows Security Center
+ for (var d in nodes) {
+ if (nodes[d].wsc) {
+ if ((wscSearch == 1) && (nodes[d].wsc.antiVirus == 'OK') && (nodes[d].wsc.autoUpdate == 'OK') && (nodes[d].wsc.firewall == 'OK')) { r.push(nodes[d]); }
+ else if (((wscSearch == 2) || (wscSearch == 5)) && (nodes[d].wsc.antiVirus != 'OK')) { r.push(nodes[d]); }
+ else if (((wscSearch == 3) || (wscSearch == 5)) && (nodes[d].wsc.autoUpdate != 'OK')) { r.push(nodes[d]); }
+ else if (((wscSearch == 4) || (wscSearch == 5)) && (nodes[d].wsc.firewall != 'OK')) { r.push(nodes[d]); }
+ }
+ }
+ } else if (x == '*') {
+ // Star filter
+ for (var d in nodes) { if (stars[nodes[d]._id] == 1) { r.push(nodes[d]); } }
+ } else {
+ // Device name search
+ try {
+ var rs = x.split(/\s+/).join('|'), rx = new RegExp(rs); // In some cases (like +), this can throw an exception.
+ for (var d in nodes) {
+ //if (showRealNames) {
+ //if (nodes[d].rnamel != null && rx.test(nodes[d].rnamel.toLowerCase())) { r.push(nodes[d]); }
+ //} else {
+ if (rx.test(nodes[d].name.toLowerCase())) { r.push(nodes[d]); }
+ //}
+ }
+ } catch (ex) { for (var d in nodes) { r.push(nodes[d]); } }
+ }
+
+ return r;
+}
+
+
+// Connect tunnel to a remote agent
+function connectTunnel(url) {
+ // Setup WebSocket options
+ var options = { rejectUnauthorized: false, checkServerIdentity: onVerifyServer }
+
+ // Setup the HTTP proxy if needed
+ if (args.proxy != null) { const HttpsProxyAgent = require('https-proxy-agent'); options.agent = new HttpsProxyAgent(require('url').parse(args.proxy)); }
+
+ // Connect the WebSocket
+ console.log('Connecting...');
+ const WebSocket = require('ws');
+ settings.tunnelwsstate = 0;
+ settings.tunnelws = new WebSocket(url, options);
+ settings.tunnelws.on('open', function () { console.log('Waiting for Agent...'); }); // Wait for agent connection
+ settings.tunnelws.on('close', function () { console.log('Connection Closed.'); process.exit(); });
+ settings.tunnelws.on('error', function (err) { console.log(err); process.exit(); });
+
+ if (settings.cmd == 'shell') {
+ // This code does all of the work for a shell command
+ settings.tunnelws.on('message', function (rawdata) {
+ var data = rawdata.toString();
+ if (settings.tunnelwsstate == 1) {
+ // If the incoming text looks exactly like a control command, ignore it.
+ if ((typeof data == 'string') && (data.startsWith('{"ctrlChannel":"102938","type":"'))) {
+ var ctrlCmd = null;
+ try { ctrlCmd = JSON.parse(data); } catch (ex) { }
+ if ((ctrlCmd != null) && (ctrlCmd.ctrlChannel == '102938') && (ctrlCmd.type != null)) return; // This is a control command, like ping/pong. Ignore it.
+ }
+ process.stdout.write(data);
+ } else if (settings.tunnelwsstate == 0) {
+ if (data == 'c') { console.log('Connected.'); } else if (data == 'cr') { console.log('Connected, session is being recorded.'); } else return;
+ // Send terminal size
+ var termSize = null;
+ if (typeof process.stdout.getWindowSize == 'function') { termSize = process.stdout.getWindowSize(); }
+ if (termSize != null) { settings.tunnelws.send(JSON.stringify({ ctrlChannel: '102938', type: 'options', cols: termSize[0], rows: termSize[1] })); }
+ settings.tunnelwsstate = 1;
+ settings.tunnelws.send('1'); // Terminal
+ process.stdin.setEncoding('utf8');
+ process.stdin.setRawMode(true);
+ process.stdout.setEncoding('utf8');
+ process.stdin.unpipe(process.stdout);
+ process.stdout.unpipe(process.stdin);
+ process.stdin.on('data', function (data) { settings.tunnelws.send(Buffer.from(data)); });
+ //process.stdin.on('readable', function () { var chunk; while ((chunk = process.stdin.read()) !== null) { settings.tunnelws.send(Buffer.from(chunk)); } });
+ process.stdin.on('end', function () { process.exit(); });
+ process.stdout.on('resize', function () {
+ var termSize = null;
+ if (typeof process.stdout.getWindowSize == 'function') { termSize = process.stdout.getWindowSize(); }
+ if (termSize != null) { settings.tunnelws.send(JSON.stringify({ ctrlChannel: '102938', type: 'termsize', cols: termSize[0], rows: termSize[1] })); }
+ });
+ }
+ });
+ } else if (settings.cmd == 'upload') {
+ // This code does all of the work for a file upload
+ // node meshctrl upload --id oL4Y6Eg0qjnpHFrp1AxfxnBPenbDGnDSkC@HSOnAheIyd51pKhqSCUgJZakzwfKl --file readme.md --target c:\
+ settings.tunnelws.on('message', function (rawdata) {
+ if (settings.tunnelwsstate == 1) {
+ var cmd = null;
+ try { cmd = JSON.parse(rawdata.toString()); } catch (ex) { return; }
+ if (cmd.reqid == 'up') {
+ if ((cmd.action == 'uploadack') || (cmd.action == 'uploadstart')) {
+ settings.inFlight--;
+ if (settings.uploadFile == null) { if (settings.inFlight == 0) { process.exit(); } return; } // If the file is closed and there is no more in-flight data, exit.
+ var loops = (cmd.action == 'uploadstart') ? 16 : 1; // If this is the first data to be sent, hot start now. We are going to have 16 blocks of data in-flight.
+ for (var i = 0; i < loops; i++) {
+ if (settings.uploadFile == null) continue;
+ var buf = Buffer.alloc(65565);
+ var len = require('fs').readSync(settings.uploadFile, buf, 1, 65564, settings.uploadPtr);
+ var start = 1;
+ settings.uploadPtr += len;
+ if (len > 0) {
+ if ((buf[1] == 0) || (buf[1] == 123)) { start = 0; buf[0] = 0; len++; } // If the buffer starts with 0 or 123, we must add an extra 0 at the start of the buffer
+ settings.inFlight++;
+ settings.tunnelws.send(buf.slice(start, start + len));
+ } else {
+ console.log('Upload done, ' + settings.uploadPtr + ' bytes sent.');
+ if (settings.uploadFile != null) { require('fs').closeSync(settings.uploadFile); delete settings.uploadFile; }
+ if (settings.inFlight == 0) { process.exit(); return; } // File is closed, if there is no more in-flight data, exit.
+ }
+ }
+
+ } else if (cmd.action == 'uploaderror') {
+ if (settings.uploadFile != null) { require('fs').closeSync(settings.uploadFile); }
+ console.log('Upload error.');
+ process.exit();
+ }
+ }
+ } else if (settings.tunnelwsstate == 0) {
+ var data = rawdata.toString();
+ if (data == 'c') { console.log('Connected.'); } else if (data == 'cr') { console.log('Connected, session is being recorded.'); } else return;
+ settings.tunnelwsstate = 1;
+ settings.tunnelws.send('5'); // Files
+ settings.uploadSize = require('fs').statSync(args.file).size;
+ settings.uploadFile = require('fs').openSync(args.file, 'r');
+ settings.uploadPtr = 0;
+ settings.inFlight = 1;
+ console.log('Uploading...');
+ settings.tunnelws.send(JSON.stringify({ action: 'upload', reqid: 'up', path: args.target, name: require('path').basename(args.file), size: settings.uploadSize }));
+ }
+ });
+ } else if (settings.cmd == 'download') {
+ // This code does all of the work for a file download
+ // node meshctrl download --id oL4Y6Eg0qjnpHFrp1AxfxnBPenbDGnDSkC@HSOnAheIyd51pKhqSCUgJZakzwfKl --file c:\temp\MC-8Languages.png --target c:\temp\bob.png
+ settings.tunnelws.on('message', function (rawdata) {
+ if (settings.tunnelwsstate == 1) {
+ if ((rawdata.length > 0) && (rawdata.toString()[0] != '{')) {
+ // This is binary data, this test is ok because 4 first bytes is a control value.
+ if ((rawdata.length > 4) && (settings.downloadFile != null)) { settings.downloadSize += (rawdata.length - 4); require('fs').writeSync(settings.downloadFile, rawdata, 4, rawdata.length - 4); }
+ if ((rawdata[3] & 1) != 0) { // Check end flag
+ // File is done, close everything.
+ if (settings.downloadFile != null) { require('fs').closeSync(settings.downloadFile); }
+ console.log('Download completed, ' + settings.downloadSize + ' bytes written.');
+ process.exit();
+ } else {
+ settings.tunnelws.send(JSON.stringify({ action: 'download', sub: 'ack', id: args.file })); // Send the ACK
+ }
+ } else {
+ // This is text data
+ var cmd = null;
+ try { cmd = JSON.parse(rawdata.toString()); } catch (ex) { return; }
+ if (cmd.action == 'download') {
+ if (cmd.id != args.file) return;
+ if (cmd.sub == 'start') {
+ if ((args.target.endsWith('\\')) || (args.target.endsWith('/'))) { args.target += path.parse(args.file).name; }
+ try { settings.downloadFile = require('fs').openSync(args.target, 'w'); } catch (ex) { console.log("Unable to create file: " + args.target); process.exit(); return; }
+ settings.downloadSize = 0;
+ settings.tunnelws.send(JSON.stringify({ action: 'download', sub: 'startack', id: args.file }));
+ console.log('Download started: ' + args.target);
+ } else if (cmd.sub == 'cancel') {
+ if (settings.downloadFile != null) { require('fs').closeSync(settings.downloadFile); }
+ console.log('Download canceled.');
+ process.exit();
+ }
+ }
+ }
+ } else if (settings.tunnelwsstate == 0) {
+ var data = rawdata.toString();
+ if (data == 'c') { console.log('Connected.'); } else if (data == 'cr') { console.log('Connected, session is being recorded.'); } else return;
+ settings.tunnelwsstate = 1;
+ settings.tunnelws.send('5'); // Files
+ settings.tunnelws.send(JSON.stringify({ action: 'download', sub: 'start', id: args.file, path: args.file }));
+ }
+ });
+ }
+}
+
+// Encode an object as a cookie using a key using AES-GCM. (key must be 32 bytes or more)
+function encodeCookie(o, key) {
+ try {
+ if (key == null) { return null; }
+ o.time = Math.floor(Date.now() / 1000); // Add the cookie creation time
+ const iv = Buffer.from(crypto.randomBytes(12), 'binary'), cipher = crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv);
+ const crypted = Buffer.concat([cipher.update(JSON.stringify(o), 'utf8'), cipher.final()]);
+ return Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
+ } catch (e) { return null; }
+}
+
+// Generate a random Intel AMT password
+function checkAmtPassword(p) { return (p.length > 7) && (/\d/.test(p)) && (/[a-z]/.test(p)) && (/[A-Z]/.test(p)) && (/\W/.test(p)); }
+function getRandomAmtPassword() { var p; do { p = Buffer.from(crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); } while (checkAmtPassword(p) == false); return p; }
+function getRandomHex(count) { return Buffer.from(crypto.randomBytes(count), 'binary').toString('hex'); }
+function format(format) { var args = Array.prototype.slice.call(arguments, 1); return format.replace(/{(\d+)}/g, function (match, number) { return typeof args[number] != 'undefined' ? args[number] : match; }); };
+function winRemoveSingleQuotes(str) { if (process.platform != 'win32') return str; else return str.split('\'').join(''); }
+
+function csvFormatArray(x) {
+ var y = [];
+ for (var i in x) { if ((x[i] == null) || (x[i] == '')) { y.push(''); } else { y.push('"' + x[i].split('"').join('') + '"'); } }
+ return y.join(',');
+}
+
+function displayDeviceInfo(sysinfo, lastconnect, network, nodes) {
+ //console.log('displayDeviceInfo', sysinfo, lastconnect, network, nodes);
+
+ // Fetch the node information
+ var node = null;;
+ if (sysinfo != null && (sysinfo.node != null)) {
+ // Node information came with system information
+ node = sysinfo.node;
+ } else {
+ // This device does not have system information, get node information from the nodes list.
+ for (var m in nodes.nodes) {
+ for (var n in nodes.nodes[m]) {
+ if (nodes.nodes[m][n]._id.indexOf(args.id) >= 0) { node = nodes.nodes[m][n]; }
+ }
+ }
+ }
+ if ((sysinfo == null && lastconnect == null && network == null) || (node == null)) {
+ console.log("Invalid device id");
+ process.exit(); return;
+ }
+
+ var info = {};
+
+ //if (network != null) { sysinfo.netif = network.netif; }
+ if (lastconnect != null) { node.lastconnect = lastconnect.time; node.lastaddr = lastconnect.addr; }
+ if (args.raw) { console.log(JSON.stringify(sysinfo, ' ', 2)); return; }
+
+ // General
+ var output = {}, outputCount = 0;
+ if (node.name) { output["Server Name"] = node.name; outputCount++; }
+ if (node.rname) { output["Computer Name"] = node.rname; outputCount++; }
+ if (node.host != null) { output["Hostname"] = node.host; outputCount++; }
+ if (node.ip != null) { output["IP Address"] = node.ip; outputCount++; }
+ if (node.desc != null) { output["Description"] = node.desc; outputCount++; }
+ if (node.icon != null) { output["Icon"] = node.icon; outputCount++; }
+ if (node.tags) { output["Tags"] = node.tags; outputCount++; }
+ if (node.av) {
+ var av = [];
+ for (var i in node.av) {
+ if (typeof node.av[i]['product'] == 'string') {
+ var n = node.av[i]['product'];
+ if (node.av[i]['updated'] === true) { n += ', updated'; }
+ if (node.av[i]['updated'] === false) { n += ', not updated'; }
+ if (node.av[i]['enabled'] === true) { n += ', enabled'; }
+ if (node.av[i]['enabled'] === false) { n += ', disabled'; }
+ av.push(n);
+ }
+ }
+ output["AntiVirus"] = av; outputCount++;
+ }
+ if (typeof node.wsc == 'object') {
+ output["WindowsSecurityCenter"] = node.wsc; outputCount++;
+ }
+ if (outputCount > 0) { info["General"] = output; }
+
+ // Operating System
+ var hardware = null;
+ if ((sysinfo != null) && (sysinfo.hardware != null)) { hardware = sysinfo.hardware; }
+ if ((hardware && hardware.windows && hardware.windows.osinfo) || node.osdesc) {
+ var output = {}, outputCount = 0;
+ if (node.rname) { output["Name"] = node.rname; outputCount++; }
+ if (node.osdesc) { output["Version"] = node.osdesc; outputCount++; }
+ if (hardware && hardware.windows && hardware.windows.osinfo) { var m = hardware.windows.osinfo; if (m.OSArchitecture) { output["Architecture"] = m.OSArchitecture; outputCount++; } }
+ if (outputCount > 0) { info["Operating System"] = output; }
+ }
+
+ // MeshAgent
+ if (node.agent) {
+ var output = {}, outputCount = 0;
+ var agentsStr = ["Unknown", "Windows 32bit console", "Windows 64bit console", "Windows 32bit service", "Windows 64bit service", "Linux 32bit", "Linux 64bit", "MIPS", "XENx86", "Android", "Linux ARM", "macOS x86-32bit", "Android x86", "PogoPlug ARM", "Android", "Linux Poky x86-32bit", "macOS x86-64bit", "ChromeOS", "Linux Poky x86-64bit", "Linux NoKVM x86-32bit", "Linux NoKVM x86-64bit", "Windows MinCore console", "Windows MinCore service", "NodeJS", "ARM-Linaro", "ARMv6l / ARMv7l", "ARMv8 64bit", "ARMv6l / ARMv7l / NoKVM", "MIPS24KC (OpenWRT)", "Apple Silicon", "FreeBSD x86-64", "Unknown", "Linux ARM 64 bit (glibc/2.24 NOKVM)", "Alpine Linux x86 64 Bit (MUSL)", "Assistant (Windows)", "Armada370 - ARM32/HF (libc/2.26)", "OpenWRT x86-64", "OpenBSD x86-64", "Unknown", "Unknown", "MIPSEL24KC (OpenWRT)", "ARMADA/CORTEX-A53/MUSL (OpenWRT)", "Windows ARM 64bit console", "Windows ARM 64bit service", "ARMVIRT32 (OpenWRT)", "RISC-V x86-64"];
+ if ((node.agent != null) && (node.agent.id != null) && (node.agent.ver != null)) {
+ var str = '';
+ if (node.agent.id <= agentsStr.length) { str = agentsStr[node.agent.id]; } else { str = agentsStr[0]; }
+ if (node.agent.ver != 0) { str += ' v' + node.agent.ver; }
+ output["Mesh Agent"] = str; outputCount++;
+ }
+ if ((node.conn & 1) != 0) {
+ output["Last agent connection"] = "Connected now"; outputCount++;
+ } else {
+ if (node.lastconnect) { output["Last agent connection"] = new Date(node.lastconnect).toLocaleString(); outputCount++; }
+ }
+ if (node.lastaddr) {
+ var splitip = node.lastaddr.split(':');
+ if (splitip.length > 2) {
+ output["Last agent address"] = node.lastaddr; outputCount++; // IPv6
+ } else {
+ output["Last agent address"] = splitip[0]; outputCount++; // IPv4
+ }
+ }
+ if ((node.agent != null) && (node.agent.tag != null)) {
+ output["Tag"] = node.agent.tag; outputCount++;
+ }
+ if (outputCount > 0) { info["Mesh Agent"] = output; }
+ }
+
+ // Networking
+ if (network.netif != null) {
+ var output = {}, outputCount = 0, minfo = {};
+ for (var i in network.netif) {
+ var m = network.netif[i], moutput = {}, moutputCount = 0;
+ if (m.desc) { moutput["Description"] = m.desc; moutputCount++; }
+ if (m.mac) {
+ if (m.gatewaymac) {
+ moutput["MAC Layer"] = format("MAC: {0}, Gateway: {1}", m.mac, m.gatewaymac); moutputCount++;
+ } else {
+ moutput["MAC Layer"] = format("MAC: {0}", m.mac); moutputCount++;
+ }
+ }
+ if (m.v4addr && (m.v4addr != '0.0.0.0')) {
+ if (m.v4gateway && m.v4mask) {
+ moutput["IPv4 Layer"] = format("IP: {0}, Mask: {1}, Gateway: {2}", m.v4addr, m.v4mask, m.v4gateway); moutputCount++;
+ } else {
+ moutput["IPv4 Layer"] = format("IP: {0}", m.v4addr); moutputCount++;
+ }
+ }
+ if (moutputCount > 0) { minfo[m.name + (m.dnssuffix ? (', ' + m.dnssuffix) : '')] = moutput; info["Networking"] = minfo; }
+ }
+ }
+
+ if (network.netif2 != null) {
+ var minfo = {};
+ for (var i in network.netif2) {
+ var m = network.netif2[i], moutput = {}, moutputCount = 0;
+
+ if (Array.isArray(m) == false ||
+ m.length < 1 ||
+ m[0] == null ||
+ ((typeof m[0].mac == 'string') && (m[0].mac.startsWith('00:00:00:00')))
+ )
+ continue;
+
+ var ifTitle = '' + i;
+ if (m[0].fqdn != null && m[0].fqdn != '') ifTitle += ', ' + m[0].fqdn;
+
+ if (typeof m[0].mac == 'string') {
+ if (m[0].gatewaymac) {
+ moutput['MAC Layer'] = format("MAC: {0}, Gateway: {1}", m[0].mac, m[0].gatewaymac);
+ } else {
+ moutput['MAC Layer'] = format("MAC: {0}", m[0].mac);
+ }
+ moutputCount++;
+ }
+
+ moutput['IPv4 Layer'] = '';
+ moutput['IPv6 Layer'] = '';
+ for (var j = 0; j < m.length; j++) {
+ var iplayer = m[j];
+ if (iplayer.family == 'IPv4' || iplayer.family == 'IPv6') {
+ if (iplayer.gateway && iplayer.netmask) {
+ moutput[iplayer.family + ' Layer'] += format("IP: {0}, Mask: {1}, Gateway: {2} ", iplayer.address, iplayer.netmask, iplayer.gateway);
+ moutputCount++;
+ } else {
+ if (iplayer.address) {
+ moutput[iplayer.family + ' Layer'] += format("IP: {0} ", iplayer.address);
+ moutputCount++;
+ }
+ }
+ }
+ }
+ if (moutput['IPv4 Layer'] == '') delete moutput['IPv4 Layer'];
+ if (moutput['IPv6 Layer'] == '') delete moutput['IPv6 Layer'];
+ if (moutputCount > 0) {
+ minfo[ifTitle] = moutput;
+ info["Networking"] = minfo;
+ }
+ }
+ }
+
+ // Intel AMT
+ if (node.intelamt != null) {
+ var output = {}, outputCount = 0;
+ output["Version"] = (node.intelamt.ver) ? ('v' + node.intelamt.ver) : ('' + "Unknown" + ''); outputCount++;
+ var provisioningStates = { 0: "Not Activated (Pre)", 1: "Not Activated (In)", 2: "Activated" };
+ var provisioningMode = '';
+ if ((node.intelamt.state == 2) && node.intelamt.flags) { if (node.intelamt.flags & 2) { provisioningMode = (', ' + "Client Control Mode (CCM)"); } else if (node.intelamt.flags & 4) { provisioningMode = (', ' + "Admin Control Mode (ACM)"); } }
+ output["Provisioning State"] = ((node.intelamt.state) ? (provisioningStates[node.intelamt.state]) : ('' + "Unknown" + '')) + provisioningMode; outputCount++;
+ output["Security"] = (node.intelamt.tls == 1) ? "Secured using TLS" : "TLS is not setup"; outputCount++;
+ output["Admin Credentials"] = (node.intelamt.user == null || node.intelamt.user == '') ? "Not Known" : "Known"; outputCount++;
+ if (outputCount > 0) { info["Intel Active Management Technology (Intel AMT)"] = output; }
+ }
+
+ if (hardware != null) {
+ if (hardware.identifiers) {
+ var output = {}, outputCount = 0, ident = hardware.identifiers;
+ // BIOS
+ if (ident.bios_vendor) { output["Vendor"] = ident.bios_vendor; outputCount++; }
+ if (ident.bios_version) { output["Version"] = ident.bios_version; outputCount++; }
+ if (outputCount > 0) { info["BIOS"] = output; }
+ output = {}, outputCount = 0;
+
+ // Motherboard
+ if (ident.board_vendor) { output["Vendor"] = ident.board_vendor; outputCount++; }
+ if (ident.board_name) { output["Name"] = ident.board_name; outputCount++; }
+ if (ident.board_serial && (ident.board_serial != '')) { output["Serial"] = ident.board_serial; outputCount++; }
+ if (ident.board_version) { output["Version"] = ident.board_version; }
+ if (ident.product_uuid) { output["Identifier"] = ident.product_uuid; }
+ if (ident.cpu_name) { output["CPU"] = ident.cpu_name; }
+ if (ident.gpu_name) { for (var i in ident.gpu_name) { output["GPU" + (parseInt(i) + 1)] = ident.gpu_name[i]; } }
+ if (outputCount > 0) { info["Motherboard"] = output; }
+ }
+
+ // Memory
+ if (hardware.windows) {
+ if (hardware.windows.memory) {
+ var output = {}, outputCount = 0, minfo = {};
+ hardware.windows.memory.sort(function (a, b) { if (a.BankLabel > b.BankLabel) return 1; if (a.BankLabel < b.BankLabel) return -1; return 0; });
+ for (var i in hardware.windows.memory) {
+ var m = hardware.windows.memory[i], moutput = {}, moutputCount = 0;
+ if (m.Capacity) { moutput["Capacity/Speed"] = (m.Capacity / 1024 / 1024) + " Mb, " + m.Speed + " Mhz"; moutputCount++; }
+ if (m.PartNumber) { moutput["Part Number"] = ((m.Manufacturer && m.Manufacturer != 'Undefined') ? (m.Manufacturer + ', ') : '') + m.PartNumber; moutputCount++; }
+ if (moutputCount > 0) { minfo[m.BankLabel] = moutput; info["Memory"] = minfo; }
+ }
+ }
+ }
+
+ // Storage
+ if (hardware.identifiers && ident.storage_devices) {
+ var output = {}, outputCount = 0, minfo = {};
+ // Sort Storage
+ ident.storage_devices.sort(function (a, b) { if (a.Caption > b.Caption) return 1; if (a.Caption < b.Caption) return -1; return 0; });
+ for (var i in ident.storage_devices) {
+ var m = ident.storage_devices[i], moutput = {};
+ if (m.Size) {
+ if (m.Model && (m.Model != m.Caption)) { moutput["Model"] = m.Model; outputCount++; }
+ if ((typeof m.Size == 'string') && (parseInt(m.Size) == m.Size)) { m.Size = parseInt(m.Size); }
+ if (typeof m.Size == 'number') { moutput["Capacity"] = Math.floor(m.Size / 1024 / 1024) + 'Mb'; outputCount++; }
+ if (typeof m.Size == 'string') { moutput["Capacity"] = m.Size; outputCount++; }
+ if (moutputCount > 0) { minfo[m.Caption] = moutput; info["Storage"] = minfo; }
+ }
+ }
+ }
+ }
+
+ // Display everything
+ if (args.json) {
+ console.log(JSON.stringify(info, ' ', 2));
+ } else {
+ for (var i in info) {
+ console.log('--- ' + i + ' ---');
+ for (var j in info[i]) {
+ if ((typeof info[i][j] == 'string') || (typeof info[i][j] == 'number')) {
+ console.log(' ' + j + ': ' + info[i][j]);
+ } else {
+ console.log(' ' + j + ':');
+ for (var k in info[i][j]) {
+ console.log(' ' + k + ': ' + info[i][j][k]);
+ }
+ }
+ }
+ }
+ }
+}
+
+// Read the Mesh Agent error log and index it.
+function indexAgentErrorLog() {
+ // Index the messages
+ const lines = require('fs').readFileSync('../meshcentral-data/agenterrorlogs.txt', { encoding: 'utf8', flag: 'r' }).split('\r\n');
+ var errorIndex = {}; // "msg" --> [ { lineNumber, elemenetNumber } ]
+ for (var i = 0; i < lines.length; i++) {
+ const line = lines[i];
+ if (line.length > 88) {
+ var nodeid = line.substring(0, 70);
+ var fetchTime = parseInt(line.substring(72, 85));
+ var data = JSON.parse(line.substring(87));
+ if ((data != null) && (data.action == 'errorlog') && (Array.isArray(data.log))) {
+ for (var j = 0; j < data.log.length; j++) {
+ var entry = data.log[j];
+ if ((entry != null) && (typeof entry.t == 'number') && (typeof entry.m == 'string')) {
+ const msg = entry.m;
+ if (errorIndex[msg] == null) { errorIndex[msg] = []; }
+ errorIndex[msg].push({ l: i, e: j });
+ }
+ }
+ }
+ }
+ }
+
+ // Sort the messages by frequency
+ var errorIndexCount = []; // [ { m: "msg", c: count } ]
+ for (var i in errorIndex) { errorIndexCount.push({ m: i, c: errorIndex[i].length }); }
+ errorIndexCount = errorIndexCount.sort(function (a, b) { return b.c - a.c })
+
+ // Display the results
+ for (var i = 0; i < errorIndexCount.length; i++) {
+ const m = errorIndexCount[i].m;
+ if ((m.indexOf('STUCK') >= 0) || (m.indexOf('FATAL') >= 0)) { console.log(errorIndexCount[i].c, m); }
+ }
+}
From 3eb5cc3fe49a5fa4d6ce21f6f3e01a0485716464 Mon Sep 17 00:00:00 2001
From: stephannn <38951729+stephannn@users.noreply.github.com>
Date: Mon, 13 Oct 2025 15:25:03 +0200
Subject: [PATCH 2/8] Add files via upload
---
meshagent.js | 4464 +++++++++++++++++++++++++-------------------------
1 file changed, 2232 insertions(+), 2232 deletions(-)
diff --git a/meshagent.js b/meshagent.js
index 3c9e16fc3e..04d88bf970 100644
--- a/meshagent.js
+++ b/meshagent.js
@@ -1,2232 +1,2232 @@
-/**
-* @description MeshCentral MeshAgent communication module
-* @author Ylian Saint-Hilaire & Bryan Roe
-* @copyright Intel Corporation 2018-2022
-* @license Apache-2.0
-* @version v0.0.1
-*/
-
-/*xjslint node: true */
-/*xjslint plusplus: true */
-/*xjslint maxlen: 256 */
-/*jshint node: true */
-/*jshint strict: false */
-/*jshint esversion: 6 */
-"use strict";
-
-// Construct a MeshAgent object, called upon connection
-module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
- const forge = parent.parent.certificateOperations.forge;
- const common = parent.parent.common;
- parent.agentStats.createMeshAgentCount++;
- parent.parent.debug('agent', 'New agent at ' + req.clientIp + ':' + ws._socket.remotePort);
-
- var obj = {};
- obj.domain = domain;
- obj.authenticated = 0;
- obj.receivedCommands = 0;
- obj.agentCoreCheck = 0;
- obj.remoteaddr = req.clientIp;
- obj.remoteaddrport = obj.remoteaddr + ':' + ws._socket.remotePort;
- obj.nonce = parent.crypto.randomBytes(48).toString('binary');
- //ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive, 4 minutes
- if (args.agentidletimeout != 0) { ws._socket.setTimeout(args.agentidletimeout, function () { obj.close(1); }); } // Inactivity timeout of 2:30 minutes, by default agent will WebSocket ping every 2 minutes and server will pong back.
- //obj.nodeid = null;
- //obj.meshid = null;
- //obj.dbNodeKey = null;
- //obj.dbMeshKey = null;
- //obj.connectTime = null;
- //obj.agentInfo = null;
-
- ws._socket.bytesReadEx = 0;
- ws._socket.bytesWrittenEx = 0;
-
- // Perform data accounting
- function dataAccounting() {
- parent.trafficStats.AgentCtrlIn += (ws._socket.bytesRead - ws._socket.bytesReadEx);
- parent.trafficStats.AgentCtrlOut += (ws._socket.bytesWritten - ws._socket.bytesWrittenEx);
- ws._socket.bytesReadEx = ws._socket.bytesRead;
- ws._socket.bytesWrittenEx = ws._socket.bytesWritten;
- }
-
- // Send a message to the mesh agent
- obj.send = function (data, func) { try { if (typeof data == 'string') { ws.send(Buffer.from(data), func); } else { ws.send(data, func); } } catch (e) { } };
- obj.sendBinary = function (data, func) { try { if (typeof data == 'string') { ws.send(Buffer.from(data, 'binary'), func); } else { ws.send(data, func); } } catch (e) { } };
-
- // Disconnect this agent
- obj.close = function (arg) {
- dataAccounting();
-
- if ((arg == 1) || (arg == null)) { try { ws.close(); if (obj.nodeid != null) { parent.parent.debug('agent', 'Soft disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ')'); } } catch (e) { console.log(e); } } // Soft close, close the websocket
- if (arg == 2) {
- try {
- if (ws._socket._parent != null)
- ws._socket._parent.end();
- else
- ws._socket.end();
-
- if (obj.nodeid != null) {
- parent.parent.debug('agent', 'Hard disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ')');
- }
- } catch (e) { console.log(e); }
- }
- // If arg == 2, hard close, close the TCP socket
- // If arg == 3, don't communicate with this agent anymore, but don't disconnect (Duplicate agent).
-
- // Stop any current self-share
- if (obj.guestSharing === true) { removeGuestSharing(); }
-
- // Remove this agent from the webserver list
- if (parent.wsagents[obj.dbNodeKey] == obj) {
- delete parent.wsagents[obj.dbNodeKey];
- parent.parent.ClearConnectivityState(obj.dbMeshKey, obj.dbNodeKey, 1, null, { remoteaddrport: obj.remoteaddrport, name: obj.name });
- }
-
- // Remove this agent from the list of agents with bad web certificates
- if (obj.badWebCert) { delete parent.wsagentsWithBadWebCerts[obj.badWebCert]; }
-
- // Get the current mesh
- const mesh = parent.meshes[obj.dbMeshKey];
-
- // If this is a temporary or recovery agent, or all devices in this group are temporary, remove the agent (0x20 = Temporary, 0x40 = Recovery)
- if (((obj.agentInfo) && (obj.agentInfo.capabilities) && ((obj.agentInfo.capabilities & 0x20) || (obj.agentInfo.capabilities & 0x40))) || ((mesh) && (mesh.flags) && (mesh.flags & 1))) {
- // Delete this node including network interface information and events
- db.Remove(obj.dbNodeKey); // Remove node with that id
- db.Remove('if' + obj.dbNodeKey); // Remove interface information
- db.Remove('nt' + obj.dbNodeKey); // Remove notes
- db.Remove('lc' + obj.dbNodeKey); // Remove last connect time
- db.Remove('si' + obj.dbNodeKey); // Remove system information
- db.Remove('al' + obj.dbNodeKey); // Remove error log last time
- if (db.RemoveSMBIOS) { db.RemoveSMBIOS(obj.dbNodeKey); } // Remove SMBios data
- db.RemoveAllNodeEvents(obj.dbNodeKey); // Remove all events for this node
- db.removeAllPowerEventsForNode(obj.dbNodeKey); // Remove all power events for this node
-
- // Event node deletion
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, { etype: 'node', action: 'removenode', nodeid: obj.dbNodeKey, domain: domain.id, nolog: 1 });
-
- // Disconnect all connections if needed
- const state = parent.parent.GetConnectivityState(obj.dbNodeKey);
- if ((state != null) && (state.connectivity != null)) {
- if ((state.connectivity & 1) != 0) { parent.wsagents[obj.dbNodeKey].close(); } // Disconnect mesh agent
- if ((state.connectivity & 2) != 0) { parent.parent.mpsserver.closeAllForNode(obj.dbNodeKey); } // Disconnect CIRA connection
- }
- }
-
- // Set this agent as no longer authenticated
- obj.authenticated = -1;
-
- // If we where updating the agent using native method, clean that up.
- if (obj.agentUpdate != null) {
- if (obj.agentUpdate.fd) { try { parent.fs.close(obj.agentUpdate.fd); } catch (ex) { } }
- parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete
- delete obj.agentUpdate.buf;
- delete obj.agentUpdate;
- }
-
- // If we where updating the agent meshcore method, clean that up.
- if (obj.agentCoreUpdateTaskId != null) {
- parent.parent.taskLimiter.completed(obj.agentCoreUpdateTaskId);
- delete obj.agentCoreUpdateTaskId;
- }
-
- // Perform timer cleanup
- if (obj.pingtimer) { clearInterval(obj.pingtimer); delete obj.pingtimer; }
- if (obj.pongtimer) { clearInterval(obj.pongtimer); delete obj.pongtimer; }
-
- // Perform aggressive cleanup
- delete obj.name;
- delete obj.nonce;
- delete obj.nodeid;
- delete obj.unauth;
- delete obj.remoteaddr;
- delete obj.remoteaddrport;
- delete obj.meshid;
- delete obj.connectTime;
- delete obj.agentInfo;
- delete obj.agentExeInfo;
- ws.removeAllListeners(['message', 'close', 'error']);
- };
-
- // When data is received from the mesh agent web socket
- ws.on('message', function (msg) {
- dataAccounting();
- if (msg.length < 2) return;
- if (typeof msg == 'object') { msg = msg.toString('binary'); } // TODO: Could change this entire method to use Buffer instead of binary string
- if (obj.authenticated == 2) { // We are authenticated
- if ((obj.agentUpdate == null) && (msg.charCodeAt(0) == 123)) { processAgentData(msg); } // Only process JSON messages if meshagent update is not in progress
- if (msg.length < 2) return;
- const cmdid = common.ReadShort(msg, 0);
- if (cmdid == 11) { // MeshCommand_CoreModuleHash
- if (msg.length == 4) { ChangeAgentCoreInfo({ 'caps': 0 }); } // If the agent indicated that no core is running, clear the core information string.
- // Mesh core hash, sent by agent with the hash of the current mesh core.
-
- // If we are performing an agent update, don't update the core.
- if (obj.agentUpdate != null) { return; }
-
- // If we are using a custom core, don't try to update it.
- if (obj.agentCoreCheck == 1000) {
- obj.sendBinary(common.ShortToStr(16) + common.ShortToStr(0)); // MeshCommand_CoreOk. Indicates to the agent that the core is ok. Start it if it's not already started.
- agentCoreIsStable();
- return;
- }
-
- // Get the current meshcore hash
- const agentMeshCoreHash = (msg.length == 52) ? msg.substring(4, 52) : null;
-
- // If the agent indicates this is a custom core, we are done.
- if ((agentMeshCoreHash != null) && (agentMeshCoreHash == '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0')) {
- obj.agentCoreCheck = 0;
- obj.sendBinary(common.ShortToStr(16) + common.ShortToStr(0)); // MeshCommand_CoreOk. Indicates to the agent that the core is ok. Start it if it's not already started.
- agentCoreIsStable();
- return;
- }
-
- // We need to check if the core is current. Figure out what core we need.
- var corename = null;
- if ((obj.agentInfo != null) && (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId] != null)) {
- if ((obj.agentCoreCheck == 1001) || (obj.agentCoreUpdate == true)) {
- // If the user asked, use the recovery core.
- corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].rcore;
- } else if (obj.agentCoreCheck == 1011) {
- // If the user asked, use the tiny core.
- corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].tcore;
- } else if (obj.agentInfo.capabilities & 0x40) {
- // If this is a recovery agent, use the agent recovery core.
- corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].arcore;
- } else {
- // This is the normal core for this agent type.
- corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].core;
- }
- }
-
- // If we have a core, use it.
- if (corename != null) {
- const meshcorehash = parent.parent.defaultMeshCoresHash[corename];
- if (agentMeshCoreHash != meshcorehash) {
- if ((obj.agentCoreCheck < 5) || (obj.agentCoreCheck == 1001) || (obj.agentCoreCheck == 1011) || (obj.agentCoreUpdate == true)) {
- if (meshcorehash == null) {
- // Clear the core
- obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0)); // MeshCommand_CoreModule, ask mesh agent to clear the core
- parent.agentStats.clearingCoreCount++;
- parent.parent.debug('agent', "Clearing core");
- } else {
- // Setup task limiter options, this system limits how many tasks can run at the same time to spread the server load.
- var taskLimiterOptions = { hash: meshcorehash, core: parent.parent.defaultMeshCores[corename], name: corename };
-
- // If the agent supports compression, sent the core compressed.
- if ((obj.agentInfo.capabilities & 0x100) && (parent.parent.defaultMeshCoresDeflate[corename])) {
- args.core = parent.parent.defaultMeshCoresDeflate[corename];
- }
-
- // Update new core with task limiting so not to flood the server. This is a high priority task.
- obj.agentCoreUpdatePending = true;
- parent.parent.taskLimiter.launch(function (argument, taskid, taskLimiterQueue) {
- if (obj.authenticated == 2) {
- // Send the updated core.
- delete obj.agentCoreUpdatePending;
- obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0) + argument.hash + argument.core.toString('binary'), function () { parent.parent.taskLimiter.completed(taskid); }); // MeshCommand_CoreModule, start core update
- parent.agentStats.updatingCoreCount++;
- parent.parent.debug('agent', "Updating core " + argument.name);
- } else {
- // This agent is probably disconnected, nothing to do.
- parent.parent.taskLimiter.completed(taskid);
- }
- }, taskLimiterOptions, 0);
- }
- obj.agentCoreCheck++;
- }
- } else {
- obj.agentCoreCheck = 0;
- obj.sendBinary(common.ShortToStr(16) + common.ShortToStr(0)); // MeshCommand_CoreOk. Indicates to the agent that the core is ok. Start it if it's not already started.
- agentCoreIsStable(); // No updates needed, agent is ready to go.
- }
- }
-
- /*
- // TODO: Check if we have a mesh specific core. If so, use that.
- var agentMeshCoreHash = null;
- if (msg.length == 52) { agentMeshCoreHash = msg.substring(4, 52); }
- if ((agentMeshCoreHash != parent.parent.defaultMeshCoreHash) && (agentMeshCoreHash != parent.parent.defaultMeshCoreNoMeiHash)) {
- if (obj.agentCoreCheck < 5) { // This check is in place to avoid a looping core update.
- if (parent.parent.defaultMeshCoreHash == null) {
- // Update no core
- obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0)); // Command 10, ask mesh agent to clear the core
- } else {
- // Update new core
- if ((parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId] != null) && (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].amt == true)) {
- obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0) + parent.parent.defaultMeshCoreHash + parent.parent.defaultMeshCore); // Command 10, ask mesh agent to set the core (with MEI support)
- } else {
- obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0) + parent.parent.defaultMeshCoreNoMeiHash + parent.parent.defaultMeshCoreNoMei); // Command 10, ask mesh agent to set the core (No MEI)
- }
- }
- obj.agentCoreCheck++;
- }
- } else {
- obj.agentCoreCheck = 0;
- }
- */
- }
- else if (cmdid == 12) { // MeshCommand_AgentHash
- if ((msg.length == 52) && (obj.agentExeInfo != null) && (obj.agentExeInfo.update == true)) {
- const agenthash = msg.substring(4);
- const agentUpdateMethod = compareAgentBinaryHash(obj.agentExeInfo, agenthash);
- if (agentUpdateMethod === 2) { // Use meshcore agent update system
- // Send the recovery core to the agent, if the agent is capable of running one
- if (((obj.agentInfo.capabilities & 16) != 0) && (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].core != null)) {
- parent.agentStats.agentMeshCoreBinaryUpdate++;
- obj.agentCoreUpdate = true;
- obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0)); // Ask to clear the core
- obj.sendBinary(common.ShortToStr(11) + common.ShortToStr(0)); // Ask for meshcore hash
- }
- } else if (agentUpdateMethod === 1) { // Use native agent update system
- // Mesh agent update required, do it using task limiter so not to flood the network. Medium priority task.
- parent.parent.taskLimiter.launch(function (argument, taskid, taskLimiterQueue) {
- if (obj.authenticated != 2) { parent.parent.taskLimiter.completed(taskid); return; } // If agent disconnection, complete and exit now.
- if (obj.nodeid != null) { parent.parent.debug('agent', "Agent update required, NodeID=0x" + obj.nodeid.substring(0, 16) + ', ' + obj.agentExeInfo.desc); }
- parent.agentStats.agentBinaryUpdate++;
- if ((obj.agentExeInfo.data == null) && (((obj.agentInfo.capabilities & 0x100) == 0) || (obj.agentExeInfo.zdata == null))) {
- // Read the agent from disk
- parent.fs.open(obj.agentExeInfo.path, 'r', function (err, fd) {
- if (obj.agentExeInfo == null) return; // Agent disconnected during this call.
- if (err) { parent.parent.debug('agentupdate', "ERROR: " + err); return console.error(err); }
- obj.agentUpdate = { ptr: 0, buf: Buffer.alloc(parent.parent.agentUpdateBlockSize + 4), fd: fd, taskid: taskid };
-
- // MeshCommand_CoreModule, ask mesh agent to clear the core.
- // The new core will only be sent after the agent updates.
- obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0));
-
- // We got the agent file open on the server side, tell the agent we are sending an update ending with the SHA384 hash of the result
- //console.log("Agent update file open.");
- obj.sendBinary(common.ShortToStr(13) + common.ShortToStr(0)); // Command 13, start mesh agent download
-
- // Send the first mesh agent update data block
- obj.agentUpdate.buf[0] = 0;
- obj.agentUpdate.buf[1] = 14;
- obj.agentUpdate.buf[2] = 0;
- obj.agentUpdate.buf[3] = 1;
- parent.fs.read(obj.agentUpdate.fd, obj.agentUpdate.buf, 4, parent.parent.agentUpdateBlockSize, obj.agentUpdate.ptr, function (err, bytesRead, buffer) {
- if (obj.agentUpdate == null) return;
- if ((err != null) || (bytesRead == 0)) {
- // Error reading the agent file, stop here.
- try { parent.fs.close(obj.agentUpdate.fd); } catch (ex) { }
- parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete
- parent.parent.debug('agentupdate', "ERROR: Unable to read first block of agent binary from disk.");
- delete obj.agentUpdate.buf;
- delete obj.agentUpdate;
- } else {
- // Send the first block to the agent
- obj.agentUpdate.ptr += bytesRead;
- parent.parent.debug('agentupdate', "Sent first block of " + bytesRead + " bytes from disk.");
- obj.sendBinary(obj.agentUpdate.buf); // Command 14, mesh agent first data block
- }
- });
- });
- } else {
- // Send the agent from RAM
- obj.agentUpdate = { ptr: 0, buf: Buffer.alloc(parent.parent.agentUpdateBlockSize + 4), taskid: taskid };
-
- // MeshCommand_CoreModule, ask mesh agent to clear the core.
- // The new core will only be sent after the agent updates.
- obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0));
-
- // We got the agent file open on the server side, tell the agent we are sending an update ending with the SHA384 hash of the result
- obj.sendBinary(common.ShortToStr(13) + common.ShortToStr(0)); // Command 13, start mesh agent download
-
- // Send the first mesh agent update data block
- obj.agentUpdate.buf[0] = 0;
- obj.agentUpdate.buf[1] = 14;
- obj.agentUpdate.buf[2] = 0;
- obj.agentUpdate.buf[3] = 1;
-
- // If agent supports compression, send the compressed agent if possible.
- if ((obj.agentInfo.capabilities & 0x100) && (obj.agentExeInfo.zdata != null)) {
- // Send compressed data
- obj.agentUpdate.agentUpdateData = obj.agentExeInfo.zdata;
- obj.agentUpdate.agentUpdateHash = obj.agentExeInfo.zhash;
- } else {
- // Send uncompressed data
- obj.agentUpdate.agentUpdateData = obj.agentExeInfo.data;
- obj.agentUpdate.agentUpdateHash = obj.agentExeInfo.hash;
- }
-
- const len = Math.min(parent.parent.agentUpdateBlockSize, obj.agentUpdate.agentUpdateData.length - obj.agentUpdate.ptr);
- if (len > 0) {
- // Send the first block
- obj.agentUpdate.agentUpdateData.copy(obj.agentUpdate.buf, 4, obj.agentUpdate.ptr, obj.agentUpdate.ptr + len);
- obj.agentUpdate.ptr += len;
- obj.sendBinary(obj.agentUpdate.buf); // Command 14, mesh agent first data block
- parent.parent.debug('agentupdate', "Sent first block of " + len + " bytes from RAM.");
- } else {
- // Error
- parent.parent.debug('agentupdate', "ERROR: Len of " + len + " is invalid.");
- parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete
- delete obj.agentUpdate.buf;
- delete obj.agentUpdate;
- }
- }
- }, null, 1);
-
- } else {
- // Check the mesh core, if the agent is capable of running one
- if (((obj.agentInfo.capabilities & 16) != 0) && (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].core != null)) {
- obj.sendBinary(common.ShortToStr(11) + common.ShortToStr(0)); // Command 11, ask for mesh core hash.
- }
- }
- }
- }
- else if (cmdid == 14) { // MeshCommand_AgentBinaryBlock
- if ((msg.length == 4) && (obj.agentUpdate != null)) {
- const status = common.ReadShort(msg, 2);
- if (status == 1) {
- if (obj.agentExeInfo.data == null) {
- // Read the agent from disk
- parent.fs.read(obj.agentUpdate.fd, obj.agentUpdate.buf, 4, parent.parent.agentUpdateBlockSize, obj.agentUpdate.ptr, function (err, bytesRead, buffer) {
- if ((obj.agentExeInfo == null) || (obj.agentUpdate == null)) return; // Agent disconnected during this async call.
- if ((err != null) || (bytesRead < 0)) {
- // Error reading the agent file, stop here.
- parent.parent.debug('agentupdate', "ERROR: Unable to read agent #" + obj.agentExeInfo.id + " binary from disk.");
- try { parent.fs.close(obj.agentUpdate.fd); } catch (ex) { }
- parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete
- delete obj.agentUpdate.buf;
- delete obj.agentUpdate;
- } else {
- // Send the next block to the agent
- parent.parent.debug('agentupdate', "Sending disk agent #" + obj.agentExeInfo.id + " block, ptr=" + obj.agentUpdate.ptr + ", len=" + bytesRead + ".");
- obj.agentUpdate.ptr += bytesRead;
- if (bytesRead == parent.parent.agentUpdateBlockSize) { obj.sendBinary(obj.agentUpdate.buf); } else { obj.sendBinary(obj.agentUpdate.buf.slice(0, bytesRead + 4)); } // Command 14, mesh agent next data block
- if ((bytesRead < parent.parent.agentUpdateBlockSize) || (obj.agentUpdate.ptr == obj.agentExeInfo.size)) {
- parent.parent.debug('agentupdate', "Completed agent #" + obj.agentExeInfo.id + " update from disk, ptr=" + obj.agentUpdate.ptr + ".");
- obj.sendBinary(common.ShortToStr(13) + common.ShortToStr(0) + obj.agentExeInfo.hash); // Command 13, end mesh agent download, send agent SHA384 hash
- try { parent.fs.close(obj.agentUpdate.fd); } catch (ex) { }
- parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete
- delete obj.agentUpdate.buf;
- delete obj.agentUpdate;
- }
- }
- });
- } else {
- // Send the agent from RAM
- const len = Math.min(parent.parent.agentUpdateBlockSize, obj.agentUpdate.agentUpdateData.length - obj.agentUpdate.ptr);
- if (len > 0) {
- obj.agentUpdate.agentUpdateData.copy(obj.agentUpdate.buf, 4, obj.agentUpdate.ptr, obj.agentUpdate.ptr + len);
- if (len == parent.parent.agentUpdateBlockSize) { obj.sendBinary(obj.agentUpdate.buf); } else { obj.sendBinary(obj.agentUpdate.buf.slice(0, len + 4)); } // Command 14, mesh agent next data block
- parent.parent.debug('agentupdate', "Sending RAM agent #" + obj.agentExeInfo.id + " block, ptr=" + obj.agentUpdate.ptr + ", len=" + len + ".");
- obj.agentUpdate.ptr += len;
- }
-
- if (obj.agentUpdate.ptr == obj.agentUpdate.agentUpdateData.length) {
- parent.parent.debug('agentupdate', "Completed agent #" + obj.agentExeInfo.id + " update from RAM, ptr=" + obj.agentUpdate.ptr + ".");
- obj.sendBinary(common.ShortToStr(13) + common.ShortToStr(0) + obj.agentUpdate.agentUpdateHash); // Command 13, end mesh agent download, send agent SHA384 hash
- parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete
- delete obj.agentUpdate.buf;
- delete obj.agentUpdate;
- }
- }
- }
- }
- }
- else if (cmdid == 15) { // MeshCommand_AgentTag
- var tag = msg.substring(2);
- while (tag.charCodeAt(tag.length - 1) == 0) { tag = tag.substring(0, tag.length - 1); } // Remove end-of-line zeros.
- ChangeAgentTag(tag);
- }
- } else if (obj.authenticated < 2) { // We are not authenticated
- // Check if this is a un-authenticated JSON
- if (msg.charCodeAt(0) == 123) {
- var str = msg.toString('utf8'), command = null;
- if (str[0] == '{') {
- try { command = JSON.parse(str); } catch (ex) { } // If the command can't be parsed, ignore it.
- if ((command != null) && (command.action === 'agentName') && (typeof command.value == 'string') && (command.value.length > 0) && (command.value.length < 256)) { obj.agentName = command.value; }
- }
- return;
- }
- const cmd = common.ReadShort(msg, 0);
- if (cmd == 1) {
- // Agent authentication request
- if ((msg.length != 98) || ((obj.receivedCommands & 1) != 0)) return;
- obj.receivedCommands += 1; // Agent can't send the same command twice on the same connection ever. Block DOS attack path.
-
- if (isIgnoreHashCheck()) {
- // Send the agent web hash back to the agent
- // Send 384 bits SHA384 hash of TLS cert + 384 bits nonce
- obj.sendBinary(common.ShortToStr(1) + msg.substring(2, 50) + obj.nonce); // Command 1, hash + nonce. Use the web hash given by the agent.
- } else {
- // Check that the server hash matches our own web certificate hash (SHA384)
- obj.agentSeenCerthash = msg.substring(2, 50);
- if ((getWebCertHash(domain) != obj.agentSeenCerthash) && (getWebCertFullHash(domain) != obj.agentSeenCerthash) && (parent.defaultWebCertificateHash != obj.agentSeenCerthash) && (parent.defaultWebCertificateFullHash != obj.agentSeenCerthash)) {
- if (parent.parent.supportsProxyCertificatesRequest !== false) {
- obj.badWebCert = Buffer.from(parent.crypto.randomBytes(16), 'binary').toString('base64');
- parent.wsagentsWithBadWebCerts[obj.badWebCert] = obj; // Add this agent to the list of of agents with bad web certificates.
- parent.parent.updateProxyCertificates(false);
- }
- parent.agentStats.agentBadWebCertHashCount++;
- parent.setAgentIssue(obj, "BadWebCertHash: " + Buffer.from(msg.substring(2, 50), 'binary').toString('hex'));
- parent.parent.debug('agent', 'Agent bad web cert hash (Agent:' + (Buffer.from(msg.substring(2, 50), 'binary').toString('hex').substring(0, 10)) + ' != Server:' + (Buffer.from(getWebCertHash(domain), 'binary').toString('hex').substring(0, 10)) + ' or ' + (Buffer.from(getWebCertFullHash(domain), 'binary').toString('hex').substring(0, 10)) + '), holding connection (' + obj.remoteaddrport + ').');
- parent.parent.debug('agent', 'Agent reported web cert hash:' + (Buffer.from(msg.substring(2, 50), 'binary').toString('hex')) + '.');
- console.log('Agent bad web cert hash (Agent:' + (Buffer.from(msg.substring(2, 50), 'binary').toString('hex').substring(0, 10)) + ' != Server:' + (Buffer.from(getWebCertHash(domain), 'binary').toString('hex').substring(0, 10)) + ' or ' + (Buffer.from(getWebCertFullHash(domain), 'binary').toString('hex').substring(0, 10)) + '), holding connection (' + obj.remoteaddrport + ').');
- console.log('Agent reported web cert hash:' + (Buffer.from(msg.substring(2, 50), 'binary').toString('hex')) + '.');
- delete obj.agentSeenCerthash;
- return;
- } else {
- // The hash matched one of the acceptable values, send the agent web hash back to the agent
- // Send 384 bits SHA384 hash of TLS cert + 384 bits nonce
- // Command 1, hash + nonce. Use the web hash given by the agent.
- obj.sendBinary(common.ShortToStr(1) + obj.agentSeenCerthash + obj.nonce);
- }
- }
-
- // Use our server private key to sign the ServerHash + AgentNonce + ServerNonce
- obj.agentnonce = msg.substring(50, 98);
-
- // Check if we got the agent auth confirmation
- if ((obj.receivedCommands & 8) == 0) {
- // If we did not get an indication that the agent already validated this server, send the server signature.
- if (obj.useSwarmCert == true) {
- // Perform the hash signature using older swarm server certificate
- parent.parent.certificateOperations.acceleratorPerformSignature(1, msg.substring(2) + obj.nonce, null, function (tag, signature) {
- // Send back our certificate + signature
- obj.sendBinary(common.ShortToStr(2) + common.ShortToStr(parent.swarmCertificateAsn1.length) + parent.swarmCertificateAsn1 + signature); // Command 2, certificate + signature
- });
- } else {
- // Perform the hash signature using the server agent certificate
- parent.parent.certificateOperations.acceleratorPerformSignature(0, msg.substring(2) + obj.nonce, null, function (tag, signature) {
- // Send back our certificate + signature
- obj.sendBinary(common.ShortToStr(2) + common.ShortToStr(parent.agentCertificateAsn1.length) + parent.agentCertificateAsn1 + signature); // Command 2, certificate + signature
- });
- }
- }
-
- // Check the agent signature if we can
- if (obj.unauthsign != null) {
- if (processAgentSignature(obj.unauthsign) == false) {
- parent.agentStats.agentBadSignature1Count++;
- parent.setAgentIssue(obj, "BadSignature1");
- parent.parent.debug('agent', 'Agent connected with bad signature, holding connection (' + obj.remoteaddrport + ').');
- console.log('Agent connected with bad signature, holding connection (' + obj.remoteaddrport + ').'); return;
- } else { completeAgentConnection(); }
- }
- }
- else if (cmd == 2) {
- // Agent certificate
- if ((msg.length < 4) || ((obj.receivedCommands & 2) != 0)) return;
- obj.receivedCommands += 2; // Agent can't send the same command twice on the same connection ever. Block DOS attack path.
-
- // Decode the certificate
- const certlen = common.ReadShort(msg, 2);
- obj.unauth = {};
- try { obj.unauth.nodeid = Buffer.from(forge.pki.getPublicKeyFingerprint(forge.pki.certificateFromAsn1(forge.asn1.fromDer(msg.substring(4, 4 + certlen))).publicKey, { md: forge.md.sha384.create() }).data, 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); } catch (ex) { console.log(ex); parent.parent.debug('agent', ex); return; }
- obj.unauth.nodeCertPem = '-----BEGIN CERTIFICATE-----\r\n' + Buffer.from(msg.substring(4, 4 + certlen), 'binary').toString('base64') + '\r\n-----END CERTIFICATE-----';
-
- // Check the agent signature if we can
- if (obj.agentnonce == null) { obj.unauthsign = msg.substring(4 + certlen); } else {
- if (processAgentSignature(msg.substring(4 + certlen)) == false) {
- parent.agentStats.agentBadSignature2Count++;
- parent.setAgentIssue(obj, "BadSignature2");
- parent.parent.debug('agent', 'Agent connected with bad signature, holding connection (' + obj.remoteaddrport + ').');
- console.log('Agent connected with bad signature, holding connection (' + obj.remoteaddrport + ').'); return;
- }
- }
- completeAgentConnection();
- }
- else if (cmd == 3) {
- // Agent meshid
- if ((msg.length < 70) || ((obj.receivedCommands & 4) != 0)) return;
- obj.receivedCommands += 4; // Agent can't send the same command twice on the same connection ever. Block DOS attack path.
-
- // Set the meshid
- obj.agentInfo = {};
- obj.agentInfo.infoVersion = common.ReadInt(msg, 2);
- obj.agentInfo.agentId = common.ReadInt(msg, 6);
- obj.agentInfo.agentVersion = common.ReadInt(msg, 10);
- obj.agentInfo.platformType = common.ReadInt(msg, 14);
- if (obj.agentInfo.platformType > 8 || obj.agentInfo.platformType < 1) { obj.agentInfo.platformType = 1; }
- if (msg.substring(50, 66) == '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0') {
- obj.meshid = Buffer.from(msg.substring(18, 50), 'binary').toString('hex'); // Older HEX MeshID
- } else {
- obj.meshid = Buffer.from(msg.substring(18, 66), 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); // New Base64 MeshID
- }
- //console.log('MeshID', obj.meshid);
- obj.agentInfo.capabilities = common.ReadInt(msg, 66);
- if (msg.length > 70) {
- const computerNameLen = common.ReadShort(msg, 70);
- obj.agentInfo.computerName = Buffer.from(msg.substring(72, 72 + computerNameLen), 'binary').toString('utf8');
- //console.log('computerName', msg.length, computerNameLen, obj.agentInfo.computerName);
- } else {
- obj.agentInfo.computerName = '';
- //console.log('computerName-none');
- }
-
- obj.dbMeshKey = 'mesh/' + domain.id + '/' + obj.meshid;
- completeAgentConnection();
- } else if (cmd == 4) {
- if ((msg.length < 2) || ((obj.receivedCommands & 8) != 0)) return;
- obj.receivedCommands += 8; // Agent can't send the same command twice on the same connection ever. Block DOS attack path.
- // Agent already authenticated the server, wants to skip the server signature - which is great for server performance.
- } else if (cmd == 5) {
- // ServerID. Agent is telling us what serverid it expects. Useful if we have many server certificates.
- if ((msg.substring(2, 34) == parent.swarmCertificateHash256) || (msg.substring(2, 50) == parent.swarmCertificateHash384)) { obj.useSwarmCert = true; }
- } else if (cmd == 30) {
- // Agent Commit Date. This is future proofing. Can be used to change server behavior depending on the date range of the agent.
- try { obj.AgentCommitDate = Date.parse(msg.substring(2)) } catch (ex) { }
- //console.log('Connected Agent Commit Date: ' + msg.substring(2) + ", " + Date.parse(msg.substring(2)));
- }
- }
- });
-
- // If error, do nothing
- ws.on('error', function (err) { parent.parent.debug('agent', 'AGENT WSERR: ' + err); console.log('AGENT WSERR: ' + err); obj.close(0); });
-
- // If the mesh agent web socket is closed, clean up.
- ws.on('close', function (req) {
- parent.agentStats.agentClose++;
- if (obj.nodeid != null) {
- const agentId = (obj.agentInfo && obj.agentInfo.agentId) ? obj.agentInfo.agentId : 'Unknown';
- //console.log('Agent disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ') id=' + agentId);
- parent.parent.debug('agent', 'Agent disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ') id=' + agentId);
-
- // Log the agent disconnection if we are not testing agent update
- if (args.agentupdatetest == null) {
- if (parent.wsagentsDisconnections[obj.nodeid] == null) {
- parent.wsagentsDisconnections[obj.nodeid] = 1;
- } else {
- parent.wsagentsDisconnections[obj.nodeid] = ++parent.wsagentsDisconnections[obj.nodeid];
- }
- }
- }
- obj.close(0);
- });
-
- // Return the mesh for this device, in some cases, we may auto-create the mesh.
- function getMeshAutoCreate() {
- var mesh = parent.meshes[obj.dbMeshKey];
-
- // If the mesh was not found and we are in LAN mode, check of the domain can be corrected
- if ((args.lanonly == true) && (mesh == null)) {
- var smesh = obj.dbMeshKey.split('/');
- for (var i in parent.parent.config.domains) {
- mesh = parent.meshes['mesh/' + i + '/' + smesh[2]];
- if (mesh != null) {
- obj.domain = domain = parent.parent.config.domains[i];
- obj.meshid = smesh[2];
- obj.dbMeshKey = 'mesh/' + i + '/' + smesh[2];
- obj.dbNodeKey = 'node/' + domain.id + '/' + obj.nodeid;
- break;
- }
- }
- }
-
- if ((mesh == null) && (typeof domain.orphanagentuser == 'string')) {
- const adminUser = parent.users['user/' + domain.id + '/' + domain.orphanagentuser];
- if ((adminUser != null) && (adminUser.siteadmin == 0xFFFFFFFF)) {
- // Mesh name is hex instead of base64
- const meshname = obj.meshid.substring(0, 18);
-
- // Create a new mesh for this device
- const links = {};
- links[adminUser._id] = { name: adminUser.name, rights: 0xFFFFFFFF };
- mesh = { type: 'mesh', _id: obj.dbMeshKey, name: meshname, mtype: 2, desc: '', domain: domain.id, links: links };
- db.Set(mesh);
- parent.meshes[obj.dbMeshKey] = mesh;
-
- if (adminUser.links == null) adminUser.links = {};
- adminUser.links[obj.dbMeshKey] = { rights: 0xFFFFFFFF };
- db.SetUser(adminUser);
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [adminUser._id, obj.dbNodeKey]), obj, { etype: 'mesh', username: adminUser.name, meshid: obj.dbMeshKey, name: meshname, mtype: 2, desc: '', action: 'createmesh', links: links, msgid: 55, msgArgs: [obj.meshid], msg: "Created device group: " + obj.meshid, domain: domain.id });
- }
- } else {
- if ((mesh != null) && (mesh.deleted != null) && (mesh.links)) {
- // Must un-delete this mesh
- var ids = parent.CreateMeshDispatchTargets(mesh._id, [obj.dbNodeKey]);
-
- // See if users still exists, if so, add links to the mesh
- for (var userid in mesh.links) {
- const user = parent.users[userid];
- if (user) {
- if (user.links == null) { user.links = {}; }
- if (user.links[mesh._id] == null) {
- user.links[mesh._id] = { rights: mesh.links[userid].rights };
- ids.push(user._id);
- db.SetUser(user);
- }
- }
- }
-
- // Send out an event indicating this mesh was "created"
- parent.parent.DispatchEvent(ids, obj, { etype: 'mesh', meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, action: 'createmesh', links: mesh.links, msgid: 56, msgArgs: [mesh._id], msg: "Device group undeleted: " + mesh._id, domain: domain.id });
-
- // Mark the mesh as active
- delete mesh.deleted;
- db.Set(mesh);
- }
- }
- return mesh;
- }
-
- // Send a PING/PONG message
- function sendPing() { obj.send('{"action":"ping"}'); }
- function sendPong() { obj.send('{"action":"pong"}'); }
-
- // Once we get all the information about an agent, run this to hook everything up to the server
- function completeAgentConnection() {
- if ((obj.authenticated != 1) || (obj.meshid == null) || obj.pendingCompleteAgentConnection || (obj.agentInfo == null)) { return; }
- obj.pendingCompleteAgentConnection = true;
-
- // Setup the agent PING/PONG timers
- if ((typeof args.agentping == 'number') && (obj.pingtimer == null)) { obj.pingtimer = setInterval(sendPing, args.agentping * 1000); }
- else if ((typeof args.agentpong == 'number') && (obj.pongtimer == null)) { obj.pongtimer = setInterval(sendPong, args.agentpong * 1000); }
-
- // If this is a recovery agent
- if (obj.agentInfo.capabilities & 0x40) {
- // Inform mesh agent that it's authenticated.
- delete obj.pendingCompleteAgentConnection;
- obj.authenticated = 2;
- obj.sendBinary(common.ShortToStr(4));
-
- // Ask for mesh core hash.
- obj.sendBinary(common.ShortToStr(11) + common.ShortToStr(0));
- return;
- }
-
- // Check if we have too many agent sessions
- if (typeof domain.limits.maxagentsessions == 'number') {
- // Count the number of agent sessions for this domain
- var domainAgentSessionCount = 0;
- for (var i in parent.wsagents) { if (parent.wsagents[i].domain.id == domain.id) { domainAgentSessionCount++; } }
-
- // Check if we have too many user sessions
- if (domainAgentSessionCount >= domain.limits.maxagentsessions) {
- // Too many, hold the connection.
- parent.agentStats.agentMaxSessionHoldCount++;
- return;
- }
- }
-
- /*
- // Check that the mesh exists
- var mesh = parent.meshes[obj.dbMeshKey];
- if (mesh == null) {
- var holdConnection = true;
- if (typeof domain.orphanagentuser == 'string') {
- var adminUser = parent.users['user/' + domain.id + '/' + args.orphanagentuser];
- if ((adminUser != null) && (adminUser.siteadmin == 0xFFFFFFFF)) {
- // Create a new mesh for this device
- holdConnection = false;
- var links = {};
- links[user._id] = { name: adminUser.name, rights: 0xFFFFFFFF };
- mesh = { type: 'mesh', _id: obj.dbMeshKey, name: obj.meshid, mtype: 2, desc: '', domain: domain.id, links: links };
- db.Set(mesh);
- parent.meshes[obj.meshid] = mesh;
- parent.parent.AddEventDispatch(parent.CreateMeshDispatchTargets(obj.meshid, [obj.dbNodeKey]), ws);
-
- if (adminUser.links == null) user.links = {};
- adminUser.links[obj.meshid] = { rights: 0xFFFFFFFF };
- //adminUser.subscriptions = parent.subscribe(adminUser._id, ws);
- db.SetUser(user);
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(meshid, [user._id, obj.dbNodeKey]), obj, { etype: 'mesh', username: user.name, meshid: obj.meshid, name: obj.meshid, mtype: 2, desc: '', action: 'createmesh', links: links, msg: 'Mesh created: ' + obj.meshid, domain: domain.id });
- }
- }
-
- if (holdConnection == true) {
- // If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
- parent.parent.debug('agent', 'Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
- console.log('Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
- return;
- }
- }
- if (mesh.mtype != 2) { // If we disconnect, the agnet will just reconnect. We need to log this or tell agent to connect in a few hours.
- parent.parent.debug('agent', 'Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
- console.log('Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
- return;
- }
- */
-
- // Check that the node exists
- db.Get(obj.dbNodeKey, async function (err, nodes) {
- if (obj.agentInfo == null) { return; }
- var device, mesh;
- var nodeExists = Boolean(false);
-
- if ((nodes == null) || (nodes.length == 0)) {
- if(domain.preventduplicatedevices){
-
- const existingNodes = await new Promise((resolve, reject) => {
- db.GetNodeByComputerName(domain.id, obj.agentInfo.computerName, (err, result) => {
- if (err) reject(err);
- else resolve(result);
- });
- });
-
- if (!existingNodes || existingNodes.length === 0) {
- // Device does not exist with the name
- nodeExists = false;
- } else {
- console.log("Device already exists");
- // Remove nodes with the same name
- existingNodes.forEach((eNode) => {
-
- parent.parent.debug('agent', 'Removing old dublicated node (' + eNode.rname + ', ' + eNode._id + ').');
-
- db.Remove(eNode._id); // Remove node with that id
- db.Remove('if' + eNode._id); // Remove interface information
- db.Remove('nt' + eNode._id); // Remove notes
- db.Remove('lc' + eNode._id); // Remove last connect time
- db.Remove('si' + eNode._id); // Remove system information
- db.Remove('al' + eNode._id); // Remove error log last time
- if (db.RemoveSMBIOS) { db.RemoveSMBIOS(eNode._id); } // Remove SMBios data
- db.RemoveAllNodeEvents(eNode._id); // Remove all events for this node
- db.removeAllPowerEventsForNode(eNode._id); // Remove all power events for this node
-
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(eNode.meshid, [eNode._id]), obj, { etype: 'node', action: 'removenode', nodeid: eNode._id, domain: eNode.domain, nolog: 1 });
- });
-
- // Set mesh from previous node
- obj.dbMeshKey = existingNodes[0].meshid
-
- nodeExists = false;
-
- }
- } else {
- nodeExists = false;
- }
-
- } else {
- nodeExists = true;
- }
-
- // See if this node exists in the database
- if (nodeExists == false) {
- // This device does not exist, use the meshid given by the device
-
- // Check if we already have too many devices for this domain
- if (domain.limits && (typeof domain.limits.maxdevices == 'number')) {
- db.isMaxType(domain.limits.maxdevices, 'node', domain.id, function (ismax, count) {
- if (ismax == true) {
- // Too many devices in this domain.
- parent.agentStats.maxDomainDevicesReached++;
- } else {
- // We are under the limit, create the new device.
- completeAgentConnection2();
- }
- });
- } else {
- completeAgentConnection2();
- }
- return;
- } else {
- device = nodes[0];
- obj.name = device.name;
-
- // This device exists, meshid given by the device must be ignored, use the server side one.
- if ((device.meshid != null) && (device.meshid != obj.dbMeshKey)) {
- obj.dbMeshKey = device.meshid;
- obj.meshid = device.meshid.split('/')[2];
- }
-
- // See if this mesh exists, if it does not we may want to create it.
- mesh = getMeshAutoCreate();
-
- // Check if the mesh exists
- if (mesh == null) {
- // If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
- parent.agentStats.invalidDomainMesh2Count++;
- parent.setAgentIssue(obj, "invalidDomainMesh2");
- parent.parent.debug('agent', 'Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
- console.log('Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
- return;
- }
-
- // Check if the mesh is the right type
- if (mesh.mtype != 2) {
- // If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
- parent.agentStats.invalidMeshType2Count++;
- parent.setAgentIssue(obj, "invalidMeshType2");
- parent.parent.debug('agent', 'Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
- console.log('Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
- return;
- }
-
- // Mark when this device connected
- obj.connectTime = Date.now();
-
- // Device already exists, look if changes have occured
- var changes = [], change = 0, log = 0;
- if (device.agent == null) { device.agent = { ver: obj.agentInfo.agentVersion, id: obj.agentInfo.agentId, caps: obj.agentInfo.capabilities }; change = 1; }
- if (device.rname != obj.agentInfo.computerName) { device.rname = obj.agentInfo.computerName; change = 1; changes.push('computer name'); }
- if (device.agent.ver != obj.agentInfo.agentVersion) { device.agent.ver = obj.agentInfo.agentVersion; change = 1; changes.push('agent version'); }
- if (device.agent.id != obj.agentInfo.agentId) { device.agent.id = obj.agentInfo.agentId; change = 1; changes.push('agent type'); }
- if ((device.agent.caps & 24) != (obj.agentInfo.capabilities & 24)) { device.agent.caps = obj.agentInfo.capabilities; change = 1; changes.push('agent capabilities'); } // If agent console or javascript support changes, update capabilities
- // We want the server name to be sync'ed to the hostname or the --agentName
- // (flag 16 allows to override the name until next connection)
- if (mesh.flags && (mesh.flags & 2)) {
- var preferredName = (mesh.flags & 8) && obj.agentName || obj.agentInfo.computerName;
- if (device.name != preferredName) {device.name = preferredName; change = 1; }
- }
- if (device.ip != obj.remoteaddr) { device.ip = obj.remoteaddr; change = 1; }
-
- if (change == 1) {
- // Do some clean up if needed, these values should not be in the database.
- if (device.conn != null) { delete device.conn; }
- if (device.pwr != null) { delete device.pwr; }
- if (device.agct != null) { delete device.agct; }
- if (device.cict != null) { delete device.cict; }
-
- // Save the updated device in the database
- db.Set(device);
-
- // If this is a temporary device, don't log changes
- if (obj.agentInfo.capabilities & 0x20) { log = 0; }
-
- // Event the node change
- var event = { etype: 'node', action: 'changenode', nodeid: obj.dbNodeKey, domain: domain.id, node: parent.CloneSafeNode(device) };
- if (log == 0) { event.nolog = 1; } else { event.msg = 'Changed device ' + device.name + ' from group ' + mesh.name + ': ' + changes.join(', '); }
- if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(device.meshid, [obj.dbNodeKey]), obj, event);
- }
- }
-
- completeAgentConnection3(device, mesh);
- });
- }
-
- function completeAgentConnection2() {
- // See if this mesh exists, if it does not we may want to create it.
- var mesh = getMeshAutoCreate();
-
- // Check if the mesh exists
- if (mesh == null) {
- // If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
- parent.agentStats.invalidDomainMeshCount++;
- parent.setAgentIssue(obj, "invalidDomainMesh");
- parent.parent.debug('agent', 'Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
- console.log('Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
- return;
- }
-
- // Check if the mesh is the right type
- if (mesh.mtype != 2) {
- // If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
- parent.agentStats.invalidMeshTypeCount++;
- parent.setAgentIssue(obj, "invalidMeshType");
- parent.parent.debug('agent', 'Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
- console.log('Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
- return;
- }
-
- // Mark when this device connected
- obj.connectTime = Date.now();
-
- // This node does not exist, create it.
- var agentName = obj.agentName ? obj.agentName : obj.agentInfo.computerName;
- var device = { type: 'node', mtype: mesh.mtype, _id: obj.dbNodeKey, icon: obj.agentInfo.platformType, meshid: obj.dbMeshKey, name: agentName, rname: obj.agentInfo.computerName, domain: domain.id, agent: { ver: obj.agentInfo.agentVersion, id: obj.agentInfo.agentId, caps: obj.agentInfo.capabilities }, host: null };
- db.Set(device);
-
- // Event the new node
- if (obj.agentInfo.capabilities & 0x20) {
- // This is a temporary agent, don't log.
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, { etype: 'node', action: 'addnode', node: device, domain: domain.id, nolog: 1 });
- } else {
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, { etype: 'node', action: 'addnode', node: device, msgid: 57, msgArgs: [obj.agentInfo.computerName, mesh.name], msg: ('Added device ' + obj.agentInfo.computerName + ' to device group ' + mesh.name), domain: domain.id });
- }
-
- completeAgentConnection3(device, mesh);
- }
-
- function completeAgentConnection3(device, mesh) {
- // Check if this agent is already connected
- const dupAgent = parent.wsagents[obj.dbNodeKey];
- parent.wsagents[obj.dbNodeKey] = obj;
- if (dupAgent) {
- // Record duplicate agents
- if (parent.duplicateAgentsLog[obj.dbNodeKey] == null) {
- if (dupAgent.remoteaddr == obj.remoteaddr) {
- parent.duplicateAgentsLog[obj.dbNodeKey] = { name: device.name, group: mesh.name, ip: [obj.remoteaddr], count: 1 };
- } else {
- parent.duplicateAgentsLog[obj.dbNodeKey] = { name: device.name, group: mesh.name, ip: [obj.remoteaddr, dupAgent.remoteaddr], count: 1 };
- }
- } else {
- parent.duplicateAgentsLog[obj.dbNodeKey].name = device.name;
- parent.duplicateAgentsLog[obj.dbNodeKey].group = mesh.name;
- parent.duplicateAgentsLog[obj.dbNodeKey].count++;
- if (parent.duplicateAgentsLog[obj.dbNodeKey].ip.indexOf(obj.remoteaddr) == -1) { parent.duplicateAgentsLog[obj.dbNodeKey].ip.push(obj.remoteaddr); }
- }
-
- // Close the duplicate agent
- parent.agentStats.duplicateAgentCount++;
- parent.setAgentIssue(obj, 'duplicateAgent');
- if (obj.nodeid != null) { parent.parent.debug('agent', 'Duplicate agent ' + obj.nodeid + ' (' + obj.remoteaddrport + ')'); }
- dupAgent.close(3);
- } else {
- // Indicate the agent is connected
- parent.parent.SetConnectivityState(obj.dbMeshKey, obj.dbNodeKey, obj.connectTime, 1, 1, null, { remoteaddrport: obj.remoteaddrport, name: device.name });
- }
-
- // We are done, ready to communicate with this agent
- delete obj.pendingCompleteAgentConnection;
- obj.authenticated = 2;
-
- // Check how many times this agent disconnected in the last few minutes.
- const disconnectCount = parent.wsagentsDisconnections[obj.nodeid];
- if (disconnectCount > 6) {
- parent.parent.debug('agent', 'Agent in big trouble: NodeId=' + obj.nodeid + ', IP=' + obj.remoteaddrport + ', Agent=' + obj.agentInfo.agentId + '.');
- console.log('Agent in big trouble: NodeId=' + obj.nodeid + ', IP=' + obj.remoteaddrport + ', Agent=' + obj.agentInfo.agentId + '.');
- parent.agentStats.agentInBigTrouble++;
- // TODO: Log or do something to recover?
- return;
- }
-
- // Command 4, inform mesh agent that it's authenticated.
- obj.sendBinary(common.ShortToStr(4));
-
- // Not sure why, but in rare cases, obj.agentInfo is undefined here.
- if ((obj.agentInfo == null) || (typeof obj.agentInfo.capabilities != 'number')) { return; } // This is an odd case.
- obj.agentExeInfo = parent.parent.meshAgentBinaries[obj.agentInfo.agentId];
- if (domain.meshAgentBinaries && domain.meshAgentBinaries[obj.agentInfo.agentId]) { obj.agentExeInfo = domain.meshAgentBinaries[obj.agentInfo.agentId]; }
-
- // Check if this agent is reconnecting too often.
- if (disconnectCount > 4) {
- // Too many disconnections, this agent has issues. Just clear the core.
- obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0));
- parent.parent.debug('agent', 'Agent in trouble: NodeId=' + obj.nodeid + ', IP=' + obj.remoteaddrport + ', Agent=' + obj.agentInfo.agentId + '.');
- parent.agentStats.agentInTrouble++;
- //console.log('Agent in trouble: NodeId=' + obj.nodeid + ', IP=' + obj.remoteaddrport + ', Agent=' + obj.agentInfo.agentId + '.');
- // TODO: Log or do something to recover?
- return;
- }
-
- // Check if we need to make an native update check
- var corename = null;
- if (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId] != null) {
- corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].core;
- } else {
- // MeshCommand_CoreModule, ask mesh agent to clear the core
- obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0));
- }
-
- if ((obj.agentExeInfo != null) && (obj.agentExeInfo.update == true)) {
- // Ask the agent for it's executable binary hash
- obj.sendBinary(common.ShortToStr(12) + common.ShortToStr(0));
- } else {
- // Check the mesh core, if the agent is capable of running one
- if (((obj.agentInfo.capabilities & 16) != 0) && (corename != null)) {
- obj.sendBinary(common.ShortToStr(11) + common.ShortToStr(0)); // Command 11, ask for mesh core hash.
- } else {
- agentCoreIsStable(); // No updates needed, agent is ready to go.
- }
- }
- }
-
- // Indicate to the agent that we want to check Intel AMT configuration
- // This may trigger a CIRA-LMS tunnel from the agent so the server can inspect the device.
- obj.sendUpdatedIntelAmtPolicy = function (policy) {
- if (obj.agentExeInfo && (obj.agentExeInfo.amt == true)) { // Only send Intel AMT policy to agents what could have AMT.
- if (policy == null) { var mesh = parent.meshes[obj.dbMeshKey]; if (mesh == null) return; policy = mesh.amt; }
- if ((policy != null) && (policy.type != 0)) {
- const cookie = parent.parent.encodeCookie({ a: 'apf', n: obj.dbNodeKey, m: obj.dbMeshKey }, parent.parent.loginCookieEncryptionKey);
- try { obj.send(JSON.stringify({ action: 'amtconfig', user: '**MeshAgentApfTunnel**', pass: cookie })); } catch (ex) { }
- }
- }
- }
-
- function recoveryAgentCoreIsStable(mesh) {
- parent.agentStats.recoveryCoreIsStableCount++;
-
- // Recovery agent is doing ok, lets perform main agent checking.
- //console.log('recoveryAgentCoreIsStable()');
-
- // Fetch the the real agent nodeid
- db.Get('da' + obj.dbNodeKey, function (err, nodes, self) {
- if ((nodes != null) && (nodes.length == 1)) {
- self.realNodeKey = nodes[0].raid;
-
- // Get agent connection state
- var agentConnected = false;
- var state = parent.parent.GetConnectivityState(self.realNodeKey);
- if (state) { agentConnected = ((state.connectivity & 1) != 0) }
-
- self.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: self.realNodeKey, agent: agentConnected } }));
- } else {
- self.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: null } }));
- }
- }, obj);
- }
-
- function agentCoreIsStable() {
- parent.agentStats.coreIsStableCount++;
-
- // Check that the mesh exists
- const mesh = parent.meshes[obj.dbMeshKey];
- if (mesh == null) {
- parent.agentStats.meshDoesNotExistCount++;
- parent.setAgentIssue(obj, "meshDoesNotExist");
- // TODO: Mark this agent as part of a mesh that does not exists.
- return; // Probably not worth doing anything else. Hold this agent.
- }
-
- // Check if this is a recovery agent
- if (obj.agentInfo.capabilities & 0x40) {
- recoveryAgentCoreIsStable(mesh);
- return;
- }
-
- // Fetch the the diagnostic agent nodeid
- db.Get('ra' + obj.dbNodeKey, function (err, nodes) {
- if ((nodes != null) && (nodes.length == 1)) {
- obj.diagnosticNodeKey = nodes[0].daid;
- obj.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: obj.diagnosticNodeKey } }));
- }
- });
-
- // Indicate that we want to check the Intel AMT configuration
- // This may trigger a CIRA-LMS tunnel to the server for further processing
- obj.sendUpdatedIntelAmtPolicy();
-
- // Fetch system information
- db.GetHash('si' + obj.dbNodeKey, function (err, results) {
- if ((results != null) && (results.length == 1)) { obj.send(JSON.stringify({ action: 'sysinfo', hash: results[0].hash })); } else { obj.send(JSON.stringify({ action: 'sysinfo' })); }
- });
-
- // Agent error log dump
- if (parent.parent.agentErrorLog != null) {
- db.Get('al' + obj.dbNodeKey, function (err, docs) { // Agent Log
- if ((docs != null) && (docs.length == 1) && (typeof docs[0].lastEvent)) {
- obj.send('{"action":"errorlog","startTime":' + docs[0].lastEvent + '}'); // Ask all events after a given time
- } else {
- obj.send('{"action":"errorlog"}'); // Ask all
- }
- });
- }
-
- // Set agent core dump
- if ((parent.parent.config.settings != null) && ((parent.parent.config.settings.agentcoredump === true) || (parent.parent.config.settings.agentcoredump === false))) {
- obj.send(JSON.stringify({ action: 'coredump', value: parent.parent.config.settings.agentcoredump }));
- if (parent.parent.config.settings.agentcoredump === true) {
- // Check if we requested a core dump file in the last minute, if not, ask if one is present.
- if ((parent.lastCoreDumpRequest == null) || ((Date.now() - parent.lastCoreDumpRequest) >= 60000)) { obj.send(JSON.stringify({ action: 'getcoredump' })); }
- }
- }
-
- // Do this if IP location is enabled on this domain TODO: Set IP location per device group?
- if (domain.iplocation == true) {
- // Check if we already have IP location information for this node
- db.Get('iploc_' + obj.remoteaddr, function (err, iplocs) {
- if ((iplocs != null) && (iplocs.length == 1)) {
- // We have a location in the database for this remote IP
- const iploc = iplocs[0], x = {};
- if ((iploc != null) && (iploc.ip != null) && (iploc.loc != null)) {
- x.publicip = iploc.ip;
- x.iploc = iploc.loc + ',' + (Math.floor((new Date(iploc.date)) / 1000));
- ChangeAgentLocationInfo(x);
- }
- } else {
- // Check if we need to ask for the IP location
- var doIpLocation = 0;
- if (obj.iploc == null) {
- doIpLocation = 1;
- } else {
- const loc = obj.iploc.split(',');
- if (loc.length < 3) {
- doIpLocation = 2;
- } else {
- var t = new Date((parseFloat(loc[2]) * 1000)), now = Date.now();
- t.setDate(t.getDate() + 20);
- if (t < now) { doIpLocation = 3; }
- }
- }
-
- // If we need to ask for IP location, see if we have the quota to do it.
- if (doIpLocation > 0) {
- db.getValueOfTheDay('ipLocationRequestLimitor', 10, function (ipLocationLimitor) {
- if ((ipLocationLimitor != null) && (ipLocationLimitor.value > 0)) {
- ipLocationLimitor.value--;
- db.Set(ipLocationLimitor);
- obj.send(JSON.stringify({ action: 'iplocation' }));
- }
- });
- }
- }
- });
- }
-
- // Indicate server information to the agent.
- var serverInfo = { action: 'serverInfo' };
- if ((typeof domain.terminal == 'object') && (typeof domain.terminal.launchcommand == 'object')) {
- // Send terminal starting command
- serverInfo.termlaunchcommand = {};
- if (typeof domain.terminal.launchcommand.linux == 'string') { serverInfo.termlaunchcommand.linux = domain.terminal.launchcommand.linux; }
- if (typeof domain.terminal.launchcommand.darwin == 'string') { serverInfo.termlaunchcommand.darwin = domain.terminal.launchcommand.darwin; }
- if (typeof domain.terminal.launchcommand.freebsd == 'string') { serverInfo.termlaunchcommand.freebsd = domain.terminal.launchcommand.freebsd; }
- }
- // Enable agent self guest sharing if allowed
- if (domain.agentselfguestsharing) { serverInfo.agentSelfGuestSharing = true; }
- obj.send(JSON.stringify(serverInfo));
-
- // Plug in handler
- if (parent.parent.pluginHandler != null) {
- parent.parent.pluginHandler.callHook('hook_agentCoreIsStable', obj, parent);
- }
- }
-
- // Get the web certificate private key hash for the specified domain
- function getWebCertHash(domain) {
- const hash = parent.webCertificateHashs[domain.id];
- if (hash != null) return hash;
- return parent.webCertificateHash;
- }
-
- // Get the web certificate hash for the specified domain
- function getWebCertFullHash(domain) {
- const hash = parent.webCertificateFullHashs[domain.id];
- if (hash != null) return hash;
- return parent.webCertificateFullHash;
- }
-
- // Verify the agent signature
- function processAgentSignature(msg) {
- if (isIgnoreHashCheck() == false) {
- var verified = false;
-
- // This agent did not report a valid TLS certificate hash, fail now.
- if (obj.agentSeenCerthash == null) return false;
-
- // Raw RSA signatures have an exact length of 256 or 384. PKCS7 is larger.
- if ((msg.length != 384) && (msg.length != 256)) {
- // Verify a PKCS7 signature.
- var msgDer = null;
- try { msgDer = forge.asn1.fromDer(forge.util.createBuffer(msg, 'binary')); } catch (ex) { }
- if (msgDer != null) {
- try {
- const p7 = forge.pkcs7.messageFromAsn1(msgDer);
- const sig = p7.rawCapture.signature;
-
- // Verify with key hash
- var buf = Buffer.from(obj.agentSeenCerthash + obj.nonce + obj.agentnonce, 'binary');
- var verifier = parent.crypto.createVerify('RSA-SHA384');
- verifier.update(buf);
- verified = verifier.verify(obj.unauth.nodeCertPem, sig, 'binary');
- if (verified !== true) {
- // Not a valid signature
- parent.agentStats.invalidPkcsSignatureCount++;
- parent.setAgentIssue(obj, "invalidPkcsSignature");
- return false;
- }
- } catch (ex) { };
- }
- }
-
- if (verified == false) {
- // Verify the RSA signature. This is the fast way, without using forge.
- const verify = parent.crypto.createVerify('SHA384');
- verify.end(Buffer.from(obj.agentSeenCerthash + obj.nonce + obj.agentnonce, 'binary')); // Test using the private key hash
- if (verify.verify(obj.unauth.nodeCertPem, Buffer.from(msg, 'binary')) !== true) {
- parent.agentStats.invalidRsaSignatureCount++;
- parent.setAgentIssue(obj, "invalidRsaSignature");
- return false;
- }
- }
- }
-
- // Connection is a success, clean up
- obj.nodeid = obj.unauth.nodeid;
- obj.dbNodeKey = 'node/' + domain.id + '/' + obj.nodeid;
- delete obj.nonce;
- delete obj.agentnonce;
- delete obj.unauth;
- delete obj.receivedCommands;
- delete obj.agentSeenCerthash;
- if (obj.unauthsign) delete obj.unauthsign;
- parent.agentStats.verifiedAgentConnectionCount++;
- parent.parent.debug('agent', 'Verified agent connection to ' + obj.nodeid + ' (' + obj.remoteaddrport + ').');
- obj.authenticated = 1;
- return true;
- }
-
- // Process incoming agent JSON data
- function processAgentData(msg) {
- if (obj.agentInfo == null) return;
- var i, str = msg.toString('utf8'), command = null;
- if (str[0] == '{') {
- try { command = JSON.parse(str); } catch (ex) {
- // If the command can't be parsed, ignore it.
- parent.agentStats.invalidJsonCount++;
- parent.setAgentIssue(obj, "invalidJson (" + str.length + "): " + str);
- parent.parent.debug('agent', 'Unable to parse agent JSON (' + obj.remoteaddrport + ')');
- console.log('Unable to parse agent JSON (' + obj.remoteaddrport + '): ' + str, ex);
- return;
- }
- if (typeof command != 'object') { return; }
- switch (command.action) {
- case 'msg':
- {
- // If the same console command is processed many times, kick out this agent.
- // This is a safety mesure to guard against the agent DOS'ing the server.
- if (command.type == 'console') {
- if (obj.consoleKickValue == command.value) {
- if (obj.consoleKickCount) { obj.consoleKickCount++; } else { obj.consoleKickCount = 1; }
- if (obj.consoleKickCount > 30) { obj.close(); return; } // 30 identical console messages received, kick out this agent.
- } else {
- obj.consoleKickValue = command.value;
- }
- }
-
- // Route a message
- parent.routeAgentCommand(command, obj.domain.id, obj.dbNodeKey, obj.dbMeshKey);
- break;
- }
- case 'coreinfo':
- {
- // Sent by the agent to update agent information
- ChangeAgentCoreInfo(command);
-
- if ((obj.agentCoreUpdate === true) && (obj.agentExeInfo != null) && (typeof obj.agentExeInfo.url == 'string')) {
- // Agent update. The recovery core was loaded in the agent, send a command to update the agent
- parent.parent.taskLimiter.launch(function (argument, taskid, taskLimiterQueue) { // Medium priority task
- // If agent disconnection, complete and exit now.
- if ((obj.authenticated != 2) || (obj.agentExeInfo == null)) { parent.parent.taskLimiter.completed(taskid); return; }
-
- // Agent update. The recovery core was loaded in the agent, send a command to update the agent
- obj.agentCoreUpdateTaskId = taskid;
- const url = '*' + require('url').parse(obj.agentExeInfo.url).path;
- var cmd = { action: 'agentupdate', url: url, hash: obj.agentExeInfo.hashhex };
- parent.parent.debug('agentupdate', "Sending agent update url: " + cmd.url);
-
- // Add the hash
- if (obj.agentExeInfo.fileHash != null) { cmd.hash = obj.agentExeInfo.fileHashHex; } else { cmd.hash = obj.agentExeInfo.hashhex; }
-
- // Add server TLS cert hash
- if (isIgnoreHashCheck() == false) {
- const tlsCertHash = parent.webCertificateFullHashs[domain.id];
- if (tlsCertHash != null) { cmd.servertlshash = Buffer.from(tlsCertHash, 'binary').toString('hex'); }
- }
-
- // Send the agent update command
- obj.send(JSON.stringify(cmd));
- }, null, 1);
- }
- break;
- }
- case 'smbios':
- {
- // SMBIOS information must never be saved when NeDB is in use. NeDB will currupt that database.
- if (db.SetSMBIOS == null) break;
-
- // See if we need to save SMBIOS information
- if (domain.smbios === true) {
- // Store the RAW SMBios table of this computer
- // Perform sanity checks before storing
- try {
- for (var i in command.value) { var k = parseInt(i); if ((k != i) || (i > 255) || (typeof command.value[i] != 'object') || (command.value[i].length == null) || (command.value[i].length > 1024) || (command.value[i].length < 0)) { delete command.value[i]; } }
- db.SetSMBIOS({ _id: obj.dbNodeKey, domain: domain.id, time: new Date(), value: command.value });
- } catch (ex) { }
- }
-
- // Event the node interface information change (This is a lot of traffic, probably don't need this).
- //parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.meshid, [obj.dbNodeKey]), obj, { action: 'smBiosChange', nodeid: obj.dbNodeKey, domain: domain.id, smbios: command.value, nolog: 1 });
-
- break;
- }
- case 'netinfo':
- {
- // Check if network information is present
- if ((command.netif2 == null) && (command.netif == null)) return;
-
- // Escape any field names that have special characters
- if (command.netif2 != null) {
- for (var i in command.netif2) {
- var esc = common.escapeFieldName(i);
- if (esc !== i) { command.netif2[esc] = command.netif2[i]; delete command.netif2[i]; }
- }
- }
-
- // Sent by the agent to update agent network interface information
- delete command.action;
- command.updateTime = Date.now();
- command._id = 'if' + obj.dbNodeKey;
- command.domain = domain.id;
- command.type = 'ifinfo';
- db.Set(command);
-
- // Event the node interface information change
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.meshid, [obj.dbNodeKey]), obj, { action: 'ifchange', nodeid: obj.dbNodeKey, domain: domain.id, nolog: 1 });
-
- break;
- }
- case 'iplocation':
- {
- // Sent by the agent to update location information
- if ((command.type == 'publicip') && (command.value != null) && (typeof command.value == 'object') && (command.value.ip) && (command.value.loc)) {
- var x = {};
- x.publicip = command.value.ip;
- x.iploc = command.value.loc + ',' + (Math.floor(Date.now() / 1000));
- ChangeAgentLocationInfo(x);
- command.value._id = 'iploc_' + command.value.ip;
- command.value.type = 'iploc';
- command.value.date = Date.now();
- db.Set(command.value); // Store the IP to location data in the database
- // Sample Value: { ip: '192.55.64.246', city: 'Hillsboro', region: 'Oregon', country: 'US', loc: '45.4443,-122.9663', org: 'AS4983 Intel Corporation', postal: '97123' }
- }
- break;
- }
- case 'mc1migration':
- {
- if (command.oldnodeid.length != 64) break;
- const oldNodeKey = 'node//' + command.oldnodeid.toLowerCase();
- db.Get(oldNodeKey, function (err, nodes) {
- if ((nodes == null) || (nodes.length != 1)) return;
- const node = nodes[0];
- if (node.meshid == obj.dbMeshKey) {
- // Update the device name & host
- const newNode = { "name": node.name };
- if (node.intelamt != null) { newNode.intelamt = node.intelamt; }
- ChangeAgentCoreInfo(newNode);
-
- // Delete this node including network interface information and events
- db.Remove(node._id);
- db.Remove('if' + node._id);
-
- // Event node deletion
- const change = 'Migrated device ' + node.name;
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(node.meshid, [obj.dbNodeKey]), obj, { etype: 'node', action: 'removenode', nodeid: node._id, msg: change, domain: node.domain });
- }
- });
- break;
- }
- case 'openUrl':
- {
- // Sent by the agent to return the status of a open URL action.
- // Nothing is done right now.
- break;
- }
- case 'log':
- {
- // Log a value in the event log
- if ((typeof command.msg == 'string') && (command.msg.length < 4096)) {
- var event = { etype: 'node', action: 'agentlog', nodeid: obj.dbNodeKey, domain: domain.id, msg: command.msg };
- if (typeof command.msgid == 'number') { event.msgid = command.msgid; }
- if (typeof command.guestname == 'string') { event.guestname = command.guestname; }
- if (Array.isArray(command.msgArgs)) { event.msgArgs = command.msgArgs; }
- if (typeof command.remoteaddr == 'string') { event.remoteaddr = command.remoteaddr; }
- var targets = parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]);
- if (typeof command.userid == 'string') {
- var loguser = parent.users[command.userid];
- if (loguser) { event.userid = command.userid; event.username = loguser.name; targets.push(command.userid); }
- }
- if (typeof command.xuserid == 'string') {
- var xloguser = parent.users[command.xuserid];
- if (xloguser) { targets.push(command.xuserid); }
- }
- if ((typeof command.sessionid == 'string') && (command.sessionid.length < 500)) { event.sessionid = command.sessionid; }
- parent.parent.DispatchEvent(targets, obj, event);
-
- // If this is a help request, see if we need to email notify anyone
- if (event.msgid == 98) {
- // Get the node and change it if needed
- db.Get(obj.dbNodeKey, function (err, nodes) { // TODO: THIS IS A BIG RACE CONDITION HERE, WE NEED TO FIX THAT. If this call is made twice at the same time on the same device, data will be missed.
- if ((nodes == null) || (nodes.length != 1)) { delete obj.deviceChanging; return; }
- const device = nodes[0];
- if (typeof device.name == 'string') { parent.parent.NotifyUserOfDeviceHelpRequest(domain, device.meshid, device._id, device.name, command.msgArgs[0], command.msgArgs[1]); }
- });
- }
- }
- break;
- }
- case 'ping': { sendPong(); break; }
- case 'pong': { break; }
- case 'getScript':
- {
- // Used by the agent to get configuration scripts.
- if (command.type == 1) {
- parent.getCiraConfigurationScript(obj.dbMeshKey, function (script) {
- obj.send(JSON.stringify({ action: 'getScript', type: 1, script: script.toString() }));
- });
- } else if (command.type == 2) {
- parent.getCiraCleanupScript(function (script) {
- obj.send(JSON.stringify({ action: 'getScript', type: 2, script: script.toString() }));
- });
- }
- break;
- }
- case 'diagnostic':
- {
- if (typeof command.value == 'object') {
- switch (command.value.command) {
- case 'register': {
- // Only main agent can do this
- if (((obj.agentInfo.capabilities & 0x40) == 0) && (typeof command.value.value == 'string') && (command.value.value.length == 64)) {
- // Store links to diagnostic agent id
- var daNodeKey = 'node/' + domain.id + '/' + db.escapeBase64(command.value.value);
- db.Set({ _id: 'da' + daNodeKey, domain: domain.id, time: obj.connectTime, raid: obj.dbNodeKey }); // DiagnosticAgent --> Agent
- db.Set({ _id: 'ra' + obj.dbNodeKey, domain: domain.id, time: obj.connectTime, daid: daNodeKey }); // Agent --> DiagnosticAgent
- }
- break;
- }
- case 'query': {
- // Only the diagnostic agent can do
- if ((obj.agentInfo.capabilities & 0x40) != 0) {
- // Return nodeid of main agent + connection status
- db.Get('da' + obj.dbNodeKey, function (err, nodes) {
- if ((nodes != null) && (nodes.length == 1)) {
- obj.realNodeKey = nodes[0].raid;
-
- // Get agent connection state
- var agentConnected = false;
- var state = parent.parent.GetConnectivityState(obj.realNodeKey);
- if (state) { agentConnected = ((state.connectivity & 1) != 0) }
-
- obj.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: obj.realNodeKey, agent: agentConnected } }));
- } else {
- obj.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: null } }));
- }
- });
- }
- break;
- }
- case 'log': {
- if (((obj.agentInfo.capabilities & 0x40) != 0) && (typeof command.value.value == 'string') && (command.value.value.length < 256)) {
- // If this is a diagnostic agent, log the event in the log of the main agent
- var event = { etype: 'node', action: 'diagnostic', nodeid: obj.realNodeKey, snodeid: obj.dbNodeKey, domain: domain.id, msg: command.value.value };
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, event);
- }
- break;
- }
- }
- }
- break;
- }
- case 'sysinfo': {
- if ((typeof command.data == 'object') && (typeof command.data.hash == 'string')) {
- // Validate command.data.
- if (common.validateObjectForMongo(command.data, 1024) == false) break;
-
- // Save to database
- command.data._id = 'si' + obj.dbNodeKey;
- command.data.type = 'sysinfo';
- command.data.domain = domain.id;
- command.data.time = Date.now();
- db.Set(command.data); // Update system information in the database.
-
- // Event the new sysinfo hash, this will notify everyone that the sysinfo document was changed
- var event = { etype: 'node', action: 'sysinfohash', nodeid: obj.dbNodeKey, domain: domain.id, hash: command.data.hash, nolog: 1 };
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, event);
- }
- break;
- }
- case 'sysinfocheck': {
- // Check system information update
- db.GetHash('si' + obj.dbNodeKey, function (err, results) {
- if ((results != null) && (results.length == 1)) { obj.send(JSON.stringify({ action: 'sysinfo', hash: results[0].hash })); } else { obj.send(JSON.stringify({ action: 'sysinfo' })); }
- });
- break;
- }
- case 'sessions': {
- // This is a list of sessions provided by the agent
- if (obj.sessions == null) { obj.sessions = {}; }
- if (typeof command.value != null) {
- if (command.type == 'kvm') { obj.sessions.kvm = command.value; }
- else if (command.type == 'terminal') { obj.sessions.terminal = command.value; }
- else if (command.type == 'files') { obj.sessions.files = command.value; }
- else if (command.type == 'help') { obj.sessions.help = command.value; }
- else if (command.type == 'tcp') { obj.sessions.tcp = command.value; }
- else if (command.type == 'udp') { obj.sessions.udp = command.value; }
- else if (command.type == 'msg') { obj.sessions.msg = command.value; }
- else if (command.type == 'app') { obj.sessions.app = command.value; }
- }
-
- // Any "help" session must have an associated app, if not, remove it.
- if (obj.sessions.help != null) {
- for (var i in obj.sessions.help) { if (obj.sessions.help[i] == null) { delete obj.sessions.help[i]; } }
- if (Object.keys(obj.sessions.help).length == 0) { delete obj.sessions.help; }
- }
-
- // Inform everyone of updated sessions
- obj.updateSessions();
- break;
- }
- case 'battery': {
- // Device battery and power state
- if (obj.sessions == null) { obj.sessions = {}; }
- if (obj.sessions.battery == null) { obj.sessions.battery = {}; }
- if ((command.state == 'ac') || (command.state == 'dc')) { obj.sessions.battery.state = command.state; } else { delete obj.sessions.battery.state; }
- if ((typeof command.level == 'number') && (command.level >= 0) && (command.level <= 100)) { obj.sessions.battery.level = command.level; } else { delete obj.sessions.battery.level; }
- obj.updateSessions();
- break;
- }
- case 'getcoredump': {
- // Check if we requested a core dump file in the last minute, if so, ignore this.
- if ((parent.lastCoreDumpRequest != null) && ((Date.now() - parent.lastCoreDumpRequest) < 60000)) break;
-
- // Indicates if the agent has a coredump available
- if ((command.exists === true) && (typeof command.agenthashhex == 'string') && (command.agenthashhex.length == 96)) {
- // Check if we already have this exact dump file
- const coreDumpFile = parent.path.join(parent.parent.datapath, '..', 'meshcentral-coredumps', obj.agentInfo.agentId + '-' + command.agenthashhex + '-' + obj.nodeid + '.dmp');
- parent.fs.stat(coreDumpFile, function (err, stats) {
- if (stats != null) return;
- obj.coreDumpPresent = true;
-
- // Check how many files are in the coredumps folder
- const coreDumpPath = parent.path.join(parent.parent.datapath, '..', 'meshcentral-coredumps');
- parent.fs.readdir(coreDumpPath, function (err, files) {
- if ((files != null) && (files.length >= 20)) return; // Don't get more than 20 core dump files.
-
- // Get the core dump uploaded to the server.
- parent.lastCoreDumpRequest = Date.now();
- obj.RequestCoreDump(command.agenthashhex, command.corehashhex);
- });
- });
- }
- break;
- }
- case 'tunnelCloseStats': {
- // TODO: This this extra stats from the tunnel, you can merge this into the tunnel event in the database.
- //console.log(command);
-
- // Validate input
- if ((command.sent == null) || (typeof command.sent != 'string')) return;
- if ((command.sentActual == null) || (typeof command.sentActual != 'string')) return;
- if ((command.sentActual == null) || (typeof command.sentActual != 'number')) return;
-
- // Event the session closed compression data.
- var event = { etype: 'node', action: 'sessioncompression', nodeid: obj.dbNodeKey, domain: domain.id, sent: parseInt(command.sent), sentActual: parseInt(command.sentActual), msgid: 54, msgArgs: [command.sentRatio, parseInt(command.sent), parseInt(command.sentActual)], msg: 'Agent closed session with ' + command.sentRatio + '% agent to server compression. Sent: ' + command.sent + ', Compressed: ' + command.sentActual + '.' };
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, event);
- break;
- }
- case 'lmsinfo': {
- // Agents send the LMS port bindings
- // Example: {"action":"lmsinfo","value":{"ports":["623","16992"]}}
- break;
- }
- case 'plugin': {
- if ((parent.parent.pluginHandler == null) || (typeof command.plugin != 'string')) break;
- try {
- parent.parent.pluginHandler.plugins[command.plugin].serveraction(command, obj, parent);
- } catch (e) {
- parent.parent.debug('agent', 'Error loading plugin handler (' + e + ')');
- console.log('Error loading plugin handler (' + e + ')');
- }
- break;
- }
- case 'meshToolInfo': {
- // Return information about a MeshCentral tool. Current tools are 'MeshCentralRouter' and 'MeshCentralAssistant'
- // Information includes file hash and download location URL
- if (typeof command.name != 'string') break;
- var info = parent.parent.meshToolsBinaries[command.name];
- if ((command.hash != null) && (info.hash == command.hash)) return;
-
- // To build the connection URL, if we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
- var xdomain = (domain.dns == null) ? domain.id : '';
- if (xdomain != '') xdomain += '/';
-
- // Build the response
- const responseCmd = { action: 'meshToolInfo', name: command.name, tag: command.tag, sessionid: command.sessionid, hash: info.hash, size: info.size, url: info.url };
- if ((command.name == 'MeshCentralAssistant') && (command.msh == true)) { responseCmd.url = '*/' + xdomain + 'meshagents?id=10006'; } // If this is Assistant and the MSH needs to be included in the executable, change the URL.
- if (command.cookie === true) { responseCmd.url += ('&auth=' + parent.parent.encodeCookie({ download: info.dlname }, parent.parent.loginCookieEncryptionKey)); }
- if (command.pipe === true) { responseCmd.pipe = true; }
- if (parent.webCertificateHashs[domain.id] != null) { responseCmd.serverhash = Buffer.from(parent.webCertificateHashs[domain.id], 'binary').toString('hex'); }
- try { ws.send(JSON.stringify(responseCmd)); } catch (ex) { }
- break;
- }
- case 'agentupdate': {
- if ((obj.agentExeInfo != null) && (typeof obj.agentExeInfo.url == 'string')) {
- var func = function agentUpdateFunc(argument, taskid, taskLimiterQueue) { // Medium priority task
- // If agent disconnection, complete and exit now.
- if (obj.authenticated != 2) { parent.parent.taskLimiter.completed(taskid); return; }
-
- // Agent is requesting an agent update
- obj.agentCoreUpdateTaskId = taskid;
- const url = '*' + require('url').parse(obj.agentExeInfo.url).path;
- var cmd = { action: 'agentupdate', url: url, hash: obj.agentExeInfo.hashhex, sessionid: agentUpdateFunc.sessionid };
- parent.parent.debug('agentupdate', "Sending user requested agent update url: " + cmd.url);
-
- // Add the hash
- if (obj.agentExeInfo.fileHash != null) { cmd.hash = obj.agentExeInfo.fileHashHex; } else { cmd.hash = obj.agentExeInfo.hashhex; }
-
- // Add server TLS cert hash
- if (isIgnoreHashCheck() == false) {
- const tlsCertHash = parent.webCertificateFullHashs[domain.id];
- if (tlsCertHash != null) { cmd.servertlshash = Buffer.from(tlsCertHash, 'binary').toString('hex'); }
- }
-
- // Send the agent update command
- obj.send(JSON.stringify(cmd));
- }
- func.sessionid = command.sessionid;
-
- // Agent update. The recovery core was loaded in the agent, send a command to update the agent
- parent.parent.taskLimiter.launch(func, null, 1);
- }
- break;
- }
- case 'agentupdatedownloaded': {
- if (obj.agentCoreUpdateTaskId != null) {
- // Indicate this udpate task is complete
- parent.parent.taskLimiter.completed(obj.agentCoreUpdateTaskId);
- delete obj.agentCoreUpdateTaskId;
- }
- break;
- }
- case 'errorlog': { // This is the agent error log
- if ((!Array.isArray(command.log)) || (command.log.length == 0) || (parent.parent.agentErrorLog == null)) break;
- var lastLogEntry = command.log[command.log.length - 1];
- if ((lastLogEntry != null) && (typeof lastLogEntry == 'object') && (typeof lastLogEntry.t == 'number')) {
- parent.fs.write(parent.parent.agentErrorLog, obj.dbNodeKey + ', ' + Date.now() + ', ' + str + '\r\n', function (err) { });
- db.Set({ _id: 'al' + obj.dbNodeKey, lastEvent: lastLogEntry.t });
- }
- break;
- }
- case '2faauth': {
- // Validate input
- if ((typeof command.url != 'string') || (typeof command.approved != 'boolean') || (command.url.startsWith('2fa://') == false)) return;
-
- // parse the URL
- var url = null;
- try { url = require('url').parse(command.url); } catch (ex) { }
- if (url == null) return;
-
- // Decode the cookie
- var urlSplit = url.query.split('&c=');
- if (urlSplit.length != 2) return;
- const authCookie = parent.parent.decodeCookie(urlSplit[1], null, 1);
- if ((authCookie == null) || (typeof authCookie.c != 'string') || (('code=' + authCookie.c) != urlSplit[0])) return;
- if ((typeof authCookie.n != 'string') || (authCookie.n != obj.dbNodeKey) || (typeof authCookie.u != 'string')) return;
-
- // Fetch the user
- const user = parent.users[authCookie.u];
- if (user == null) return;
-
- // Add this device as the authentication push notification device for this user
- if (authCookie.a == 'addAuth') {
- // Do nothing if authentication is not approved.
- // We do not want to indicate that the remote user responded to this.
- if (command.approved !== true) return;
-
- // Change the user
- user.otpdev = obj.dbNodeKey;
- parent.db.SetUser(user);
-
- // Notify change
- var targets = ['*', 'server-users', user._id];
- if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
- var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: 113, msg: "Added push notification authentication device", domain: domain.id };
- if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
- parent.parent.DispatchEvent(targets, obj, event);
- }
-
- // Complete 2FA checking
- if (authCookie.a == 'checkAuth') {
- if (typeof authCookie.s != 'string') return;
- // Notify 2FA response
- parent.parent.DispatchEvent(['2fadev-' + authCookie.s], obj, { etype: '2fadev', action: '2faresponse', domain: domain.id, nodeid: obj.dbNodeKey, code: authCookie.a, userid: user._id, approved: command.approved, sessionid: authCookie.s, nolog: 1 });
- }
-
- break;
- }
- case 'getUserImage': {
- // Validate input
- if (typeof command.userid != 'string') {
- // Send back the default image if required
- if ((command.default) || (command.sentDefault)) {
- try { command.image = 'data:image/png;base64,' + Buffer.from(parent.fs.readFileSync(parent.parent.path.join(__dirname, 'public', 'images', 'user-128.png')), 'binary').toString('base64'); } catch (ex) { }
- obj.send(JSON.stringify(command));
- }
- return;
- }
- var useridsplit = command.userid.split('/');
- if ((useridsplit.length != 3) || (useridsplit[1] != domain.id)) return;
-
- // Add the user's real name if present
- var u = parent.users[command.userid];
- if (u == null) return;
- if (u.name) { command.name = u.name; }
- if (u.realname) { command.realname = u.realname; }
-
- // An agent can only request images of accounts with rights to the device.
- if (parent.GetNodeRights(command.userid, obj.dbMeshKey, obj.dbNodeKey) != 0) {
- parent.db.Get('im' + command.userid, function (err, images) {
- if ((err == null) && (images != null) && (images.length == 1)) {
- // Send back the account image
- command.image = images[0].image;
- } else {
- // Send back the default image if required
- if ((command.default) || (command.sentDefault)) {
- try { command.image = 'data:image/png;base64,' + Buffer.from(parent.fs.readFileSync(parent.parent.path.join(__dirname, 'public', 'images', 'user-128.png')), 'binary').toString('base64'); } catch (ex) { }
- }
- }
- obj.send(JSON.stringify(command));
- });
- }
- break;
- }
- case 'getServerImage': {
- if (command.agent === 'assistant') {
- // Return server title and image for MeshCentral Assistant
- if ((domain.assistantcustomization != null) && (typeof domain.assistantcustomization == 'object')) {
- var ok = false;
- if (typeof domain.assistantcustomization.title == 'string') { ok = true; command.title = domain.assistantcustomization.title; }
- if (typeof domain.assistantcustomization.image == 'string') { try { command.image = 'data:image/jpeg;base64,' + Buffer.from(parent.fs.readFileSync(parent.parent.getConfigFilePath(domain.assistantcustomization.image)), 'binary').toString('base64'); ok = true; } catch (ex) { console.log(ex); } }
- if (ok) { obj.send(JSON.stringify(command)); }
- }
- }
- if (command.agent === 'android') {
- // Return server title and image for MeshCentral Assistant
- if ((domain.androidcustomization != null) && (typeof domain.androidcustomization == 'object')) {
- var ok = false;
- if (typeof domain.androidcustomization.title == 'string') { ok = true; command.title = domain.androidcustomization.title; }
- if (typeof domain.androidcustomization.subtitle == 'string') { ok = true; command.subtitle = domain.androidcustomization.subtitle; }
- if (typeof domain.androidcustomization.image == 'string') { try { command.image = 'data:image/jpeg;base64,' + Buffer.from(parent.fs.readFileSync(parent.parent.getConfigFilePath(domain.androidcustomization.image)), 'binary').toString('base64'); ok = true; } catch (ex) { console.log(ex); } }
- if (ok) { obj.send(JSON.stringify(command)); }
- }
- }
- break;
- }
- case 'guestShare': {
- if ((command.flags == null) || (command.flags == 0)) {
- // Stop any current self-share, this is allowed even if self guest sharing is not allows so to clear any old shares.
- removeGuestSharing(function () {
- delete obj.guestSharing;
- obj.send(JSON.stringify({ action: 'guestShare', flags: command.flags, url: null, viewOnly: false }));
- });
- } else {
- // Add a new self-share, this will replace any share for this device
- if ((domain.agentselfguestsharing == null) || (domain.agentselfguestsharing == false) || (typeof command.flags != 'number')) return; // Check if agent self-sharing is allowed, this is off by default.
- if ((command.flags & 2) == 0) { command.viewOnly = false; } // Only allow "view only" if desktop is shared.
- addGuestSharing(command.flags, command.viewOnly, function (share) {
- obj.guestSharing = true;
- obj.send(JSON.stringify({ action: 'guestShare', url: share.url, flags: share.flags, viewOnly: share.viewOnly }));
- })
- }
- break;
- }
- case 'amtconfig': {
- // Sent by the agent when the agent needs a Intel AMT APF connection to the server
- const cookie = parent.parent.encodeCookie({ a: 'apf', n: obj.dbNodeKey, m: obj.dbMeshKey }, parent.parent.loginCookieEncryptionKey);
- try { obj.send(JSON.stringify({ action: 'amtconfig', user: '**MeshAgentApfTunnel**', pass: cookie })); } catch (ex) { }
- break;
- }
- case 'script-task': {
- // These command are for running regular batch jobs on the remote device
- if (parent.parent.taskManager != null) { parent.parent.taskManager.agentAction(command, obj); }
- break;
- }
- default: {
- parent.agentStats.unknownAgentActionCount++;
- parent.parent.debug('agent', 'Unknown agent action (' + obj.remoteaddrport + '): ' + JSON.stringify(command) + '.');
- console.log('Unknown agent action (' + obj.remoteaddrport + '): ' + JSON.stringify(command) + '.');
- break;
- }
- }
- if (parent.parent.pluginHandler != null) {
- parent.parent.pluginHandler.callHook('hook_processAgentData', command, obj, parent);
- }
- }
- }
-
- function addGuestSharing(flags, viewOnly, func) {
- // Create cookie
- const publicid = 'AS:' + obj.dbNodeKey;
- const extrakey = getRandomAmtPassword();
- const cookie = { a: 6, pid: publicid, k: extrakey }; // New style sharing cookie
- const inviteCookie = parent.parent.encodeCookie(cookie, parent.parent.invitationLinkEncryptionKey);
- if (inviteCookie == null) return;
-
- // Create the server url
- var serverName = parent.getWebServerName(domain, req);
- var httpsPort = ((args.aliasport == null) ? args.port : args.aliasport); // Use HTTPS alias port is specified
- var xdomain = (domain.dns == null) ? domain.id : '';
- if (xdomain != '') xdomain += '/';
- var url = 'https://' + serverName + ':' + httpsPort + '/' + xdomain + 'sharing?c=' + inviteCookie;
- if (serverName.split('.') == 1) { url = '/' + xdomain + page + '?c=' + inviteCookie; }
-
- // Create a device sharing database entry
- var shareEntry = { _id: 'deviceshare-' + publicid, type: 'deviceshare', nodeid: obj.dbNodeKey, p: flags, domain: domain.id, publicid: publicid, guestName: 'Agent', consent: 0x7F, url: url, extrakey: extrakey };
-
- // Add expire time
- if ((typeof domain.agentselfguestsharing == 'object') && (typeof domain.agentselfguestsharing.expire == 'number') && (domain.agentselfguestsharing.expire > 0)) {
- shareEntry.startTime = Date.now();
- shareEntry.expireTime = Date.now() + (60000 * domain.agentselfguestsharing.expire);
- }
-
- if (viewOnly === true) { shareEntry.viewOnly = true; }
- parent.db.Set(shareEntry);
-
- // Send out an event that we added a device share
- var targets = parent.CreateNodeDispatchTargets(obj.dbMeshKey, obj.dbNodeKey);
- var event = { etype: 'node', nodeid: obj.dbNodeKey, action: 'addedDeviceShare', msg: 'Added device share with unlimited time', msgid: 131, msgArgs: ['Agent'], domain: domain.id };
- parent.parent.DispatchEvent(targets, obj, event);
-
- // Send device share update
- parent.db.GetAllTypeNodeFiltered([obj.dbNodeKey], domain.id, 'deviceshare', null, function (err, docs) {
- if (err != null) return;
-
- // Check device sharing
- var now = Date.now();
- for (var i = 0; i < docs.length; i++) {
- const doc = docs[i];
- if (doc.expireTime < now) { parent.db.Remove(doc._id, function () { }); delete docs[i]; } else {
- // This share is ok, remove extra data we don't need to send.
- delete doc._id; delete doc.domain; delete doc.nodeid; delete doc.type;
- }
- }
-
- // Send device share update
- var targets = parent.CreateNodeDispatchTargets(obj.dbMeshKey, obj.dbNodeKey, []);
- parent.parent.DispatchEvent(targets, obj, { etype: 'node', nodeid: obj.dbNodeKey, action: 'deviceShareUpdate', domain: domain.id, deviceShares: docs, nolog: 1 });
-
- // Callback
- if (func) { func({ url: url, flags: flags, viewOnly: viewOnly }); }
- });
- }
-
- function removeGuestSharing(func) {
- var publicid = 'AS:' + obj.dbNodeKey;
- parent.db.GetAllTypeNodeFiltered([obj.dbNodeKey], domain.id, 'deviceshare', null, function (err, docs) {
- if (err != null) return;
-
- // Remove device sharing
- var now = Date.now(), removedExact = null, removed = false, okDocs = [];
- for (var i = 0; i < docs.length; i++) {
- const doc = docs[i];
- if (doc.publicid == publicid) { parent.db.Remove(doc._id, function () { }); removedExact = doc; removed = true; }
- else if (doc.expireTime < now) { parent.db.Remove(doc._id, function () { }); removed = true; } else {
- // This share is ok, remove extra data we don't need to send.
- delete doc._id; delete doc.domain; delete doc.nodeid; delete doc.type;
- okDocs.push(doc);
- }
- }
-
- // Event device share removal
- if (removedExact != null) {
- // Send out an event that we removed a device share
- var targets = parent.CreateNodeDispatchTargets(obj.dbMeshKey, obj.dbNodeKey, ['server-shareremove']);
- var event = { etype: 'node', nodeid: obj.dbNodeKey, action: 'removedDeviceShare', msg: 'Removed Device Share', msgid: 102, msgArgs: ['Agent'], domain: domain.id, publicid: publicid };
- parent.parent.DispatchEvent(targets, obj, event);
- }
-
- // If we removed any shares, send device share update
- if (removed == true) {
- var targets = parent.CreateNodeDispatchTargets(obj.dbMeshKey, obj.dbNodeKey, []);
- parent.parent.DispatchEvent(targets, obj, { etype: 'node', nodeid: obj.dbNodeKey, action: 'deviceShareUpdate', domain: domain.id, deviceShares: okDocs, nolog: 1 });
- }
-
- // Call back when done
- if (func) func(removed);
- });
- }
-
- // Notify update of sessions
- obj.updateSessions = function () {
- // Perform some clean up
- for (var i in obj.sessions) { if (Object.keys(obj.sessions[i]).length == 0) { delete obj.sessions[i]; } }
- if (Object.keys(obj.sessions).length == 0) { delete obj.sessions; }
-
- // Event the new sessions, this will notify everyone that agent sessions have changed
- var event = { etype: 'node', action: 'devicesessions', nodeid: obj.dbNodeKey, domain: domain.id, sessions: obj.sessions, nolog: 1 };
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, event);
- }
-
- // Change the current core information string and event it
- function ChangeAgentCoreInfo(command) {
- if ((obj.agentInfo == null) || (obj.agentInfo.capabilities & 0x40)) return;
- if ((command == null) || (command == null)) return; // Safety, should never happen.
-
- // If the device is pending a change, hold.
- if (obj.deviceChanging === true) { setTimeout(function () { ChangeAgentCoreInfo(command); }, 100); return; }
- obj.deviceChanging = true;
-
- // Check that the mesh exists
- const mesh = parent.meshes[obj.dbMeshKey];
- if (mesh == null) { delete obj.deviceChanging; return; }
-
- // Get the node and change it if needed
- db.Get(obj.dbNodeKey, function (err, nodes) { // TODO: THIS IS A BIG RACE CONDITION HERE, WE NEED TO FIX THAT. If this call is made twice at the same time on the same device, data will be missed.
- if ((nodes == null) || (nodes.length != 1)) { delete obj.deviceChanging; return; }
- const device = nodes[0];
- if (device.agent) {
- var changes = [], change = 0, log = 0;
-
- // Check if anything changes
- if (command.name && (typeof command.name == 'string') && (command.name != device.name)) { change = 1; log = 1; device.name = command.name; changes.push('name'); }
- if ((command.caps != null) && (device.agent.core != command.value)) { if ((command.value == null) && (device.agent.core != null)) { delete device.agent.core; } else { device.agent.core = command.value; } change = 1; } // Don't save this as an event to the db.
- if ((command.caps != null) && ((device.agent.caps & 0xFFFFFFE7) != (command.caps & 0xFFFFFFE7))) { device.agent.caps = ((device.agent.caps & 24) + (command.caps & 0xFFFFFFE7)); change = 1; } // Allow Javascript on the agent to change all capabilities except console and javascript support, Don't save this as an event to the db.
- if ((command.osdesc != null) && (typeof command.osdesc == 'string') && (device.osdesc != command.osdesc)) { device.osdesc = command.osdesc; change = 1; changes.push('os desc'); } // Don't save this as an event to the db.
- if ((typeof command.root == 'boolean') && (command.root !== device.agent.root)) { change = 1; device.agent.root = command.root; }
- if (device.ip != obj.remoteaddr) { device.ip = obj.remoteaddr; change = 1; }
- if (command.intelamt) {
- if (!device.intelamt) { device.intelamt = {}; }
- if ((command.intelamt.Versions != null) && (typeof command.intelamt.Versions == 'object')) {
- if ((command.intelamt.Versions.AMT != null) && (typeof command.intelamt.Versions.AMT == 'string') && (command.intelamt.Versions.AMT.length < 12) && (device.intelamt.ver != command.intelamt.Versions.AMT)) { changes.push('AMT version'); device.intelamt.ver = command.intelamt.Versions.AMT; change = 1; log = 1; }
- if ((command.intelamt.Versions.Sku != null) && (typeof command.intelamt.Versions.Sku == 'string')) {
- const sku = parseInt(command.intelamt.Versions.Sku);
- if (device.intelamt.sku !== sku) { device.intelamt.sku = sku; change = 1; log = 1; }
- }
- }
- if ((command.intelamt.ProvisioningState != null) && (typeof command.intelamt.ProvisioningState == 'number') && (device.intelamt.state != command.intelamt.ProvisioningState)) { changes.push('AMT state'); device.intelamt.state = command.intelamt.ProvisioningState; change = 1; log = 1; }
- if ((command.intelamt.Flags != null) && (typeof command.intelamt.Flags == 'number') && (device.intelamt.flags != command.intelamt.Flags)) {
- if (device.intelamt.flags) { changes.push('AMT flags (' + device.intelamt.flags + ' --> ' + command.intelamt.Flags + ')'); } else { changes.push('AMT flags (' + command.intelamt.Flags + ')'); }
- device.intelamt.flags = command.intelamt.Flags; change = 1; log = 1;
- }
- if ((command.intelamt.UUID != null) && (typeof command.intelamt.UUID == 'string') && (device.intelamt.uuid != command.intelamt.UUID)) { changes.push('AMT uuid'); device.intelamt.uuid = command.intelamt.UUID; change = 1; log = 1; }
- }
- if (command.av != null) { // Antivirus
- if (!device.av) { device.av = []; }
- if (JSON.stringify(device.av) != JSON.stringify(command.av)) { /*changes.push('AV status');*/ device.av = command.av; change = 1; log = 1; }
- }
- if (command.wsc != null) { // Windows Security Center
- if (!device.wsc) { device.wsc = {}; }
- if (JSON.stringify(device.wsc) != JSON.stringify(command.wsc)) { /*changes.push('Windows Security Center status');*/ device.wsc = command.wsc; change = 1; log = 1; }
- }
- if (command.defender != null) { // Defender For Windows Server
- if (!device.defender) { device.defender = {}; }
- if (JSON.stringify(device.defender) != JSON.stringify(command.defender)) { /*changes.push('Defender status');*/ device.defender = command.defender; change = 1; log = 1; }
- }
- if (command.lastbootuptime != null) { // Last Boot Up Time
- if (!device.lastbootuptime) { device.lastbootuptime = ""; }
- if (device.lastbootuptime != command.lastbootuptime) { /*changes.push('Last Boot Up Time');*/ device.lastbootuptime = command.lastbootuptime; change = 1; log = 1; }
- }
-
- // Push Messaging Token
- if ((command.pmt != null) && (typeof command.pmt == 'string') && (device.pmt != command.pmt)) {
- if (typeof device.pmt == 'string') { db.Remove('pmt_' + device.pmt); }
- device.pmt = command.pmt;
- change = 1; // Don't save this change as an event to the db, so no log=1.
- parent.removePmtFromAllOtherNodes(device); // We need to make sure to remove this push messaging token from any other device on this server, all domains included.
- }
-
- if ((command.users != null) && (Array.isArray(command.users)) && (device.users != command.users)) { device.users = command.users; change = 1; } // Don't save this to the db.
- if ((command.lusers != null) && (Array.isArray(command.lusers)) && (device.lusers != command.lusers)) { device.lusers = command.lusers; change = 1; } // Don't save this to the db.
- if ((mesh.mtype == 2) && (!args.wanonly)) {
- // In WAN mode, the hostname of a computer is not important. Don't log hostname changes.
- if (device.host != obj.remoteaddr) { device.host = obj.remoteaddr; change = 1; changes.push('host'); }
- // TODO: Check that the agent has an interface that is the same as the one we got this websocket connection on. Only set if we have a match.
- }
-
- // Remove old volumes and BitLocker data, this is part of sysinfo.
- delete device.volumes;
-
- // If there are changes, event the new device
- if (change == 1) {
- // Do some clean up if needed, these values should not be in the database.
- if (device.conn != null) { delete device.conn; }
- if (device.pwr != null) { delete device.pwr; }
- if (device.agct != null) { delete device.agct; }
- if (device.cict != null) { delete device.cict; }
-
- // Save to the database
- db.Set(device);
-
- // Event the node change
- var event = { etype: 'node', action: 'changenode', nodeid: obj.dbNodeKey, domain: domain.id, node: parent.CloneSafeNode(device) };
- if (changes.length > 0) { event.msg = 'Changed device ' + device.name + ' from group ' + mesh.name + ': ' + changes.join(', '); }
- if ((log == 0) || ((obj.agentInfo) && (obj.agentInfo.capabilities) && (obj.agentInfo.capabilities & 0x20)) || (changes.length == 0)) { event.nolog = 1; } // If this is a temporary device, don't log changes
- if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(device.meshid, [obj.dbNodeKey]), obj, event);
- }
-
- // Device change is done.
- delete obj.deviceChanging;
- }
- });
- }
-
- // Change the current core information string and event it
- function ChangeAgentLocationInfo(command) {
- if (obj.agentInfo.capabilities & 0x40) return;
- if ((command == null) || (command == null)) { return; } // Safety, should never happen.
-
- // Check that the mesh exists
- const mesh = parent.meshes[obj.dbMeshKey];
- if (mesh == null) return;
-
- // If the device is pending a change, hold.
- if (obj.deviceChanging === true) { setTimeout(function () { ChangeAgentLocationInfo(command); }, 100); return; }
- obj.deviceChanging = true;
-
- // Get the node and change it if needed
- db.Get(obj.dbNodeKey, function (err, nodes) {
- if ((nodes == null) || (nodes.length != 1)) { delete obj.deviceChanging; return; }
- const device = nodes[0];
- if (device.agent) {
- var changes = [], change = 0;
-
- // Check if anything changes
- if ((command.publicip) && (device.publicip != command.publicip)) { device.publicip = command.publicip; change = 1; changes.push('public ip'); }
- if ((command.iploc) && (device.iploc != command.iploc)) { device.iploc = command.iploc; change = 1; changes.push('ip location'); }
-
- // If there are changes, save and event
- if (change == 1) {
- // Do some clean up if needed, these values should not be in the database.
- if (device.conn != null) { delete device.conn; }
- if (device.pwr != null) { delete device.pwr; }
- if (device.agct != null) { delete device.agct; }
- if (device.cict != null) { delete device.cict; }
-
- // Save the device
- db.Set(device);
-
- // Event the node change
- var event = { etype: 'node', action: 'changenode', nodeid: obj.dbNodeKey, domain: domain.id, node: parent.CloneSafeNode(device), msgid: 59, msgArgs: [device.name, mesh.name, changes.join(', ')], msg: 'Changed device ' + device.name + ' from group ' + mesh.name + ': ' + changes.join(', ') };
- if (obj.agentInfo.capabilities & 0x20) { event.nolog = 1; } // If this is a temporary device, don't log changes
- if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(device.meshid, [obj.dbNodeKey]), obj, event);
- }
- }
-
- // Done changing the device
- delete obj.deviceChanging;
- });
- }
-
- // Update the mesh agent tab in the database
- function ChangeAgentTag(tag) {
- if ((obj.agentInfo == null) || (obj.agentInfo.capabilities & 0x40)) return;
- if ((tag != null) && (tag.length == 0)) { tag = null; }
-
- // If the device is pending a change, hold.
- if (obj.deviceChanging === true) {
- var func = function ChangeAgentTagFunc() { ChangeAgentCoreInfo(ChangeAgentTagFunc.tag); }
- func.tag = tag;
- setTimeout(func, 100);
- return;
- }
- obj.deviceChanging = true;
-
- // Get the node and change it if needed
- db.Get(obj.dbNodeKey, function (err, nodes) {
- if ((nodes == null) || (nodes.length != 1)) { delete obj.deviceChanging; return; }
- const device = nodes[0];
- if (device.agent) {
- // Parse the agent tag
- var agentTag = null, serverName = null, serverDesc = null, serverTags = null;
- if (tag != null) {
- var taglines = tag.split('\r\n').join('\n').split('\r').join('\n').split('\n');
- for (var i in taglines) {
- var tagline = taglines[i].trim();
- if (tagline.length > 0) {
- if (tagline.startsWith('~')) {
- if (tagline.startsWith('~ServerName:') && (tagline.length > 12) && (serverName == null)) { serverName = tagline.substring(12).trim(); }
- if (tagline.startsWith('~ServerDesc:') && (tagline.length > 12) && (serverDesc == null)) { serverDesc = tagline.substring(12).trim(); }
- if (tagline.startsWith('~ServerTags:') && (tagline.length > 12) && (serverTags == null)) { serverTags = tagline.substring(12).split(','); for (var j in serverTags) { serverTags[j] = serverTags[j].trim(); } }
- } else { if (agentTag == null) { agentTag = tagline; } }
- }
- }
- }
-
- // Set the agent tag
- var changes = false;
- if (device.agent.tag != agentTag) { device.agent.tag = agentTag; if ((device.agent.tag == null) || (device.agent.tag == '')) { delete device.agent.tag; } changes = true; }
- if (domain.agenttag != null) {
- // Set the device's server name
- if ((serverName != null) && (domain.agenttag.servername === 1) && (device.name != serverName)) { device.name = serverName; changes = true; }
-
- // Set the device's server description
- if ((serverDesc != null) && (domain.agenttag.serverdesc === 1) && (device.desc != serverDesc)) { device.desc = serverDesc; changes = true; }
-
- // Set the device's server description if there is no description
- if ((serverDesc != null) && (domain.agenttag.serverdesc === 2) && (device.desc != serverDesc) && ((device.desc == null) || (device.desc == ''))) { device.desc = serverDesc; changes = true; }
-
- if ((serverTags != null) && (domain.agenttag.servertags != null) && (domain.agenttag.servertags != 0)) {
- // Sort the tags
- serverTags.sort();
-
- // Stringify the tags
- var st2 = '', st1 = serverTags.join(',');
- if (device.tags != null) { st2 = device.tags.join(','); }
-
- // Set the device's server tags
- if ((domain.agenttag.servertags === 1) && (st1 != st2)) { device.tags = serverTags; changes = true; }
-
- // Set the device's server tags if there are not tags
- if ((domain.agenttag.servertags === 2) && (st2 == '')) { device.tags = serverTags; changes = true; }
-
- // Append to device's server tags
- if ((domain.agenttag.servertags === 3) && (st1 != st2)) {
- if (device.tags == null) { device.tags = []; }
- for (var i in serverTags) { if (device.tags.indexOf(serverTags[i]) == -1) { device.tags.push(serverTags[i]); } }
- device.tags.sort();
- changes = true;
- }
- }
- }
-
- if (changes == true) {
- // Do some clean up if needed, these values should not be in the database.
- if (device.conn != null) { delete device.conn; }
- if (device.pwr != null) { delete device.pwr; }
- if (device.agct != null) { delete device.agct; }
- if (device.cict != null) { delete device.cict; }
-
- // Update the device
- db.Set(device);
-
- // Event the node change
- var event = { etype: 'node', action: 'changenode', nodeid: obj.dbNodeKey, domain: domain.id, node: parent.CloneSafeNode(device), nolog: 1 };
- if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
- parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(device.meshid, [obj.dbNodeKey]), obj, event);
- }
- }
-
- // Done changing the device
- delete obj.deviceChanging;
- });
- }
-
- // Check if we need to update this agent, return true if agent binary update required.
- // Return 0 is no update needed, 1 update using native system, 2 update using meshcore system
- function compareAgentBinaryHash(agentExeInfo, agentHash) {
- // If this is a temporary agent and the server is set to not update temporary agents, don't update the agent.
- if ((obj.agentInfo.capabilities & 0x20) && (args.temporaryagentupdate === false)) return 0;
- // If we are testing the agent update system, always return true
- if ((args.agentupdatetest === true) || (args.agentupdatetest === 1)) return 1;
- if (args.agentupdatetest === 2) return 2;
- // If the hash matches or is null, no update required.
- if ((agentExeInfo.hash == agentHash) || (agentHash == '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0')) return 0;
- // If this is a macOS x86 or ARM agent type and it matched the universal binary, no update required.
- if ((agentExeInfo.id == 16) || (agentExeInfo.id == 29)) {
- if (domain.meshAgentBinaries && domain.meshAgentBinaries[10005]) {
- if (domain.meshAgentBinaries[10005].hash == agentHash) return 0;
- } else {
- if (parent.parent.meshAgentBinaries[10005].hash == agentHash) return 0;
- }
- }
-
- // No match, update the agent.
- if (args.agentupdatesystem === 2) return 2; // If set, force a meshcore update.
- if (agentExeInfo.id == 3) return 2; // Due to a bug in Windows 7 SP1 environement variable exec, we always update 32bit Windows agent using MeshCore for now. Upcoming agent will have a fix for this.
- // NOTE: Windows agents with no commit dates may have bad native update system, so use meshcore system instead.
- // NOTE: Windows agents with commit date prior to 1612740413000 did not kill all "meshagent.exe" processes and update could fail as a result executable being locked, meshcore system will do this.
- if (((obj.AgentCommitDate == null) || (obj.AgentCommitDate < 1612740413000)) && ((agentExeInfo.id == 3) || (agentExeInfo.id == 4))) return 2; // For older Windows agents, use the meshcore update technique.
- return 1; // By default, use the native update technique.
- }
-
- // Request that the core dump file on this agent be uploaded to the server
- obj.RequestCoreDump = function (agenthashhex, corehashhex) {
- if (agenthashhex.length > 16) { agenthashhex = agenthashhex.substring(0, 16); }
- const cookie = parent.parent.encodeCookie({ a: 'aft', b: 'coredump', c: obj.agentInfo.agentId + '-' + agenthashhex + '-' + obj.nodeid + '.dmp' }, parent.parent.loginCookieEncryptionKey);
- obj.send('{"action":"msg","type":"tunnel","value":"*/' + (((domain.dns == null) && (domain.id != '')) ? (domain.id + '/') : '') + 'agenttransfer.ashx?c=' + cookie + '","rights":"4294967295"}');
- }
-
- // Return true if we need to ignore the agent hash check
- function isIgnoreHashCheck() {
- if ((args.ignoreagenthashcheck === true) || (domain.ignoreagenthashcheck === true)) return true;
-
- // Check site wide exceptions
- if (Array.isArray(args.ignoreagenthashcheck)) {
- for (var i = 0; i < args.ignoreagenthashcheck.length; i++) {
- if (require('ipcheck').match(obj.remoteaddr, args.ignoreagenthashcheck[i])) return true;
- }
- }
-
- // Check domain wide exceptions
- if (Array.isArray(domain.ignoreagenthashcheck)) {
- for (var i = 0; i < domain.ignoreagenthashcheck.length; i++) {
- if (require('ipcheck').match(obj.remoteaddr, domain.ignoreagenthashcheck[i])) return true;
- }
- }
-
- return false;
- }
-
- // Generate a random Intel AMT password
- function checkAmtPassword(p) { return (p.length > 7) && (/\d/.test(p)) && (/[a-z]/.test(p)) && (/[A-Z]/.test(p)) && (/\W/.test(p)); }
- function getRandomAmtPassword() { var p; do { p = Buffer.from(parent.crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); } while (checkAmtPassword(p) == false); return p; }
-
- return obj;
-};
+/**
+* @description MeshCentral MeshAgent communication module
+* @author Ylian Saint-Hilaire & Bryan Roe
+* @copyright Intel Corporation 2018-2022
+* @license Apache-2.0
+* @version v0.0.1
+*/
+
+/*xjslint node: true */
+/*xjslint plusplus: true */
+/*xjslint maxlen: 256 */
+/*jshint node: true */
+/*jshint strict: false */
+/*jshint esversion: 6 */
+"use strict";
+
+// Construct a MeshAgent object, called upon connection
+module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
+ const forge = parent.parent.certificateOperations.forge;
+ const common = parent.parent.common;
+ parent.agentStats.createMeshAgentCount++;
+ parent.parent.debug('agent', 'New agent at ' + req.clientIp + ':' + ws._socket.remotePort);
+
+ var obj = {};
+ obj.domain = domain;
+ obj.authenticated = 0;
+ obj.receivedCommands = 0;
+ obj.agentCoreCheck = 0;
+ obj.remoteaddr = req.clientIp;
+ obj.remoteaddrport = obj.remoteaddr + ':' + ws._socket.remotePort;
+ obj.nonce = parent.crypto.randomBytes(48).toString('binary');
+ //ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive, 4 minutes
+ if (args.agentidletimeout != 0) { ws._socket.setTimeout(args.agentidletimeout, function () { obj.close(1); }); } // Inactivity timeout of 2:30 minutes, by default agent will WebSocket ping every 2 minutes and server will pong back.
+ //obj.nodeid = null;
+ //obj.meshid = null;
+ //obj.dbNodeKey = null;
+ //obj.dbMeshKey = null;
+ //obj.connectTime = null;
+ //obj.agentInfo = null;
+
+ ws._socket.bytesReadEx = 0;
+ ws._socket.bytesWrittenEx = 0;
+
+ // Perform data accounting
+ function dataAccounting() {
+ parent.trafficStats.AgentCtrlIn += (ws._socket.bytesRead - ws._socket.bytesReadEx);
+ parent.trafficStats.AgentCtrlOut += (ws._socket.bytesWritten - ws._socket.bytesWrittenEx);
+ ws._socket.bytesReadEx = ws._socket.bytesRead;
+ ws._socket.bytesWrittenEx = ws._socket.bytesWritten;
+ }
+
+ // Send a message to the mesh agent
+ obj.send = function (data, func) { try { if (typeof data == 'string') { ws.send(Buffer.from(data), func); } else { ws.send(data, func); } } catch (e) { } };
+ obj.sendBinary = function (data, func) { try { if (typeof data == 'string') { ws.send(Buffer.from(data, 'binary'), func); } else { ws.send(data, func); } } catch (e) { } };
+
+ // Disconnect this agent
+ obj.close = function (arg) {
+ dataAccounting();
+
+ if ((arg == 1) || (arg == null)) { try { ws.close(); if (obj.nodeid != null) { parent.parent.debug('agent', 'Soft disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ')'); } } catch (e) { console.log(e); } } // Soft close, close the websocket
+ if (arg == 2) {
+ try {
+ if (ws._socket._parent != null)
+ ws._socket._parent.end();
+ else
+ ws._socket.end();
+
+ if (obj.nodeid != null) {
+ parent.parent.debug('agent', 'Hard disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ')');
+ }
+ } catch (e) { console.log(e); }
+ }
+ // If arg == 2, hard close, close the TCP socket
+ // If arg == 3, don't communicate with this agent anymore, but don't disconnect (Duplicate agent).
+
+ // Stop any current self-share
+ if (obj.guestSharing === true) { removeGuestSharing(); }
+
+ // Remove this agent from the webserver list
+ if (parent.wsagents[obj.dbNodeKey] == obj) {
+ delete parent.wsagents[obj.dbNodeKey];
+ parent.parent.ClearConnectivityState(obj.dbMeshKey, obj.dbNodeKey, 1, null, { remoteaddrport: obj.remoteaddrport, name: obj.name });
+ }
+
+ // Remove this agent from the list of agents with bad web certificates
+ if (obj.badWebCert) { delete parent.wsagentsWithBadWebCerts[obj.badWebCert]; }
+
+ // Get the current mesh
+ const mesh = parent.meshes[obj.dbMeshKey];
+
+ // If this is a temporary or recovery agent, or all devices in this group are temporary, remove the agent (0x20 = Temporary, 0x40 = Recovery)
+ if (((obj.agentInfo) && (obj.agentInfo.capabilities) && ((obj.agentInfo.capabilities & 0x20) || (obj.agentInfo.capabilities & 0x40))) || ((mesh) && (mesh.flags) && (mesh.flags & 1))) {
+ // Delete this node including network interface information and events
+ db.Remove(obj.dbNodeKey); // Remove node with that id
+ db.Remove('if' + obj.dbNodeKey); // Remove interface information
+ db.Remove('nt' + obj.dbNodeKey); // Remove notes
+ db.Remove('lc' + obj.dbNodeKey); // Remove last connect time
+ db.Remove('si' + obj.dbNodeKey); // Remove system information
+ db.Remove('al' + obj.dbNodeKey); // Remove error log last time
+ if (db.RemoveSMBIOS) { db.RemoveSMBIOS(obj.dbNodeKey); } // Remove SMBios data
+ db.RemoveAllNodeEvents(obj.dbNodeKey); // Remove all events for this node
+ db.removeAllPowerEventsForNode(obj.dbNodeKey); // Remove all power events for this node
+
+ // Event node deletion
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, { etype: 'node', action: 'removenode', nodeid: obj.dbNodeKey, domain: domain.id, nolog: 1 });
+
+ // Disconnect all connections if needed
+ const state = parent.parent.GetConnectivityState(obj.dbNodeKey);
+ if ((state != null) && (state.connectivity != null)) {
+ if ((state.connectivity & 1) != 0) { parent.wsagents[obj.dbNodeKey].close(); } // Disconnect mesh agent
+ if ((state.connectivity & 2) != 0) { parent.parent.mpsserver.closeAllForNode(obj.dbNodeKey); } // Disconnect CIRA connection
+ }
+ }
+
+ // Set this agent as no longer authenticated
+ obj.authenticated = -1;
+
+ // If we where updating the agent using native method, clean that up.
+ if (obj.agentUpdate != null) {
+ if (obj.agentUpdate.fd) { try { parent.fs.close(obj.agentUpdate.fd); } catch (ex) { } }
+ parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete
+ delete obj.agentUpdate.buf;
+ delete obj.agentUpdate;
+ }
+
+ // If we where updating the agent meshcore method, clean that up.
+ if (obj.agentCoreUpdateTaskId != null) {
+ parent.parent.taskLimiter.completed(obj.agentCoreUpdateTaskId);
+ delete obj.agentCoreUpdateTaskId;
+ }
+
+ // Perform timer cleanup
+ if (obj.pingtimer) { clearInterval(obj.pingtimer); delete obj.pingtimer; }
+ if (obj.pongtimer) { clearInterval(obj.pongtimer); delete obj.pongtimer; }
+
+ // Perform aggressive cleanup
+ delete obj.name;
+ delete obj.nonce;
+ delete obj.nodeid;
+ delete obj.unauth;
+ delete obj.remoteaddr;
+ delete obj.remoteaddrport;
+ delete obj.meshid;
+ delete obj.connectTime;
+ delete obj.agentInfo;
+ delete obj.agentExeInfo;
+ ws.removeAllListeners(['message', 'close', 'error']);
+ };
+
+ // When data is received from the mesh agent web socket
+ ws.on('message', function (msg) {
+ dataAccounting();
+ if (msg.length < 2) return;
+ if (typeof msg == 'object') { msg = msg.toString('binary'); } // TODO: Could change this entire method to use Buffer instead of binary string
+ if (obj.authenticated == 2) { // We are authenticated
+ if ((obj.agentUpdate == null) && (msg.charCodeAt(0) == 123)) { processAgentData(msg); } // Only process JSON messages if meshagent update is not in progress
+ if (msg.length < 2) return;
+ const cmdid = common.ReadShort(msg, 0);
+ if (cmdid == 11) { // MeshCommand_CoreModuleHash
+ if (msg.length == 4) { ChangeAgentCoreInfo({ 'caps': 0 }); } // If the agent indicated that no core is running, clear the core information string.
+ // Mesh core hash, sent by agent with the hash of the current mesh core.
+
+ // If we are performing an agent update, don't update the core.
+ if (obj.agentUpdate != null) { return; }
+
+ // If we are using a custom core, don't try to update it.
+ if (obj.agentCoreCheck == 1000) {
+ obj.sendBinary(common.ShortToStr(16) + common.ShortToStr(0)); // MeshCommand_CoreOk. Indicates to the agent that the core is ok. Start it if it's not already started.
+ agentCoreIsStable();
+ return;
+ }
+
+ // Get the current meshcore hash
+ const agentMeshCoreHash = (msg.length == 52) ? msg.substring(4, 52) : null;
+
+ // If the agent indicates this is a custom core, we are done.
+ if ((agentMeshCoreHash != null) && (agentMeshCoreHash == '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0')) {
+ obj.agentCoreCheck = 0;
+ obj.sendBinary(common.ShortToStr(16) + common.ShortToStr(0)); // MeshCommand_CoreOk. Indicates to the agent that the core is ok. Start it if it's not already started.
+ agentCoreIsStable();
+ return;
+ }
+
+ // We need to check if the core is current. Figure out what core we need.
+ var corename = null;
+ if ((obj.agentInfo != null) && (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId] != null)) {
+ if ((obj.agentCoreCheck == 1001) || (obj.agentCoreUpdate == true)) {
+ // If the user asked, use the recovery core.
+ corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].rcore;
+ } else if (obj.agentCoreCheck == 1011) {
+ // If the user asked, use the tiny core.
+ corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].tcore;
+ } else if (obj.agentInfo.capabilities & 0x40) {
+ // If this is a recovery agent, use the agent recovery core.
+ corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].arcore;
+ } else {
+ // This is the normal core for this agent type.
+ corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].core;
+ }
+ }
+
+ // If we have a core, use it.
+ if (corename != null) {
+ const meshcorehash = parent.parent.defaultMeshCoresHash[corename];
+ if (agentMeshCoreHash != meshcorehash) {
+ if ((obj.agentCoreCheck < 5) || (obj.agentCoreCheck == 1001) || (obj.agentCoreCheck == 1011) || (obj.agentCoreUpdate == true)) {
+ if (meshcorehash == null) {
+ // Clear the core
+ obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0)); // MeshCommand_CoreModule, ask mesh agent to clear the core
+ parent.agentStats.clearingCoreCount++;
+ parent.parent.debug('agent', "Clearing core");
+ } else {
+ // Setup task limiter options, this system limits how many tasks can run at the same time to spread the server load.
+ var taskLimiterOptions = { hash: meshcorehash, core: parent.parent.defaultMeshCores[corename], name: corename };
+
+ // If the agent supports compression, sent the core compressed.
+ if ((obj.agentInfo.capabilities & 0x100) && (parent.parent.defaultMeshCoresDeflate[corename])) {
+ args.core = parent.parent.defaultMeshCoresDeflate[corename];
+ }
+
+ // Update new core with task limiting so not to flood the server. This is a high priority task.
+ obj.agentCoreUpdatePending = true;
+ parent.parent.taskLimiter.launch(function (argument, taskid, taskLimiterQueue) {
+ if (obj.authenticated == 2) {
+ // Send the updated core.
+ delete obj.agentCoreUpdatePending;
+ obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0) + argument.hash + argument.core.toString('binary'), function () { parent.parent.taskLimiter.completed(taskid); }); // MeshCommand_CoreModule, start core update
+ parent.agentStats.updatingCoreCount++;
+ parent.parent.debug('agent', "Updating core " + argument.name);
+ } else {
+ // This agent is probably disconnected, nothing to do.
+ parent.parent.taskLimiter.completed(taskid);
+ }
+ }, taskLimiterOptions, 0);
+ }
+ obj.agentCoreCheck++;
+ }
+ } else {
+ obj.agentCoreCheck = 0;
+ obj.sendBinary(common.ShortToStr(16) + common.ShortToStr(0)); // MeshCommand_CoreOk. Indicates to the agent that the core is ok. Start it if it's not already started.
+ agentCoreIsStable(); // No updates needed, agent is ready to go.
+ }
+ }
+
+ /*
+ // TODO: Check if we have a mesh specific core. If so, use that.
+ var agentMeshCoreHash = null;
+ if (msg.length == 52) { agentMeshCoreHash = msg.substring(4, 52); }
+ if ((agentMeshCoreHash != parent.parent.defaultMeshCoreHash) && (agentMeshCoreHash != parent.parent.defaultMeshCoreNoMeiHash)) {
+ if (obj.agentCoreCheck < 5) { // This check is in place to avoid a looping core update.
+ if (parent.parent.defaultMeshCoreHash == null) {
+ // Update no core
+ obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0)); // Command 10, ask mesh agent to clear the core
+ } else {
+ // Update new core
+ if ((parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId] != null) && (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].amt == true)) {
+ obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0) + parent.parent.defaultMeshCoreHash + parent.parent.defaultMeshCore); // Command 10, ask mesh agent to set the core (with MEI support)
+ } else {
+ obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0) + parent.parent.defaultMeshCoreNoMeiHash + parent.parent.defaultMeshCoreNoMei); // Command 10, ask mesh agent to set the core (No MEI)
+ }
+ }
+ obj.agentCoreCheck++;
+ }
+ } else {
+ obj.agentCoreCheck = 0;
+ }
+ */
+ }
+ else if (cmdid == 12) { // MeshCommand_AgentHash
+ if ((msg.length == 52) && (obj.agentExeInfo != null) && (obj.agentExeInfo.update == true)) {
+ const agenthash = msg.substring(4);
+ const agentUpdateMethod = compareAgentBinaryHash(obj.agentExeInfo, agenthash);
+ if (agentUpdateMethod === 2) { // Use meshcore agent update system
+ // Send the recovery core to the agent, if the agent is capable of running one
+ if (((obj.agentInfo.capabilities & 16) != 0) && (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].core != null)) {
+ parent.agentStats.agentMeshCoreBinaryUpdate++;
+ obj.agentCoreUpdate = true;
+ obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0)); // Ask to clear the core
+ obj.sendBinary(common.ShortToStr(11) + common.ShortToStr(0)); // Ask for meshcore hash
+ }
+ } else if (agentUpdateMethod === 1) { // Use native agent update system
+ // Mesh agent update required, do it using task limiter so not to flood the network. Medium priority task.
+ parent.parent.taskLimiter.launch(function (argument, taskid, taskLimiterQueue) {
+ if (obj.authenticated != 2) { parent.parent.taskLimiter.completed(taskid); return; } // If agent disconnection, complete and exit now.
+ if (obj.nodeid != null) { parent.parent.debug('agent', "Agent update required, NodeID=0x" + obj.nodeid.substring(0, 16) + ', ' + obj.agentExeInfo.desc); }
+ parent.agentStats.agentBinaryUpdate++;
+ if ((obj.agentExeInfo.data == null) && (((obj.agentInfo.capabilities & 0x100) == 0) || (obj.agentExeInfo.zdata == null))) {
+ // Read the agent from disk
+ parent.fs.open(obj.agentExeInfo.path, 'r', function (err, fd) {
+ if (obj.agentExeInfo == null) return; // Agent disconnected during this call.
+ if (err) { parent.parent.debug('agentupdate', "ERROR: " + err); return console.error(err); }
+ obj.agentUpdate = { ptr: 0, buf: Buffer.alloc(parent.parent.agentUpdateBlockSize + 4), fd: fd, taskid: taskid };
+
+ // MeshCommand_CoreModule, ask mesh agent to clear the core.
+ // The new core will only be sent after the agent updates.
+ obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0));
+
+ // We got the agent file open on the server side, tell the agent we are sending an update ending with the SHA384 hash of the result
+ //console.log("Agent update file open.");
+ obj.sendBinary(common.ShortToStr(13) + common.ShortToStr(0)); // Command 13, start mesh agent download
+
+ // Send the first mesh agent update data block
+ obj.agentUpdate.buf[0] = 0;
+ obj.agentUpdate.buf[1] = 14;
+ obj.agentUpdate.buf[2] = 0;
+ obj.agentUpdate.buf[3] = 1;
+ parent.fs.read(obj.agentUpdate.fd, obj.agentUpdate.buf, 4, parent.parent.agentUpdateBlockSize, obj.agentUpdate.ptr, function (err, bytesRead, buffer) {
+ if (obj.agentUpdate == null) return;
+ if ((err != null) || (bytesRead == 0)) {
+ // Error reading the agent file, stop here.
+ try { parent.fs.close(obj.agentUpdate.fd); } catch (ex) { }
+ parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete
+ parent.parent.debug('agentupdate', "ERROR: Unable to read first block of agent binary from disk.");
+ delete obj.agentUpdate.buf;
+ delete obj.agentUpdate;
+ } else {
+ // Send the first block to the agent
+ obj.agentUpdate.ptr += bytesRead;
+ parent.parent.debug('agentupdate', "Sent first block of " + bytesRead + " bytes from disk.");
+ obj.sendBinary(obj.agentUpdate.buf); // Command 14, mesh agent first data block
+ }
+ });
+ });
+ } else {
+ // Send the agent from RAM
+ obj.agentUpdate = { ptr: 0, buf: Buffer.alloc(parent.parent.agentUpdateBlockSize + 4), taskid: taskid };
+
+ // MeshCommand_CoreModule, ask mesh agent to clear the core.
+ // The new core will only be sent after the agent updates.
+ obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0));
+
+ // We got the agent file open on the server side, tell the agent we are sending an update ending with the SHA384 hash of the result
+ obj.sendBinary(common.ShortToStr(13) + common.ShortToStr(0)); // Command 13, start mesh agent download
+
+ // Send the first mesh agent update data block
+ obj.agentUpdate.buf[0] = 0;
+ obj.agentUpdate.buf[1] = 14;
+ obj.agentUpdate.buf[2] = 0;
+ obj.agentUpdate.buf[3] = 1;
+
+ // If agent supports compression, send the compressed agent if possible.
+ if ((obj.agentInfo.capabilities & 0x100) && (obj.agentExeInfo.zdata != null)) {
+ // Send compressed data
+ obj.agentUpdate.agentUpdateData = obj.agentExeInfo.zdata;
+ obj.agentUpdate.agentUpdateHash = obj.agentExeInfo.zhash;
+ } else {
+ // Send uncompressed data
+ obj.agentUpdate.agentUpdateData = obj.agentExeInfo.data;
+ obj.agentUpdate.agentUpdateHash = obj.agentExeInfo.hash;
+ }
+
+ const len = Math.min(parent.parent.agentUpdateBlockSize, obj.agentUpdate.agentUpdateData.length - obj.agentUpdate.ptr);
+ if (len > 0) {
+ // Send the first block
+ obj.agentUpdate.agentUpdateData.copy(obj.agentUpdate.buf, 4, obj.agentUpdate.ptr, obj.agentUpdate.ptr + len);
+ obj.agentUpdate.ptr += len;
+ obj.sendBinary(obj.agentUpdate.buf); // Command 14, mesh agent first data block
+ parent.parent.debug('agentupdate', "Sent first block of " + len + " bytes from RAM.");
+ } else {
+ // Error
+ parent.parent.debug('agentupdate', "ERROR: Len of " + len + " is invalid.");
+ parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete
+ delete obj.agentUpdate.buf;
+ delete obj.agentUpdate;
+ }
+ }
+ }, null, 1);
+
+ } else {
+ // Check the mesh core, if the agent is capable of running one
+ if (((obj.agentInfo.capabilities & 16) != 0) && (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].core != null)) {
+ obj.sendBinary(common.ShortToStr(11) + common.ShortToStr(0)); // Command 11, ask for mesh core hash.
+ }
+ }
+ }
+ }
+ else if (cmdid == 14) { // MeshCommand_AgentBinaryBlock
+ if ((msg.length == 4) && (obj.agentUpdate != null)) {
+ const status = common.ReadShort(msg, 2);
+ if (status == 1) {
+ if (obj.agentExeInfo.data == null) {
+ // Read the agent from disk
+ parent.fs.read(obj.agentUpdate.fd, obj.agentUpdate.buf, 4, parent.parent.agentUpdateBlockSize, obj.agentUpdate.ptr, function (err, bytesRead, buffer) {
+ if ((obj.agentExeInfo == null) || (obj.agentUpdate == null)) return; // Agent disconnected during this async call.
+ if ((err != null) || (bytesRead < 0)) {
+ // Error reading the agent file, stop here.
+ parent.parent.debug('agentupdate', "ERROR: Unable to read agent #" + obj.agentExeInfo.id + " binary from disk.");
+ try { parent.fs.close(obj.agentUpdate.fd); } catch (ex) { }
+ parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete
+ delete obj.agentUpdate.buf;
+ delete obj.agentUpdate;
+ } else {
+ // Send the next block to the agent
+ parent.parent.debug('agentupdate', "Sending disk agent #" + obj.agentExeInfo.id + " block, ptr=" + obj.agentUpdate.ptr + ", len=" + bytesRead + ".");
+ obj.agentUpdate.ptr += bytesRead;
+ if (bytesRead == parent.parent.agentUpdateBlockSize) { obj.sendBinary(obj.agentUpdate.buf); } else { obj.sendBinary(obj.agentUpdate.buf.slice(0, bytesRead + 4)); } // Command 14, mesh agent next data block
+ if ((bytesRead < parent.parent.agentUpdateBlockSize) || (obj.agentUpdate.ptr == obj.agentExeInfo.size)) {
+ parent.parent.debug('agentupdate', "Completed agent #" + obj.agentExeInfo.id + " update from disk, ptr=" + obj.agentUpdate.ptr + ".");
+ obj.sendBinary(common.ShortToStr(13) + common.ShortToStr(0) + obj.agentExeInfo.hash); // Command 13, end mesh agent download, send agent SHA384 hash
+ try { parent.fs.close(obj.agentUpdate.fd); } catch (ex) { }
+ parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete
+ delete obj.agentUpdate.buf;
+ delete obj.agentUpdate;
+ }
+ }
+ });
+ } else {
+ // Send the agent from RAM
+ const len = Math.min(parent.parent.agentUpdateBlockSize, obj.agentUpdate.agentUpdateData.length - obj.agentUpdate.ptr);
+ if (len > 0) {
+ obj.agentUpdate.agentUpdateData.copy(obj.agentUpdate.buf, 4, obj.agentUpdate.ptr, obj.agentUpdate.ptr + len);
+ if (len == parent.parent.agentUpdateBlockSize) { obj.sendBinary(obj.agentUpdate.buf); } else { obj.sendBinary(obj.agentUpdate.buf.slice(0, len + 4)); } // Command 14, mesh agent next data block
+ parent.parent.debug('agentupdate', "Sending RAM agent #" + obj.agentExeInfo.id + " block, ptr=" + obj.agentUpdate.ptr + ", len=" + len + ".");
+ obj.agentUpdate.ptr += len;
+ }
+
+ if (obj.agentUpdate.ptr == obj.agentUpdate.agentUpdateData.length) {
+ parent.parent.debug('agentupdate', "Completed agent #" + obj.agentExeInfo.id + " update from RAM, ptr=" + obj.agentUpdate.ptr + ".");
+ obj.sendBinary(common.ShortToStr(13) + common.ShortToStr(0) + obj.agentUpdate.agentUpdateHash); // Command 13, end mesh agent download, send agent SHA384 hash
+ parent.parent.taskLimiter.completed(obj.agentUpdate.taskid); // Indicate this task complete
+ delete obj.agentUpdate.buf;
+ delete obj.agentUpdate;
+ }
+ }
+ }
+ }
+ }
+ else if (cmdid == 15) { // MeshCommand_AgentTag
+ var tag = msg.substring(2);
+ while (tag.charCodeAt(tag.length - 1) == 0) { tag = tag.substring(0, tag.length - 1); } // Remove end-of-line zeros.
+ ChangeAgentTag(tag);
+ }
+ } else if (obj.authenticated < 2) { // We are not authenticated
+ // Check if this is a un-authenticated JSON
+ if (msg.charCodeAt(0) == 123) {
+ var str = msg.toString('utf8'), command = null;
+ if (str[0] == '{') {
+ try { command = JSON.parse(str); } catch (ex) { } // If the command can't be parsed, ignore it.
+ if ((command != null) && (command.action === 'agentName') && (typeof command.value == 'string') && (command.value.length > 0) && (command.value.length < 256)) { obj.agentName = command.value; }
+ }
+ return;
+ }
+ const cmd = common.ReadShort(msg, 0);
+ if (cmd == 1) {
+ // Agent authentication request
+ if ((msg.length != 98) || ((obj.receivedCommands & 1) != 0)) return;
+ obj.receivedCommands += 1; // Agent can't send the same command twice on the same connection ever. Block DOS attack path.
+
+ if (isIgnoreHashCheck()) {
+ // Send the agent web hash back to the agent
+ // Send 384 bits SHA384 hash of TLS cert + 384 bits nonce
+ obj.sendBinary(common.ShortToStr(1) + msg.substring(2, 50) + obj.nonce); // Command 1, hash + nonce. Use the web hash given by the agent.
+ } else {
+ // Check that the server hash matches our own web certificate hash (SHA384)
+ obj.agentSeenCerthash = msg.substring(2, 50);
+ if ((getWebCertHash(domain) != obj.agentSeenCerthash) && (getWebCertFullHash(domain) != obj.agentSeenCerthash) && (parent.defaultWebCertificateHash != obj.agentSeenCerthash) && (parent.defaultWebCertificateFullHash != obj.agentSeenCerthash)) {
+ if (parent.parent.supportsProxyCertificatesRequest !== false) {
+ obj.badWebCert = Buffer.from(parent.crypto.randomBytes(16), 'binary').toString('base64');
+ parent.wsagentsWithBadWebCerts[obj.badWebCert] = obj; // Add this agent to the list of of agents with bad web certificates.
+ parent.parent.updateProxyCertificates(false);
+ }
+ parent.agentStats.agentBadWebCertHashCount++;
+ parent.setAgentIssue(obj, "BadWebCertHash: " + Buffer.from(msg.substring(2, 50), 'binary').toString('hex'));
+ parent.parent.debug('agent', 'Agent bad web cert hash (Agent:' + (Buffer.from(msg.substring(2, 50), 'binary').toString('hex').substring(0, 10)) + ' != Server:' + (Buffer.from(getWebCertHash(domain), 'binary').toString('hex').substring(0, 10)) + ' or ' + (Buffer.from(getWebCertFullHash(domain), 'binary').toString('hex').substring(0, 10)) + '), holding connection (' + obj.remoteaddrport + ').');
+ parent.parent.debug('agent', 'Agent reported web cert hash:' + (Buffer.from(msg.substring(2, 50), 'binary').toString('hex')) + '.');
+ console.log('Agent bad web cert hash (Agent:' + (Buffer.from(msg.substring(2, 50), 'binary').toString('hex').substring(0, 10)) + ' != Server:' + (Buffer.from(getWebCertHash(domain), 'binary').toString('hex').substring(0, 10)) + ' or ' + (Buffer.from(getWebCertFullHash(domain), 'binary').toString('hex').substring(0, 10)) + '), holding connection (' + obj.remoteaddrport + ').');
+ console.log('Agent reported web cert hash:' + (Buffer.from(msg.substring(2, 50), 'binary').toString('hex')) + '.');
+ delete obj.agentSeenCerthash;
+ return;
+ } else {
+ // The hash matched one of the acceptable values, send the agent web hash back to the agent
+ // Send 384 bits SHA384 hash of TLS cert + 384 bits nonce
+ // Command 1, hash + nonce. Use the web hash given by the agent.
+ obj.sendBinary(common.ShortToStr(1) + obj.agentSeenCerthash + obj.nonce);
+ }
+ }
+
+ // Use our server private key to sign the ServerHash + AgentNonce + ServerNonce
+ obj.agentnonce = msg.substring(50, 98);
+
+ // Check if we got the agent auth confirmation
+ if ((obj.receivedCommands & 8) == 0) {
+ // If we did not get an indication that the agent already validated this server, send the server signature.
+ if (obj.useSwarmCert == true) {
+ // Perform the hash signature using older swarm server certificate
+ parent.parent.certificateOperations.acceleratorPerformSignature(1, msg.substring(2) + obj.nonce, null, function (tag, signature) {
+ // Send back our certificate + signature
+ obj.sendBinary(common.ShortToStr(2) + common.ShortToStr(parent.swarmCertificateAsn1.length) + parent.swarmCertificateAsn1 + signature); // Command 2, certificate + signature
+ });
+ } else {
+ // Perform the hash signature using the server agent certificate
+ parent.parent.certificateOperations.acceleratorPerformSignature(0, msg.substring(2) + obj.nonce, null, function (tag, signature) {
+ // Send back our certificate + signature
+ obj.sendBinary(common.ShortToStr(2) + common.ShortToStr(parent.agentCertificateAsn1.length) + parent.agentCertificateAsn1 + signature); // Command 2, certificate + signature
+ });
+ }
+ }
+
+ // Check the agent signature if we can
+ if (obj.unauthsign != null) {
+ if (processAgentSignature(obj.unauthsign) == false) {
+ parent.agentStats.agentBadSignature1Count++;
+ parent.setAgentIssue(obj, "BadSignature1");
+ parent.parent.debug('agent', 'Agent connected with bad signature, holding connection (' + obj.remoteaddrport + ').');
+ console.log('Agent connected with bad signature, holding connection (' + obj.remoteaddrport + ').'); return;
+ } else { completeAgentConnection(); }
+ }
+ }
+ else if (cmd == 2) {
+ // Agent certificate
+ if ((msg.length < 4) || ((obj.receivedCommands & 2) != 0)) return;
+ obj.receivedCommands += 2; // Agent can't send the same command twice on the same connection ever. Block DOS attack path.
+
+ // Decode the certificate
+ const certlen = common.ReadShort(msg, 2);
+ obj.unauth = {};
+ try { obj.unauth.nodeid = Buffer.from(forge.pki.getPublicKeyFingerprint(forge.pki.certificateFromAsn1(forge.asn1.fromDer(msg.substring(4, 4 + certlen))).publicKey, { md: forge.md.sha384.create() }).data, 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); } catch (ex) { console.log(ex); parent.parent.debug('agent', ex); return; }
+ obj.unauth.nodeCertPem = '-----BEGIN CERTIFICATE-----\r\n' + Buffer.from(msg.substring(4, 4 + certlen), 'binary').toString('base64') + '\r\n-----END CERTIFICATE-----';
+
+ // Check the agent signature if we can
+ if (obj.agentnonce == null) { obj.unauthsign = msg.substring(4 + certlen); } else {
+ if (processAgentSignature(msg.substring(4 + certlen)) == false) {
+ parent.agentStats.agentBadSignature2Count++;
+ parent.setAgentIssue(obj, "BadSignature2");
+ parent.parent.debug('agent', 'Agent connected with bad signature, holding connection (' + obj.remoteaddrport + ').');
+ console.log('Agent connected with bad signature, holding connection (' + obj.remoteaddrport + ').'); return;
+ }
+ }
+ completeAgentConnection();
+ }
+ else if (cmd == 3) {
+ // Agent meshid
+ if ((msg.length < 70) || ((obj.receivedCommands & 4) != 0)) return;
+ obj.receivedCommands += 4; // Agent can't send the same command twice on the same connection ever. Block DOS attack path.
+
+ // Set the meshid
+ obj.agentInfo = {};
+ obj.agentInfo.infoVersion = common.ReadInt(msg, 2);
+ obj.agentInfo.agentId = common.ReadInt(msg, 6);
+ obj.agentInfo.agentVersion = common.ReadInt(msg, 10);
+ obj.agentInfo.platformType = common.ReadInt(msg, 14);
+ if (obj.agentInfo.platformType > 8 || obj.agentInfo.platformType < 1) { obj.agentInfo.platformType = 1; }
+ if (msg.substring(50, 66) == '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0') {
+ obj.meshid = Buffer.from(msg.substring(18, 50), 'binary').toString('hex'); // Older HEX MeshID
+ } else {
+ obj.meshid = Buffer.from(msg.substring(18, 66), 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); // New Base64 MeshID
+ }
+ //console.log('MeshID', obj.meshid);
+ obj.agentInfo.capabilities = common.ReadInt(msg, 66);
+ if (msg.length > 70) {
+ const computerNameLen = common.ReadShort(msg, 70);
+ obj.agentInfo.computerName = Buffer.from(msg.substring(72, 72 + computerNameLen), 'binary').toString('utf8');
+ //console.log('computerName', msg.length, computerNameLen, obj.agentInfo.computerName);
+ } else {
+ obj.agentInfo.computerName = '';
+ //console.log('computerName-none');
+ }
+
+ obj.dbMeshKey = 'mesh/' + domain.id + '/' + obj.meshid;
+ completeAgentConnection();
+ } else if (cmd == 4) {
+ if ((msg.length < 2) || ((obj.receivedCommands & 8) != 0)) return;
+ obj.receivedCommands += 8; // Agent can't send the same command twice on the same connection ever. Block DOS attack path.
+ // Agent already authenticated the server, wants to skip the server signature - which is great for server performance.
+ } else if (cmd == 5) {
+ // ServerID. Agent is telling us what serverid it expects. Useful if we have many server certificates.
+ if ((msg.substring(2, 34) == parent.swarmCertificateHash256) || (msg.substring(2, 50) == parent.swarmCertificateHash384)) { obj.useSwarmCert = true; }
+ } else if (cmd == 30) {
+ // Agent Commit Date. This is future proofing. Can be used to change server behavior depending on the date range of the agent.
+ try { obj.AgentCommitDate = Date.parse(msg.substring(2)) } catch (ex) { }
+ //console.log('Connected Agent Commit Date: ' + msg.substring(2) + ", " + Date.parse(msg.substring(2)));
+ }
+ }
+ });
+
+ // If error, do nothing
+ ws.on('error', function (err) { parent.parent.debug('agent', 'AGENT WSERR: ' + err); console.log('AGENT WSERR: ' + err); obj.close(0); });
+
+ // If the mesh agent web socket is closed, clean up.
+ ws.on('close', function (req) {
+ parent.agentStats.agentClose++;
+ if (obj.nodeid != null) {
+ const agentId = (obj.agentInfo && obj.agentInfo.agentId) ? obj.agentInfo.agentId : 'Unknown';
+ //console.log('Agent disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ') id=' + agentId);
+ parent.parent.debug('agent', 'Agent disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ') id=' + agentId);
+
+ // Log the agent disconnection if we are not testing agent update
+ if (args.agentupdatetest == null) {
+ if (parent.wsagentsDisconnections[obj.nodeid] == null) {
+ parent.wsagentsDisconnections[obj.nodeid] = 1;
+ } else {
+ parent.wsagentsDisconnections[obj.nodeid] = ++parent.wsagentsDisconnections[obj.nodeid];
+ }
+ }
+ }
+ obj.close(0);
+ });
+
+ // Return the mesh for this device, in some cases, we may auto-create the mesh.
+ function getMeshAutoCreate() {
+ var mesh = parent.meshes[obj.dbMeshKey];
+
+ // If the mesh was not found and we are in LAN mode, check of the domain can be corrected
+ if ((args.lanonly == true) && (mesh == null)) {
+ var smesh = obj.dbMeshKey.split('/');
+ for (var i in parent.parent.config.domains) {
+ mesh = parent.meshes['mesh/' + i + '/' + smesh[2]];
+ if (mesh != null) {
+ obj.domain = domain = parent.parent.config.domains[i];
+ obj.meshid = smesh[2];
+ obj.dbMeshKey = 'mesh/' + i + '/' + smesh[2];
+ obj.dbNodeKey = 'node/' + domain.id + '/' + obj.nodeid;
+ break;
+ }
+ }
+ }
+
+ if ((mesh == null) && (typeof domain.orphanagentuser == 'string')) {
+ const adminUser = parent.users['user/' + domain.id + '/' + domain.orphanagentuser];
+ if ((adminUser != null) && (adminUser.siteadmin == 0xFFFFFFFF)) {
+ // Mesh name is hex instead of base64
+ const meshname = obj.meshid.substring(0, 18);
+
+ // Create a new mesh for this device
+ const links = {};
+ links[adminUser._id] = { name: adminUser.name, rights: 0xFFFFFFFF };
+ mesh = { type: 'mesh', _id: obj.dbMeshKey, name: meshname, mtype: 2, desc: '', domain: domain.id, links: links };
+ db.Set(mesh);
+ parent.meshes[obj.dbMeshKey] = mesh;
+
+ if (adminUser.links == null) adminUser.links = {};
+ adminUser.links[obj.dbMeshKey] = { rights: 0xFFFFFFFF };
+ db.SetUser(adminUser);
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [adminUser._id, obj.dbNodeKey]), obj, { etype: 'mesh', username: adminUser.name, meshid: obj.dbMeshKey, name: meshname, mtype: 2, desc: '', action: 'createmesh', links: links, msgid: 55, msgArgs: [obj.meshid], msg: "Created device group: " + obj.meshid, domain: domain.id });
+ }
+ } else {
+ if ((mesh != null) && (mesh.deleted != null) && (mesh.links)) {
+ // Must un-delete this mesh
+ var ids = parent.CreateMeshDispatchTargets(mesh._id, [obj.dbNodeKey]);
+
+ // See if users still exists, if so, add links to the mesh
+ for (var userid in mesh.links) {
+ const user = parent.users[userid];
+ if (user) {
+ if (user.links == null) { user.links = {}; }
+ if (user.links[mesh._id] == null) {
+ user.links[mesh._id] = { rights: mesh.links[userid].rights };
+ ids.push(user._id);
+ db.SetUser(user);
+ }
+ }
+ }
+
+ // Send out an event indicating this mesh was "created"
+ parent.parent.DispatchEvent(ids, obj, { etype: 'mesh', meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, action: 'createmesh', links: mesh.links, msgid: 56, msgArgs: [mesh._id], msg: "Device group undeleted: " + mesh._id, domain: domain.id });
+
+ // Mark the mesh as active
+ delete mesh.deleted;
+ db.Set(mesh);
+ }
+ }
+ return mesh;
+ }
+
+ // Send a PING/PONG message
+ function sendPing() { obj.send('{"action":"ping"}'); }
+ function sendPong() { obj.send('{"action":"pong"}'); }
+
+ // Once we get all the information about an agent, run this to hook everything up to the server
+ function completeAgentConnection() {
+ if ((obj.authenticated != 1) || (obj.meshid == null) || obj.pendingCompleteAgentConnection || (obj.agentInfo == null)) { return; }
+ obj.pendingCompleteAgentConnection = true;
+
+ // Setup the agent PING/PONG timers
+ if ((typeof args.agentping == 'number') && (obj.pingtimer == null)) { obj.pingtimer = setInterval(sendPing, args.agentping * 1000); }
+ else if ((typeof args.agentpong == 'number') && (obj.pongtimer == null)) { obj.pongtimer = setInterval(sendPong, args.agentpong * 1000); }
+
+ // If this is a recovery agent
+ if (obj.agentInfo.capabilities & 0x40) {
+ // Inform mesh agent that it's authenticated.
+ delete obj.pendingCompleteAgentConnection;
+ obj.authenticated = 2;
+ obj.sendBinary(common.ShortToStr(4));
+
+ // Ask for mesh core hash.
+ obj.sendBinary(common.ShortToStr(11) + common.ShortToStr(0));
+ return;
+ }
+
+ // Check if we have too many agent sessions
+ if (typeof domain.limits.maxagentsessions == 'number') {
+ // Count the number of agent sessions for this domain
+ var domainAgentSessionCount = 0;
+ for (var i in parent.wsagents) { if (parent.wsagents[i].domain.id == domain.id) { domainAgentSessionCount++; } }
+
+ // Check if we have too many user sessions
+ if (domainAgentSessionCount >= domain.limits.maxagentsessions) {
+ // Too many, hold the connection.
+ parent.agentStats.agentMaxSessionHoldCount++;
+ return;
+ }
+ }
+
+ /*
+ // Check that the mesh exists
+ var mesh = parent.meshes[obj.dbMeshKey];
+ if (mesh == null) {
+ var holdConnection = true;
+ if (typeof domain.orphanagentuser == 'string') {
+ var adminUser = parent.users['user/' + domain.id + '/' + args.orphanagentuser];
+ if ((adminUser != null) && (adminUser.siteadmin == 0xFFFFFFFF)) {
+ // Create a new mesh for this device
+ holdConnection = false;
+ var links = {};
+ links[user._id] = { name: adminUser.name, rights: 0xFFFFFFFF };
+ mesh = { type: 'mesh', _id: obj.dbMeshKey, name: obj.meshid, mtype: 2, desc: '', domain: domain.id, links: links };
+ db.Set(mesh);
+ parent.meshes[obj.meshid] = mesh;
+ parent.parent.AddEventDispatch(parent.CreateMeshDispatchTargets(obj.meshid, [obj.dbNodeKey]), ws);
+
+ if (adminUser.links == null) user.links = {};
+ adminUser.links[obj.meshid] = { rights: 0xFFFFFFFF };
+ //adminUser.subscriptions = parent.subscribe(adminUser._id, ws);
+ db.SetUser(user);
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(meshid, [user._id, obj.dbNodeKey]), obj, { etype: 'mesh', username: user.name, meshid: obj.meshid, name: obj.meshid, mtype: 2, desc: '', action: 'createmesh', links: links, msg: 'Mesh created: ' + obj.meshid, domain: domain.id });
+ }
+ }
+
+ if (holdConnection == true) {
+ // If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
+ parent.parent.debug('agent', 'Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
+ console.log('Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
+ return;
+ }
+ }
+ if (mesh.mtype != 2) { // If we disconnect, the agnet will just reconnect. We need to log this or tell agent to connect in a few hours.
+ parent.parent.debug('agent', 'Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
+ console.log('Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
+ return;
+ }
+ */
+
+ // Check that the node exists
+ db.Get(obj.dbNodeKey, async function (err, nodes) {
+ if (obj.agentInfo == null) { return; }
+ var device, mesh;
+ var nodeExists = Boolean(false);
+
+ // See if this node exists in the database
+ if ((nodes == null) || (nodes.length == 0)) {
+ if(domain.preventduplicatedevices){
+
+ const existingNodes = await new Promise((resolve, reject) => {
+ db.GetNodeByComputerName(domain.id, obj.agentInfo.computerName, (err, result) => {
+ if (err) reject(err);
+ else resolve(result);
+ });
+ });
+
+ if (!existingNodes || existingNodes.length === 0) {
+ // Device does not exist with the name
+ nodeExists = false;
+ } else {
+ console.log("Device already exists");
+ // Remove nodes with the same name
+ existingNodes.forEach((eNode) => {
+
+ parent.parent.debug('agent', 'Removing old dublicated node (' + eNode.rname + ', ' + eNode._id + ').');
+
+ db.Remove(eNode._id); // Remove node with that id
+ db.Remove('if' + eNode._id); // Remove interface information
+ db.Remove('nt' + eNode._id); // Remove notes
+ db.Remove('lc' + eNode._id); // Remove last connect time
+ db.Remove('si' + eNode._id); // Remove system information
+ db.Remove('al' + eNode._id); // Remove error log last time
+ if (db.RemoveSMBIOS) { db.RemoveSMBIOS(eNode._id); } // Remove SMBios data
+ db.RemoveAllNodeEvents(eNode._id); // Remove all events for this node
+ db.removeAllPowerEventsForNode(eNode._id); // Remove all power events for this node
+
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(eNode.meshid, [eNode._id]), obj, { etype: 'node', action: 'removenode', nodeid: eNode._id, domain: eNode.domain, nolog: 1 });
+ });
+
+ // Set mesh from previous node
+ obj.dbMeshKey = existingNodes[0].meshid
+
+ nodeExists = false;
+
+ }
+ } else {
+ nodeExists = false;
+ }
+
+ } else {
+ nodeExists = true;
+ }
+
+ if (nodeExists == false) {
+ // This device does not exist, use the meshid given by the device
+
+ // Check if we already have too many devices for this domain
+ if (domain.limits && (typeof domain.limits.maxdevices == 'number')) {
+ db.isMaxType(domain.limits.maxdevices, 'node', domain.id, function (ismax, count) {
+ if (ismax == true) {
+ // Too many devices in this domain.
+ parent.agentStats.maxDomainDevicesReached++;
+ } else {
+ // We are under the limit, create the new device.
+ completeAgentConnection2();
+ }
+ });
+ } else {
+ completeAgentConnection2();
+ }
+ return;
+ } else {
+ device = nodes[0];
+ obj.name = device.name;
+
+ // This device exists, meshid given by the device must be ignored, use the server side one.
+ if ((device.meshid != null) && (device.meshid != obj.dbMeshKey)) {
+ obj.dbMeshKey = device.meshid;
+ obj.meshid = device.meshid.split('/')[2];
+ }
+
+ // See if this mesh exists, if it does not we may want to create it.
+ mesh = getMeshAutoCreate();
+
+ // Check if the mesh exists
+ if (mesh == null) {
+ // If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
+ parent.agentStats.invalidDomainMesh2Count++;
+ parent.setAgentIssue(obj, "invalidDomainMesh2");
+ parent.parent.debug('agent', 'Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
+ console.log('Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
+ return;
+ }
+
+ // Check if the mesh is the right type
+ if (mesh.mtype != 2) {
+ // If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
+ parent.agentStats.invalidMeshType2Count++;
+ parent.setAgentIssue(obj, "invalidMeshType2");
+ parent.parent.debug('agent', 'Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
+ console.log('Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
+ return;
+ }
+
+ // Mark when this device connected
+ obj.connectTime = Date.now();
+
+ // Device already exists, look if changes have occured
+ var changes = [], change = 0, log = 0;
+ if (device.agent == null) { device.agent = { ver: obj.agentInfo.agentVersion, id: obj.agentInfo.agentId, caps: obj.agentInfo.capabilities }; change = 1; }
+ if (device.rname != obj.agentInfo.computerName) { device.rname = obj.agentInfo.computerName; change = 1; changes.push('computer name'); }
+ if (device.agent.ver != obj.agentInfo.agentVersion) { device.agent.ver = obj.agentInfo.agentVersion; change = 1; changes.push('agent version'); }
+ if (device.agent.id != obj.agentInfo.agentId) { device.agent.id = obj.agentInfo.agentId; change = 1; changes.push('agent type'); }
+ if ((device.agent.caps & 24) != (obj.agentInfo.capabilities & 24)) { device.agent.caps = obj.agentInfo.capabilities; change = 1; changes.push('agent capabilities'); } // If agent console or javascript support changes, update capabilities
+ // We want the server name to be sync'ed to the hostname or the --agentName
+ // (flag 16 allows to override the name until next connection)
+ if (mesh.flags && (mesh.flags & 2)) {
+ var preferredName = (mesh.flags & 8) && obj.agentName || obj.agentInfo.computerName;
+ if (device.name != preferredName) {device.name = preferredName; change = 1; }
+ }
+ if (device.ip != obj.remoteaddr) { device.ip = obj.remoteaddr; change = 1; }
+
+ if (change == 1) {
+ // Do some clean up if needed, these values should not be in the database.
+ if (device.conn != null) { delete device.conn; }
+ if (device.pwr != null) { delete device.pwr; }
+ if (device.agct != null) { delete device.agct; }
+ if (device.cict != null) { delete device.cict; }
+
+ // Save the updated device in the database
+ db.Set(device);
+
+ // If this is a temporary device, don't log changes
+ if (obj.agentInfo.capabilities & 0x20) { log = 0; }
+
+ // Event the node change
+ var event = { etype: 'node', action: 'changenode', nodeid: obj.dbNodeKey, domain: domain.id, node: parent.CloneSafeNode(device) };
+ if (log == 0) { event.nolog = 1; } else { event.msg = 'Changed device ' + device.name + ' from group ' + mesh.name + ': ' + changes.join(', '); }
+ if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(device.meshid, [obj.dbNodeKey]), obj, event);
+ }
+ }
+
+ completeAgentConnection3(device, mesh);
+ });
+ }
+
+ function completeAgentConnection2() {
+ // See if this mesh exists, if it does not we may want to create it.
+ var mesh = getMeshAutoCreate();
+
+ // Check if the mesh exists
+ if (mesh == null) {
+ // If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
+ parent.agentStats.invalidDomainMeshCount++;
+ parent.setAgentIssue(obj, "invalidDomainMesh");
+ parent.parent.debug('agent', 'Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
+ console.log('Agent connected with invalid domain/mesh, holding connection (' + obj.remoteaddrport + ', ' + obj.dbMeshKey + ').');
+ return;
+ }
+
+ // Check if the mesh is the right type
+ if (mesh.mtype != 2) {
+ // If we disconnect, the agent will just reconnect. We need to log this or tell agent to connect in a few hours.
+ parent.agentStats.invalidMeshTypeCount++;
+ parent.setAgentIssue(obj, "invalidMeshType");
+ parent.parent.debug('agent', 'Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
+ console.log('Agent connected with invalid mesh type, holding connection (' + obj.remoteaddrport + ').');
+ return;
+ }
+
+ // Mark when this device connected
+ obj.connectTime = Date.now();
+
+ // This node does not exist, create it.
+ var agentName = obj.agentName ? obj.agentName : obj.agentInfo.computerName;
+ var device = { type: 'node', mtype: mesh.mtype, _id: obj.dbNodeKey, icon: obj.agentInfo.platformType, meshid: obj.dbMeshKey, name: agentName, rname: obj.agentInfo.computerName, domain: domain.id, agent: { ver: obj.agentInfo.agentVersion, id: obj.agentInfo.agentId, caps: obj.agentInfo.capabilities }, host: null };
+ db.Set(device);
+
+ // Event the new node
+ if (obj.agentInfo.capabilities & 0x20) {
+ // This is a temporary agent, don't log.
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, { etype: 'node', action: 'addnode', node: device, domain: domain.id, nolog: 1 });
+ } else {
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, { etype: 'node', action: 'addnode', node: device, msgid: 57, msgArgs: [obj.agentInfo.computerName, mesh.name], msg: ('Added device ' + obj.agentInfo.computerName + ' to device group ' + mesh.name), domain: domain.id });
+ }
+
+ completeAgentConnection3(device, mesh);
+ }
+
+ function completeAgentConnection3(device, mesh) {
+ // Check if this agent is already connected
+ const dupAgent = parent.wsagents[obj.dbNodeKey];
+ parent.wsagents[obj.dbNodeKey] = obj;
+ if (dupAgent) {
+ // Record duplicate agents
+ if (parent.duplicateAgentsLog[obj.dbNodeKey] == null) {
+ if (dupAgent.remoteaddr == obj.remoteaddr) {
+ parent.duplicateAgentsLog[obj.dbNodeKey] = { name: device.name, group: mesh.name, ip: [obj.remoteaddr], count: 1 };
+ } else {
+ parent.duplicateAgentsLog[obj.dbNodeKey] = { name: device.name, group: mesh.name, ip: [obj.remoteaddr, dupAgent.remoteaddr], count: 1 };
+ }
+ } else {
+ parent.duplicateAgentsLog[obj.dbNodeKey].name = device.name;
+ parent.duplicateAgentsLog[obj.dbNodeKey].group = mesh.name;
+ parent.duplicateAgentsLog[obj.dbNodeKey].count++;
+ if (parent.duplicateAgentsLog[obj.dbNodeKey].ip.indexOf(obj.remoteaddr) == -1) { parent.duplicateAgentsLog[obj.dbNodeKey].ip.push(obj.remoteaddr); }
+ }
+
+ // Close the duplicate agent
+ parent.agentStats.duplicateAgentCount++;
+ parent.setAgentIssue(obj, 'duplicateAgent');
+ if (obj.nodeid != null) { parent.parent.debug('agent', 'Duplicate agent ' + obj.nodeid + ' (' + obj.remoteaddrport + ')'); }
+ dupAgent.close(3);
+ } else {
+ // Indicate the agent is connected
+ parent.parent.SetConnectivityState(obj.dbMeshKey, obj.dbNodeKey, obj.connectTime, 1, 1, null, { remoteaddrport: obj.remoteaddrport, name: device.name });
+ }
+
+ // We are done, ready to communicate with this agent
+ delete obj.pendingCompleteAgentConnection;
+ obj.authenticated = 2;
+
+ // Check how many times this agent disconnected in the last few minutes.
+ const disconnectCount = parent.wsagentsDisconnections[obj.nodeid];
+ if (disconnectCount > 6) {
+ parent.parent.debug('agent', 'Agent in big trouble: NodeId=' + obj.nodeid + ', IP=' + obj.remoteaddrport + ', Agent=' + obj.agentInfo.agentId + '.');
+ console.log('Agent in big trouble: NodeId=' + obj.nodeid + ', IP=' + obj.remoteaddrport + ', Agent=' + obj.agentInfo.agentId + '.');
+ parent.agentStats.agentInBigTrouble++;
+ // TODO: Log or do something to recover?
+ return;
+ }
+
+ // Command 4, inform mesh agent that it's authenticated.
+ obj.sendBinary(common.ShortToStr(4));
+
+ // Not sure why, but in rare cases, obj.agentInfo is undefined here.
+ if ((obj.agentInfo == null) || (typeof obj.agentInfo.capabilities != 'number')) { return; } // This is an odd case.
+ obj.agentExeInfo = parent.parent.meshAgentBinaries[obj.agentInfo.agentId];
+ if (domain.meshAgentBinaries && domain.meshAgentBinaries[obj.agentInfo.agentId]) { obj.agentExeInfo = domain.meshAgentBinaries[obj.agentInfo.agentId]; }
+
+ // Check if this agent is reconnecting too often.
+ if (disconnectCount > 4) {
+ // Too many disconnections, this agent has issues. Just clear the core.
+ obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0));
+ parent.parent.debug('agent', 'Agent in trouble: NodeId=' + obj.nodeid + ', IP=' + obj.remoteaddrport + ', Agent=' + obj.agentInfo.agentId + '.');
+ parent.agentStats.agentInTrouble++;
+ //console.log('Agent in trouble: NodeId=' + obj.nodeid + ', IP=' + obj.remoteaddrport + ', Agent=' + obj.agentInfo.agentId + '.');
+ // TODO: Log or do something to recover?
+ return;
+ }
+
+ // Check if we need to make an native update check
+ var corename = null;
+ if (parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId] != null) {
+ corename = parent.parent.meshAgentsArchitectureNumbers[obj.agentInfo.agentId].core;
+ } else {
+ // MeshCommand_CoreModule, ask mesh agent to clear the core
+ obj.sendBinary(common.ShortToStr(10) + common.ShortToStr(0));
+ }
+
+ if ((obj.agentExeInfo != null) && (obj.agentExeInfo.update == true)) {
+ // Ask the agent for it's executable binary hash
+ obj.sendBinary(common.ShortToStr(12) + common.ShortToStr(0));
+ } else {
+ // Check the mesh core, if the agent is capable of running one
+ if (((obj.agentInfo.capabilities & 16) != 0) && (corename != null)) {
+ obj.sendBinary(common.ShortToStr(11) + common.ShortToStr(0)); // Command 11, ask for mesh core hash.
+ } else {
+ agentCoreIsStable(); // No updates needed, agent is ready to go.
+ }
+ }
+ }
+
+ // Indicate to the agent that we want to check Intel AMT configuration
+ // This may trigger a CIRA-LMS tunnel from the agent so the server can inspect the device.
+ obj.sendUpdatedIntelAmtPolicy = function (policy) {
+ if (obj.agentExeInfo && (obj.agentExeInfo.amt == true)) { // Only send Intel AMT policy to agents what could have AMT.
+ if (policy == null) { var mesh = parent.meshes[obj.dbMeshKey]; if (mesh == null) return; policy = mesh.amt; }
+ if ((policy != null) && (policy.type != 0)) {
+ const cookie = parent.parent.encodeCookie({ a: 'apf', n: obj.dbNodeKey, m: obj.dbMeshKey }, parent.parent.loginCookieEncryptionKey);
+ try { obj.send(JSON.stringify({ action: 'amtconfig', user: '**MeshAgentApfTunnel**', pass: cookie })); } catch (ex) { }
+ }
+ }
+ }
+
+ function recoveryAgentCoreIsStable(mesh) {
+ parent.agentStats.recoveryCoreIsStableCount++;
+
+ // Recovery agent is doing ok, lets perform main agent checking.
+ //console.log('recoveryAgentCoreIsStable()');
+
+ // Fetch the the real agent nodeid
+ db.Get('da' + obj.dbNodeKey, function (err, nodes, self) {
+ if ((nodes != null) && (nodes.length == 1)) {
+ self.realNodeKey = nodes[0].raid;
+
+ // Get agent connection state
+ var agentConnected = false;
+ var state = parent.parent.GetConnectivityState(self.realNodeKey);
+ if (state) { agentConnected = ((state.connectivity & 1) != 0) }
+
+ self.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: self.realNodeKey, agent: agentConnected } }));
+ } else {
+ self.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: null } }));
+ }
+ }, obj);
+ }
+
+ function agentCoreIsStable() {
+ parent.agentStats.coreIsStableCount++;
+
+ // Check that the mesh exists
+ const mesh = parent.meshes[obj.dbMeshKey];
+ if (mesh == null) {
+ parent.agentStats.meshDoesNotExistCount++;
+ parent.setAgentIssue(obj, "meshDoesNotExist");
+ // TODO: Mark this agent as part of a mesh that does not exists.
+ return; // Probably not worth doing anything else. Hold this agent.
+ }
+
+ // Check if this is a recovery agent
+ if (obj.agentInfo.capabilities & 0x40) {
+ recoveryAgentCoreIsStable(mesh);
+ return;
+ }
+
+ // Fetch the the diagnostic agent nodeid
+ db.Get('ra' + obj.dbNodeKey, function (err, nodes) {
+ if ((nodes != null) && (nodes.length == 1)) {
+ obj.diagnosticNodeKey = nodes[0].daid;
+ obj.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: obj.diagnosticNodeKey } }));
+ }
+ });
+
+ // Indicate that we want to check the Intel AMT configuration
+ // This may trigger a CIRA-LMS tunnel to the server for further processing
+ obj.sendUpdatedIntelAmtPolicy();
+
+ // Fetch system information
+ db.GetHash('si' + obj.dbNodeKey, function (err, results) {
+ if ((results != null) && (results.length == 1)) { obj.send(JSON.stringify({ action: 'sysinfo', hash: results[0].hash })); } else { obj.send(JSON.stringify({ action: 'sysinfo' })); }
+ });
+
+ // Agent error log dump
+ if (parent.parent.agentErrorLog != null) {
+ db.Get('al' + obj.dbNodeKey, function (err, docs) { // Agent Log
+ if ((docs != null) && (docs.length == 1) && (typeof docs[0].lastEvent)) {
+ obj.send('{"action":"errorlog","startTime":' + docs[0].lastEvent + '}'); // Ask all events after a given time
+ } else {
+ obj.send('{"action":"errorlog"}'); // Ask all
+ }
+ });
+ }
+
+ // Set agent core dump
+ if ((parent.parent.config.settings != null) && ((parent.parent.config.settings.agentcoredump === true) || (parent.parent.config.settings.agentcoredump === false))) {
+ obj.send(JSON.stringify({ action: 'coredump', value: parent.parent.config.settings.agentcoredump }));
+ if (parent.parent.config.settings.agentcoredump === true) {
+ // Check if we requested a core dump file in the last minute, if not, ask if one is present.
+ if ((parent.lastCoreDumpRequest == null) || ((Date.now() - parent.lastCoreDumpRequest) >= 60000)) { obj.send(JSON.stringify({ action: 'getcoredump' })); }
+ }
+ }
+
+ // Do this if IP location is enabled on this domain TODO: Set IP location per device group?
+ if (domain.iplocation == true) {
+ // Check if we already have IP location information for this node
+ db.Get('iploc_' + obj.remoteaddr, function (err, iplocs) {
+ if ((iplocs != null) && (iplocs.length == 1)) {
+ // We have a location in the database for this remote IP
+ const iploc = iplocs[0], x = {};
+ if ((iploc != null) && (iploc.ip != null) && (iploc.loc != null)) {
+ x.publicip = iploc.ip;
+ x.iploc = iploc.loc + ',' + (Math.floor((new Date(iploc.date)) / 1000));
+ ChangeAgentLocationInfo(x);
+ }
+ } else {
+ // Check if we need to ask for the IP location
+ var doIpLocation = 0;
+ if (obj.iploc == null) {
+ doIpLocation = 1;
+ } else {
+ const loc = obj.iploc.split(',');
+ if (loc.length < 3) {
+ doIpLocation = 2;
+ } else {
+ var t = new Date((parseFloat(loc[2]) * 1000)), now = Date.now();
+ t.setDate(t.getDate() + 20);
+ if (t < now) { doIpLocation = 3; }
+ }
+ }
+
+ // If we need to ask for IP location, see if we have the quota to do it.
+ if (doIpLocation > 0) {
+ db.getValueOfTheDay('ipLocationRequestLimitor', 10, function (ipLocationLimitor) {
+ if ((ipLocationLimitor != null) && (ipLocationLimitor.value > 0)) {
+ ipLocationLimitor.value--;
+ db.Set(ipLocationLimitor);
+ obj.send(JSON.stringify({ action: 'iplocation' }));
+ }
+ });
+ }
+ }
+ });
+ }
+
+ // Indicate server information to the agent.
+ var serverInfo = { action: 'serverInfo' };
+ if ((typeof domain.terminal == 'object') && (typeof domain.terminal.launchcommand == 'object')) {
+ // Send terminal starting command
+ serverInfo.termlaunchcommand = {};
+ if (typeof domain.terminal.launchcommand.linux == 'string') { serverInfo.termlaunchcommand.linux = domain.terminal.launchcommand.linux; }
+ if (typeof domain.terminal.launchcommand.darwin == 'string') { serverInfo.termlaunchcommand.darwin = domain.terminal.launchcommand.darwin; }
+ if (typeof domain.terminal.launchcommand.freebsd == 'string') { serverInfo.termlaunchcommand.freebsd = domain.terminal.launchcommand.freebsd; }
+ }
+ // Enable agent self guest sharing if allowed
+ if (domain.agentselfguestsharing) { serverInfo.agentSelfGuestSharing = true; }
+ obj.send(JSON.stringify(serverInfo));
+
+ // Plug in handler
+ if (parent.parent.pluginHandler != null) {
+ parent.parent.pluginHandler.callHook('hook_agentCoreIsStable', obj, parent);
+ }
+ }
+
+ // Get the web certificate private key hash for the specified domain
+ function getWebCertHash(domain) {
+ const hash = parent.webCertificateHashs[domain.id];
+ if (hash != null) return hash;
+ return parent.webCertificateHash;
+ }
+
+ // Get the web certificate hash for the specified domain
+ function getWebCertFullHash(domain) {
+ const hash = parent.webCertificateFullHashs[domain.id];
+ if (hash != null) return hash;
+ return parent.webCertificateFullHash;
+ }
+
+ // Verify the agent signature
+ function processAgentSignature(msg) {
+ if (isIgnoreHashCheck() == false) {
+ var verified = false;
+
+ // This agent did not report a valid TLS certificate hash, fail now.
+ if (obj.agentSeenCerthash == null) return false;
+
+ // Raw RSA signatures have an exact length of 256 or 384. PKCS7 is larger.
+ if ((msg.length != 384) && (msg.length != 256)) {
+ // Verify a PKCS7 signature.
+ var msgDer = null;
+ try { msgDer = forge.asn1.fromDer(forge.util.createBuffer(msg, 'binary')); } catch (ex) { }
+ if (msgDer != null) {
+ try {
+ const p7 = forge.pkcs7.messageFromAsn1(msgDer);
+ const sig = p7.rawCapture.signature;
+
+ // Verify with key hash
+ var buf = Buffer.from(obj.agentSeenCerthash + obj.nonce + obj.agentnonce, 'binary');
+ var verifier = parent.crypto.createVerify('RSA-SHA384');
+ verifier.update(buf);
+ verified = verifier.verify(obj.unauth.nodeCertPem, sig, 'binary');
+ if (verified !== true) {
+ // Not a valid signature
+ parent.agentStats.invalidPkcsSignatureCount++;
+ parent.setAgentIssue(obj, "invalidPkcsSignature");
+ return false;
+ }
+ } catch (ex) { };
+ }
+ }
+
+ if (verified == false) {
+ // Verify the RSA signature. This is the fast way, without using forge.
+ const verify = parent.crypto.createVerify('SHA384');
+ verify.end(Buffer.from(obj.agentSeenCerthash + obj.nonce + obj.agentnonce, 'binary')); // Test using the private key hash
+ if (verify.verify(obj.unauth.nodeCertPem, Buffer.from(msg, 'binary')) !== true) {
+ parent.agentStats.invalidRsaSignatureCount++;
+ parent.setAgentIssue(obj, "invalidRsaSignature");
+ return false;
+ }
+ }
+ }
+
+ // Connection is a success, clean up
+ obj.nodeid = obj.unauth.nodeid;
+ obj.dbNodeKey = 'node/' + domain.id + '/' + obj.nodeid;
+ delete obj.nonce;
+ delete obj.agentnonce;
+ delete obj.unauth;
+ delete obj.receivedCommands;
+ delete obj.agentSeenCerthash;
+ if (obj.unauthsign) delete obj.unauthsign;
+ parent.agentStats.verifiedAgentConnectionCount++;
+ parent.parent.debug('agent', 'Verified agent connection to ' + obj.nodeid + ' (' + obj.remoteaddrport + ').');
+ obj.authenticated = 1;
+ return true;
+ }
+
+ // Process incoming agent JSON data
+ function processAgentData(msg) {
+ if (obj.agentInfo == null) return;
+ var i, str = msg.toString('utf8'), command = null;
+ if (str[0] == '{') {
+ try { command = JSON.parse(str); } catch (ex) {
+ // If the command can't be parsed, ignore it.
+ parent.agentStats.invalidJsonCount++;
+ parent.setAgentIssue(obj, "invalidJson (" + str.length + "): " + str);
+ parent.parent.debug('agent', 'Unable to parse agent JSON (' + obj.remoteaddrport + ')');
+ console.log('Unable to parse agent JSON (' + obj.remoteaddrport + '): ' + str, ex);
+ return;
+ }
+ if (typeof command != 'object') { return; }
+ switch (command.action) {
+ case 'msg':
+ {
+ // If the same console command is processed many times, kick out this agent.
+ // This is a safety mesure to guard against the agent DOS'ing the server.
+ if (command.type == 'console') {
+ if (obj.consoleKickValue == command.value) {
+ if (obj.consoleKickCount) { obj.consoleKickCount++; } else { obj.consoleKickCount = 1; }
+ if (obj.consoleKickCount > 30) { obj.close(); return; } // 30 identical console messages received, kick out this agent.
+ } else {
+ obj.consoleKickValue = command.value;
+ }
+ }
+
+ // Route a message
+ parent.routeAgentCommand(command, obj.domain.id, obj.dbNodeKey, obj.dbMeshKey);
+ break;
+ }
+ case 'coreinfo':
+ {
+ // Sent by the agent to update agent information
+ ChangeAgentCoreInfo(command);
+
+ if ((obj.agentCoreUpdate === true) && (obj.agentExeInfo != null) && (typeof obj.agentExeInfo.url == 'string')) {
+ // Agent update. The recovery core was loaded in the agent, send a command to update the agent
+ parent.parent.taskLimiter.launch(function (argument, taskid, taskLimiterQueue) { // Medium priority task
+ // If agent disconnection, complete and exit now.
+ if ((obj.authenticated != 2) || (obj.agentExeInfo == null)) { parent.parent.taskLimiter.completed(taskid); return; }
+
+ // Agent update. The recovery core was loaded in the agent, send a command to update the agent
+ obj.agentCoreUpdateTaskId = taskid;
+ const url = '*' + require('url').parse(obj.agentExeInfo.url).path;
+ var cmd = { action: 'agentupdate', url: url, hash: obj.agentExeInfo.hashhex };
+ parent.parent.debug('agentupdate', "Sending agent update url: " + cmd.url);
+
+ // Add the hash
+ if (obj.agentExeInfo.fileHash != null) { cmd.hash = obj.agentExeInfo.fileHashHex; } else { cmd.hash = obj.agentExeInfo.hashhex; }
+
+ // Add server TLS cert hash
+ if (isIgnoreHashCheck() == false) {
+ const tlsCertHash = parent.webCertificateFullHashs[domain.id];
+ if (tlsCertHash != null) { cmd.servertlshash = Buffer.from(tlsCertHash, 'binary').toString('hex'); }
+ }
+
+ // Send the agent update command
+ obj.send(JSON.stringify(cmd));
+ }, null, 1);
+ }
+ break;
+ }
+ case 'smbios':
+ {
+ // SMBIOS information must never be saved when NeDB is in use. NeDB will currupt that database.
+ if (db.SetSMBIOS == null) break;
+
+ // See if we need to save SMBIOS information
+ if (domain.smbios === true) {
+ // Store the RAW SMBios table of this computer
+ // Perform sanity checks before storing
+ try {
+ for (var i in command.value) { var k = parseInt(i); if ((k != i) || (i > 255) || (typeof command.value[i] != 'object') || (command.value[i].length == null) || (command.value[i].length > 1024) || (command.value[i].length < 0)) { delete command.value[i]; } }
+ db.SetSMBIOS({ _id: obj.dbNodeKey, domain: domain.id, time: new Date(), value: command.value });
+ } catch (ex) { }
+ }
+
+ // Event the node interface information change (This is a lot of traffic, probably don't need this).
+ //parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.meshid, [obj.dbNodeKey]), obj, { action: 'smBiosChange', nodeid: obj.dbNodeKey, domain: domain.id, smbios: command.value, nolog: 1 });
+
+ break;
+ }
+ case 'netinfo':
+ {
+ // Check if network information is present
+ if ((command.netif2 == null) && (command.netif == null)) return;
+
+ // Escape any field names that have special characters
+ if (command.netif2 != null) {
+ for (var i in command.netif2) {
+ var esc = common.escapeFieldName(i);
+ if (esc !== i) { command.netif2[esc] = command.netif2[i]; delete command.netif2[i]; }
+ }
+ }
+
+ // Sent by the agent to update agent network interface information
+ delete command.action;
+ command.updateTime = Date.now();
+ command._id = 'if' + obj.dbNodeKey;
+ command.domain = domain.id;
+ command.type = 'ifinfo';
+ db.Set(command);
+
+ // Event the node interface information change
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.meshid, [obj.dbNodeKey]), obj, { action: 'ifchange', nodeid: obj.dbNodeKey, domain: domain.id, nolog: 1 });
+
+ break;
+ }
+ case 'iplocation':
+ {
+ // Sent by the agent to update location information
+ if ((command.type == 'publicip') && (command.value != null) && (typeof command.value == 'object') && (command.value.ip) && (command.value.loc)) {
+ var x = {};
+ x.publicip = command.value.ip;
+ x.iploc = command.value.loc + ',' + (Math.floor(Date.now() / 1000));
+ ChangeAgentLocationInfo(x);
+ command.value._id = 'iploc_' + command.value.ip;
+ command.value.type = 'iploc';
+ command.value.date = Date.now();
+ db.Set(command.value); // Store the IP to location data in the database
+ // Sample Value: { ip: '192.55.64.246', city: 'Hillsboro', region: 'Oregon', country: 'US', loc: '45.4443,-122.9663', org: 'AS4983 Intel Corporation', postal: '97123' }
+ }
+ break;
+ }
+ case 'mc1migration':
+ {
+ if (command.oldnodeid.length != 64) break;
+ const oldNodeKey = 'node//' + command.oldnodeid.toLowerCase();
+ db.Get(oldNodeKey, function (err, nodes) {
+ if ((nodes == null) || (nodes.length != 1)) return;
+ const node = nodes[0];
+ if (node.meshid == obj.dbMeshKey) {
+ // Update the device name & host
+ const newNode = { "name": node.name };
+ if (node.intelamt != null) { newNode.intelamt = node.intelamt; }
+ ChangeAgentCoreInfo(newNode);
+
+ // Delete this node including network interface information and events
+ db.Remove(node._id);
+ db.Remove('if' + node._id);
+
+ // Event node deletion
+ const change = 'Migrated device ' + node.name;
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(node.meshid, [obj.dbNodeKey]), obj, { etype: 'node', action: 'removenode', nodeid: node._id, msg: change, domain: node.domain });
+ }
+ });
+ break;
+ }
+ case 'openUrl':
+ {
+ // Sent by the agent to return the status of a open URL action.
+ // Nothing is done right now.
+ break;
+ }
+ case 'log':
+ {
+ // Log a value in the event log
+ if ((typeof command.msg == 'string') && (command.msg.length < 4096)) {
+ var event = { etype: 'node', action: 'agentlog', nodeid: obj.dbNodeKey, domain: domain.id, msg: command.msg };
+ if (typeof command.msgid == 'number') { event.msgid = command.msgid; }
+ if (typeof command.guestname == 'string') { event.guestname = command.guestname; }
+ if (Array.isArray(command.msgArgs)) { event.msgArgs = command.msgArgs; }
+ if (typeof command.remoteaddr == 'string') { event.remoteaddr = command.remoteaddr; }
+ var targets = parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]);
+ if (typeof command.userid == 'string') {
+ var loguser = parent.users[command.userid];
+ if (loguser) { event.userid = command.userid; event.username = loguser.name; targets.push(command.userid); }
+ }
+ if (typeof command.xuserid == 'string') {
+ var xloguser = parent.users[command.xuserid];
+ if (xloguser) { targets.push(command.xuserid); }
+ }
+ if ((typeof command.sessionid == 'string') && (command.sessionid.length < 500)) { event.sessionid = command.sessionid; }
+ parent.parent.DispatchEvent(targets, obj, event);
+
+ // If this is a help request, see if we need to email notify anyone
+ if (event.msgid == 98) {
+ // Get the node and change it if needed
+ db.Get(obj.dbNodeKey, function (err, nodes) { // TODO: THIS IS A BIG RACE CONDITION HERE, WE NEED TO FIX THAT. If this call is made twice at the same time on the same device, data will be missed.
+ if ((nodes == null) || (nodes.length != 1)) { delete obj.deviceChanging; return; }
+ const device = nodes[0];
+ if (typeof device.name == 'string') { parent.parent.NotifyUserOfDeviceHelpRequest(domain, device.meshid, device._id, device.name, command.msgArgs[0], command.msgArgs[1]); }
+ });
+ }
+ }
+ break;
+ }
+ case 'ping': { sendPong(); break; }
+ case 'pong': { break; }
+ case 'getScript':
+ {
+ // Used by the agent to get configuration scripts.
+ if (command.type == 1) {
+ parent.getCiraConfigurationScript(obj.dbMeshKey, function (script) {
+ obj.send(JSON.stringify({ action: 'getScript', type: 1, script: script.toString() }));
+ });
+ } else if (command.type == 2) {
+ parent.getCiraCleanupScript(function (script) {
+ obj.send(JSON.stringify({ action: 'getScript', type: 2, script: script.toString() }));
+ });
+ }
+ break;
+ }
+ case 'diagnostic':
+ {
+ if (typeof command.value == 'object') {
+ switch (command.value.command) {
+ case 'register': {
+ // Only main agent can do this
+ if (((obj.agentInfo.capabilities & 0x40) == 0) && (typeof command.value.value == 'string') && (command.value.value.length == 64)) {
+ // Store links to diagnostic agent id
+ var daNodeKey = 'node/' + domain.id + '/' + db.escapeBase64(command.value.value);
+ db.Set({ _id: 'da' + daNodeKey, domain: domain.id, time: obj.connectTime, raid: obj.dbNodeKey }); // DiagnosticAgent --> Agent
+ db.Set({ _id: 'ra' + obj.dbNodeKey, domain: domain.id, time: obj.connectTime, daid: daNodeKey }); // Agent --> DiagnosticAgent
+ }
+ break;
+ }
+ case 'query': {
+ // Only the diagnostic agent can do
+ if ((obj.agentInfo.capabilities & 0x40) != 0) {
+ // Return nodeid of main agent + connection status
+ db.Get('da' + obj.dbNodeKey, function (err, nodes) {
+ if ((nodes != null) && (nodes.length == 1)) {
+ obj.realNodeKey = nodes[0].raid;
+
+ // Get agent connection state
+ var agentConnected = false;
+ var state = parent.parent.GetConnectivityState(obj.realNodeKey);
+ if (state) { agentConnected = ((state.connectivity & 1) != 0) }
+
+ obj.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: obj.realNodeKey, agent: agentConnected } }));
+ } else {
+ obj.send(JSON.stringify({ action: 'diagnostic', value: { command: 'query', value: null } }));
+ }
+ });
+ }
+ break;
+ }
+ case 'log': {
+ if (((obj.agentInfo.capabilities & 0x40) != 0) && (typeof command.value.value == 'string') && (command.value.value.length < 256)) {
+ // If this is a diagnostic agent, log the event in the log of the main agent
+ var event = { etype: 'node', action: 'diagnostic', nodeid: obj.realNodeKey, snodeid: obj.dbNodeKey, domain: domain.id, msg: command.value.value };
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, event);
+ }
+ break;
+ }
+ }
+ }
+ break;
+ }
+ case 'sysinfo': {
+ if ((typeof command.data == 'object') && (typeof command.data.hash == 'string')) {
+ // Validate command.data.
+ if (common.validateObjectForMongo(command.data, 1024) == false) break;
+
+ // Save to database
+ command.data._id = 'si' + obj.dbNodeKey;
+ command.data.type = 'sysinfo';
+ command.data.domain = domain.id;
+ command.data.time = Date.now();
+ db.Set(command.data); // Update system information in the database.
+
+ // Event the new sysinfo hash, this will notify everyone that the sysinfo document was changed
+ var event = { etype: 'node', action: 'sysinfohash', nodeid: obj.dbNodeKey, domain: domain.id, hash: command.data.hash, nolog: 1 };
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, event);
+ }
+ break;
+ }
+ case 'sysinfocheck': {
+ // Check system information update
+ db.GetHash('si' + obj.dbNodeKey, function (err, results) {
+ if ((results != null) && (results.length == 1)) { obj.send(JSON.stringify({ action: 'sysinfo', hash: results[0].hash })); } else { obj.send(JSON.stringify({ action: 'sysinfo' })); }
+ });
+ break;
+ }
+ case 'sessions': {
+ // This is a list of sessions provided by the agent
+ if (obj.sessions == null) { obj.sessions = {}; }
+ if (typeof command.value != null) {
+ if (command.type == 'kvm') { obj.sessions.kvm = command.value; }
+ else if (command.type == 'terminal') { obj.sessions.terminal = command.value; }
+ else if (command.type == 'files') { obj.sessions.files = command.value; }
+ else if (command.type == 'help') { obj.sessions.help = command.value; }
+ else if (command.type == 'tcp') { obj.sessions.tcp = command.value; }
+ else if (command.type == 'udp') { obj.sessions.udp = command.value; }
+ else if (command.type == 'msg') { obj.sessions.msg = command.value; }
+ else if (command.type == 'app') { obj.sessions.app = command.value; }
+ }
+
+ // Any "help" session must have an associated app, if not, remove it.
+ if (obj.sessions.help != null) {
+ for (var i in obj.sessions.help) { if (obj.sessions.help[i] == null) { delete obj.sessions.help[i]; } }
+ if (Object.keys(obj.sessions.help).length == 0) { delete obj.sessions.help; }
+ }
+
+ // Inform everyone of updated sessions
+ obj.updateSessions();
+ break;
+ }
+ case 'battery': {
+ // Device battery and power state
+ if (obj.sessions == null) { obj.sessions = {}; }
+ if (obj.sessions.battery == null) { obj.sessions.battery = {}; }
+ if ((command.state == 'ac') || (command.state == 'dc')) { obj.sessions.battery.state = command.state; } else { delete obj.sessions.battery.state; }
+ if ((typeof command.level == 'number') && (command.level >= 0) && (command.level <= 100)) { obj.sessions.battery.level = command.level; } else { delete obj.sessions.battery.level; }
+ obj.updateSessions();
+ break;
+ }
+ case 'getcoredump': {
+ // Check if we requested a core dump file in the last minute, if so, ignore this.
+ if ((parent.lastCoreDumpRequest != null) && ((Date.now() - parent.lastCoreDumpRequest) < 60000)) break;
+
+ // Indicates if the agent has a coredump available
+ if ((command.exists === true) && (typeof command.agenthashhex == 'string') && (command.agenthashhex.length == 96)) {
+ // Check if we already have this exact dump file
+ const coreDumpFile = parent.path.join(parent.parent.datapath, '..', 'meshcentral-coredumps', obj.agentInfo.agentId + '-' + command.agenthashhex + '-' + obj.nodeid + '.dmp');
+ parent.fs.stat(coreDumpFile, function (err, stats) {
+ if (stats != null) return;
+ obj.coreDumpPresent = true;
+
+ // Check how many files are in the coredumps folder
+ const coreDumpPath = parent.path.join(parent.parent.datapath, '..', 'meshcentral-coredumps');
+ parent.fs.readdir(coreDumpPath, function (err, files) {
+ if ((files != null) && (files.length >= 20)) return; // Don't get more than 20 core dump files.
+
+ // Get the core dump uploaded to the server.
+ parent.lastCoreDumpRequest = Date.now();
+ obj.RequestCoreDump(command.agenthashhex, command.corehashhex);
+ });
+ });
+ }
+ break;
+ }
+ case 'tunnelCloseStats': {
+ // TODO: This this extra stats from the tunnel, you can merge this into the tunnel event in the database.
+ //console.log(command);
+
+ // Validate input
+ if ((command.sent == null) || (typeof command.sent != 'string')) return;
+ if ((command.sentActual == null) || (typeof command.sentActual != 'string')) return;
+ if ((command.sentActual == null) || (typeof command.sentActual != 'number')) return;
+
+ // Event the session closed compression data.
+ var event = { etype: 'node', action: 'sessioncompression', nodeid: obj.dbNodeKey, domain: domain.id, sent: parseInt(command.sent), sentActual: parseInt(command.sentActual), msgid: 54, msgArgs: [command.sentRatio, parseInt(command.sent), parseInt(command.sentActual)], msg: 'Agent closed session with ' + command.sentRatio + '% agent to server compression. Sent: ' + command.sent + ', Compressed: ' + command.sentActual + '.' };
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, event);
+ break;
+ }
+ case 'lmsinfo': {
+ // Agents send the LMS port bindings
+ // Example: {"action":"lmsinfo","value":{"ports":["623","16992"]}}
+ break;
+ }
+ case 'plugin': {
+ if ((parent.parent.pluginHandler == null) || (typeof command.plugin != 'string')) break;
+ try {
+ parent.parent.pluginHandler.plugins[command.plugin].serveraction(command, obj, parent);
+ } catch (e) {
+ parent.parent.debug('agent', 'Error loading plugin handler (' + e + ')');
+ console.log('Error loading plugin handler (' + e + ')');
+ }
+ break;
+ }
+ case 'meshToolInfo': {
+ // Return information about a MeshCentral tool. Current tools are 'MeshCentralRouter' and 'MeshCentralAssistant'
+ // Information includes file hash and download location URL
+ if (typeof command.name != 'string') break;
+ var info = parent.parent.meshToolsBinaries[command.name];
+ if ((command.hash != null) && (info.hash == command.hash)) return;
+
+ // To build the connection URL, if we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
+ var xdomain = (domain.dns == null) ? domain.id : '';
+ if (xdomain != '') xdomain += '/';
+
+ // Build the response
+ const responseCmd = { action: 'meshToolInfo', name: command.name, tag: command.tag, sessionid: command.sessionid, hash: info.hash, size: info.size, url: info.url };
+ if ((command.name == 'MeshCentralAssistant') && (command.msh == true)) { responseCmd.url = '*/' + xdomain + 'meshagents?id=10006'; } // If this is Assistant and the MSH needs to be included in the executable, change the URL.
+ if (command.cookie === true) { responseCmd.url += ('&auth=' + parent.parent.encodeCookie({ download: info.dlname }, parent.parent.loginCookieEncryptionKey)); }
+ if (command.pipe === true) { responseCmd.pipe = true; }
+ if (parent.webCertificateHashs[domain.id] != null) { responseCmd.serverhash = Buffer.from(parent.webCertificateHashs[domain.id], 'binary').toString('hex'); }
+ try { ws.send(JSON.stringify(responseCmd)); } catch (ex) { }
+ break;
+ }
+ case 'agentupdate': {
+ if ((obj.agentExeInfo != null) && (typeof obj.agentExeInfo.url == 'string')) {
+ var func = function agentUpdateFunc(argument, taskid, taskLimiterQueue) { // Medium priority task
+ // If agent disconnection, complete and exit now.
+ if (obj.authenticated != 2) { parent.parent.taskLimiter.completed(taskid); return; }
+
+ // Agent is requesting an agent update
+ obj.agentCoreUpdateTaskId = taskid;
+ const url = '*' + require('url').parse(obj.agentExeInfo.url).path;
+ var cmd = { action: 'agentupdate', url: url, hash: obj.agentExeInfo.hashhex, sessionid: agentUpdateFunc.sessionid };
+ parent.parent.debug('agentupdate', "Sending user requested agent update url: " + cmd.url);
+
+ // Add the hash
+ if (obj.agentExeInfo.fileHash != null) { cmd.hash = obj.agentExeInfo.fileHashHex; } else { cmd.hash = obj.agentExeInfo.hashhex; }
+
+ // Add server TLS cert hash
+ if (isIgnoreHashCheck() == false) {
+ const tlsCertHash = parent.webCertificateFullHashs[domain.id];
+ if (tlsCertHash != null) { cmd.servertlshash = Buffer.from(tlsCertHash, 'binary').toString('hex'); }
+ }
+
+ // Send the agent update command
+ obj.send(JSON.stringify(cmd));
+ }
+ func.sessionid = command.sessionid;
+
+ // Agent update. The recovery core was loaded in the agent, send a command to update the agent
+ parent.parent.taskLimiter.launch(func, null, 1);
+ }
+ break;
+ }
+ case 'agentupdatedownloaded': {
+ if (obj.agentCoreUpdateTaskId != null) {
+ // Indicate this udpate task is complete
+ parent.parent.taskLimiter.completed(obj.agentCoreUpdateTaskId);
+ delete obj.agentCoreUpdateTaskId;
+ }
+ break;
+ }
+ case 'errorlog': { // This is the agent error log
+ if ((!Array.isArray(command.log)) || (command.log.length == 0) || (parent.parent.agentErrorLog == null)) break;
+ var lastLogEntry = command.log[command.log.length - 1];
+ if ((lastLogEntry != null) && (typeof lastLogEntry == 'object') && (typeof lastLogEntry.t == 'number')) {
+ parent.fs.write(parent.parent.agentErrorLog, obj.dbNodeKey + ', ' + Date.now() + ', ' + str + '\r\n', function (err) { });
+ db.Set({ _id: 'al' + obj.dbNodeKey, lastEvent: lastLogEntry.t });
+ }
+ break;
+ }
+ case '2faauth': {
+ // Validate input
+ if ((typeof command.url != 'string') || (typeof command.approved != 'boolean') || (command.url.startsWith('2fa://') == false)) return;
+
+ // parse the URL
+ var url = null;
+ try { url = require('url').parse(command.url); } catch (ex) { }
+ if (url == null) return;
+
+ // Decode the cookie
+ var urlSplit = url.query.split('&c=');
+ if (urlSplit.length != 2) return;
+ const authCookie = parent.parent.decodeCookie(urlSplit[1], null, 1);
+ if ((authCookie == null) || (typeof authCookie.c != 'string') || (('code=' + authCookie.c) != urlSplit[0])) return;
+ if ((typeof authCookie.n != 'string') || (authCookie.n != obj.dbNodeKey) || (typeof authCookie.u != 'string')) return;
+
+ // Fetch the user
+ const user = parent.users[authCookie.u];
+ if (user == null) return;
+
+ // Add this device as the authentication push notification device for this user
+ if (authCookie.a == 'addAuth') {
+ // Do nothing if authentication is not approved.
+ // We do not want to indicate that the remote user responded to this.
+ if (command.approved !== true) return;
+
+ // Change the user
+ user.otpdev = obj.dbNodeKey;
+ parent.db.SetUser(user);
+
+ // Notify change
+ var targets = ['*', 'server-users', user._id];
+ if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
+ var event = { etype: 'user', userid: user._id, username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msgid: 113, msg: "Added push notification authentication device", domain: domain.id };
+ if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
+ parent.parent.DispatchEvent(targets, obj, event);
+ }
+
+ // Complete 2FA checking
+ if (authCookie.a == 'checkAuth') {
+ if (typeof authCookie.s != 'string') return;
+ // Notify 2FA response
+ parent.parent.DispatchEvent(['2fadev-' + authCookie.s], obj, { etype: '2fadev', action: '2faresponse', domain: domain.id, nodeid: obj.dbNodeKey, code: authCookie.a, userid: user._id, approved: command.approved, sessionid: authCookie.s, nolog: 1 });
+ }
+
+ break;
+ }
+ case 'getUserImage': {
+ // Validate input
+ if (typeof command.userid != 'string') {
+ // Send back the default image if required
+ if ((command.default) || (command.sentDefault)) {
+ try { command.image = 'data:image/png;base64,' + Buffer.from(parent.fs.readFileSync(parent.parent.path.join(__dirname, 'public', 'images', 'user-128.png')), 'binary').toString('base64'); } catch (ex) { }
+ obj.send(JSON.stringify(command));
+ }
+ return;
+ }
+ var useridsplit = command.userid.split('/');
+ if ((useridsplit.length != 3) || (useridsplit[1] != domain.id)) return;
+
+ // Add the user's real name if present
+ var u = parent.users[command.userid];
+ if (u == null) return;
+ if (u.name) { command.name = u.name; }
+ if (u.realname) { command.realname = u.realname; }
+
+ // An agent can only request images of accounts with rights to the device.
+ if (parent.GetNodeRights(command.userid, obj.dbMeshKey, obj.dbNodeKey) != 0) {
+ parent.db.Get('im' + command.userid, function (err, images) {
+ if ((err == null) && (images != null) && (images.length == 1)) {
+ // Send back the account image
+ command.image = images[0].image;
+ } else {
+ // Send back the default image if required
+ if ((command.default) || (command.sentDefault)) {
+ try { command.image = 'data:image/png;base64,' + Buffer.from(parent.fs.readFileSync(parent.parent.path.join(__dirname, 'public', 'images', 'user-128.png')), 'binary').toString('base64'); } catch (ex) { }
+ }
+ }
+ obj.send(JSON.stringify(command));
+ });
+ }
+ break;
+ }
+ case 'getServerImage': {
+ if (command.agent === 'assistant') {
+ // Return server title and image for MeshCentral Assistant
+ if ((domain.assistantcustomization != null) && (typeof domain.assistantcustomization == 'object')) {
+ var ok = false;
+ if (typeof domain.assistantcustomization.title == 'string') { ok = true; command.title = domain.assistantcustomization.title; }
+ if (typeof domain.assistantcustomization.image == 'string') { try { command.image = 'data:image/jpeg;base64,' + Buffer.from(parent.fs.readFileSync(parent.parent.getConfigFilePath(domain.assistantcustomization.image)), 'binary').toString('base64'); ok = true; } catch (ex) { console.log(ex); } }
+ if (ok) { obj.send(JSON.stringify(command)); }
+ }
+ }
+ if (command.agent === 'android') {
+ // Return server title and image for MeshCentral Assistant
+ if ((domain.androidcustomization != null) && (typeof domain.androidcustomization == 'object')) {
+ var ok = false;
+ if (typeof domain.androidcustomization.title == 'string') { ok = true; command.title = domain.androidcustomization.title; }
+ if (typeof domain.androidcustomization.subtitle == 'string') { ok = true; command.subtitle = domain.androidcustomization.subtitle; }
+ if (typeof domain.androidcustomization.image == 'string') { try { command.image = 'data:image/jpeg;base64,' + Buffer.from(parent.fs.readFileSync(parent.parent.getConfigFilePath(domain.androidcustomization.image)), 'binary').toString('base64'); ok = true; } catch (ex) { console.log(ex); } }
+ if (ok) { obj.send(JSON.stringify(command)); }
+ }
+ }
+ break;
+ }
+ case 'guestShare': {
+ if ((command.flags == null) || (command.flags == 0)) {
+ // Stop any current self-share, this is allowed even if self guest sharing is not allows so to clear any old shares.
+ removeGuestSharing(function () {
+ delete obj.guestSharing;
+ obj.send(JSON.stringify({ action: 'guestShare', flags: command.flags, url: null, viewOnly: false }));
+ });
+ } else {
+ // Add a new self-share, this will replace any share for this device
+ if ((domain.agentselfguestsharing == null) || (domain.agentselfguestsharing == false) || (typeof command.flags != 'number')) return; // Check if agent self-sharing is allowed, this is off by default.
+ if ((command.flags & 2) == 0) { command.viewOnly = false; } // Only allow "view only" if desktop is shared.
+ addGuestSharing(command.flags, command.viewOnly, function (share) {
+ obj.guestSharing = true;
+ obj.send(JSON.stringify({ action: 'guestShare', url: share.url, flags: share.flags, viewOnly: share.viewOnly }));
+ })
+ }
+ break;
+ }
+ case 'amtconfig': {
+ // Sent by the agent when the agent needs a Intel AMT APF connection to the server
+ const cookie = parent.parent.encodeCookie({ a: 'apf', n: obj.dbNodeKey, m: obj.dbMeshKey }, parent.parent.loginCookieEncryptionKey);
+ try { obj.send(JSON.stringify({ action: 'amtconfig', user: '**MeshAgentApfTunnel**', pass: cookie })); } catch (ex) { }
+ break;
+ }
+ case 'script-task': {
+ // These command are for running regular batch jobs on the remote device
+ if (parent.parent.taskManager != null) { parent.parent.taskManager.agentAction(command, obj); }
+ break;
+ }
+ default: {
+ parent.agentStats.unknownAgentActionCount++;
+ parent.parent.debug('agent', 'Unknown agent action (' + obj.remoteaddrport + '): ' + JSON.stringify(command) + '.');
+ console.log('Unknown agent action (' + obj.remoteaddrport + '): ' + JSON.stringify(command) + '.');
+ break;
+ }
+ }
+ if (parent.parent.pluginHandler != null) {
+ parent.parent.pluginHandler.callHook('hook_processAgentData', command, obj, parent);
+ }
+ }
+ }
+
+ function addGuestSharing(flags, viewOnly, func) {
+ // Create cookie
+ const publicid = 'AS:' + obj.dbNodeKey;
+ const extrakey = getRandomAmtPassword();
+ const cookie = { a: 6, pid: publicid, k: extrakey }; // New style sharing cookie
+ const inviteCookie = parent.parent.encodeCookie(cookie, parent.parent.invitationLinkEncryptionKey);
+ if (inviteCookie == null) return;
+
+ // Create the server url
+ var serverName = parent.getWebServerName(domain, req);
+ var httpsPort = ((args.aliasport == null) ? args.port : args.aliasport); // Use HTTPS alias port is specified
+ var xdomain = (domain.dns == null) ? domain.id : '';
+ if (xdomain != '') xdomain += '/';
+ var url = 'https://' + serverName + ':' + httpsPort + '/' + xdomain + 'sharing?c=' + inviteCookie;
+ if (serverName.split('.') == 1) { url = '/' + xdomain + page + '?c=' + inviteCookie; }
+
+ // Create a device sharing database entry
+ var shareEntry = { _id: 'deviceshare-' + publicid, type: 'deviceshare', nodeid: obj.dbNodeKey, p: flags, domain: domain.id, publicid: publicid, guestName: 'Agent', consent: 0x7F, url: url, extrakey: extrakey };
+
+ // Add expire time
+ if ((typeof domain.agentselfguestsharing == 'object') && (typeof domain.agentselfguestsharing.expire == 'number') && (domain.agentselfguestsharing.expire > 0)) {
+ shareEntry.startTime = Date.now();
+ shareEntry.expireTime = Date.now() + (60000 * domain.agentselfguestsharing.expire);
+ }
+
+ if (viewOnly === true) { shareEntry.viewOnly = true; }
+ parent.db.Set(shareEntry);
+
+ // Send out an event that we added a device share
+ var targets = parent.CreateNodeDispatchTargets(obj.dbMeshKey, obj.dbNodeKey);
+ var event = { etype: 'node', nodeid: obj.dbNodeKey, action: 'addedDeviceShare', msg: 'Added device share with unlimited time', msgid: 131, msgArgs: ['Agent'], domain: domain.id };
+ parent.parent.DispatchEvent(targets, obj, event);
+
+ // Send device share update
+ parent.db.GetAllTypeNodeFiltered([obj.dbNodeKey], domain.id, 'deviceshare', null, function (err, docs) {
+ if (err != null) return;
+
+ // Check device sharing
+ var now = Date.now();
+ for (var i = 0; i < docs.length; i++) {
+ const doc = docs[i];
+ if (doc.expireTime < now) { parent.db.Remove(doc._id, function () { }); delete docs[i]; } else {
+ // This share is ok, remove extra data we don't need to send.
+ delete doc._id; delete doc.domain; delete doc.nodeid; delete doc.type;
+ }
+ }
+
+ // Send device share update
+ var targets = parent.CreateNodeDispatchTargets(obj.dbMeshKey, obj.dbNodeKey, []);
+ parent.parent.DispatchEvent(targets, obj, { etype: 'node', nodeid: obj.dbNodeKey, action: 'deviceShareUpdate', domain: domain.id, deviceShares: docs, nolog: 1 });
+
+ // Callback
+ if (func) { func({ url: url, flags: flags, viewOnly: viewOnly }); }
+ });
+ }
+
+ function removeGuestSharing(func) {
+ var publicid = 'AS:' + obj.dbNodeKey;
+ parent.db.GetAllTypeNodeFiltered([obj.dbNodeKey], domain.id, 'deviceshare', null, function (err, docs) {
+ if (err != null) return;
+
+ // Remove device sharing
+ var now = Date.now(), removedExact = null, removed = false, okDocs = [];
+ for (var i = 0; i < docs.length; i++) {
+ const doc = docs[i];
+ if (doc.publicid == publicid) { parent.db.Remove(doc._id, function () { }); removedExact = doc; removed = true; }
+ else if (doc.expireTime < now) { parent.db.Remove(doc._id, function () { }); removed = true; } else {
+ // This share is ok, remove extra data we don't need to send.
+ delete doc._id; delete doc.domain; delete doc.nodeid; delete doc.type;
+ okDocs.push(doc);
+ }
+ }
+
+ // Event device share removal
+ if (removedExact != null) {
+ // Send out an event that we removed a device share
+ var targets = parent.CreateNodeDispatchTargets(obj.dbMeshKey, obj.dbNodeKey, ['server-shareremove']);
+ var event = { etype: 'node', nodeid: obj.dbNodeKey, action: 'removedDeviceShare', msg: 'Removed Device Share', msgid: 102, msgArgs: ['Agent'], domain: domain.id, publicid: publicid };
+ parent.parent.DispatchEvent(targets, obj, event);
+ }
+
+ // If we removed any shares, send device share update
+ if (removed == true) {
+ var targets = parent.CreateNodeDispatchTargets(obj.dbMeshKey, obj.dbNodeKey, []);
+ parent.parent.DispatchEvent(targets, obj, { etype: 'node', nodeid: obj.dbNodeKey, action: 'deviceShareUpdate', domain: domain.id, deviceShares: okDocs, nolog: 1 });
+ }
+
+ // Call back when done
+ if (func) func(removed);
+ });
+ }
+
+ // Notify update of sessions
+ obj.updateSessions = function () {
+ // Perform some clean up
+ for (var i in obj.sessions) { if (Object.keys(obj.sessions[i]).length == 0) { delete obj.sessions[i]; } }
+ if (Object.keys(obj.sessions).length == 0) { delete obj.sessions; }
+
+ // Event the new sessions, this will notify everyone that agent sessions have changed
+ var event = { etype: 'node', action: 'devicesessions', nodeid: obj.dbNodeKey, domain: domain.id, sessions: obj.sessions, nolog: 1 };
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(obj.dbMeshKey, [obj.dbNodeKey]), obj, event);
+ }
+
+ // Change the current core information string and event it
+ function ChangeAgentCoreInfo(command) {
+ if ((obj.agentInfo == null) || (obj.agentInfo.capabilities & 0x40)) return;
+ if ((command == null) || (command == null)) return; // Safety, should never happen.
+
+ // If the device is pending a change, hold.
+ if (obj.deviceChanging === true) { setTimeout(function () { ChangeAgentCoreInfo(command); }, 100); return; }
+ obj.deviceChanging = true;
+
+ // Check that the mesh exists
+ const mesh = parent.meshes[obj.dbMeshKey];
+ if (mesh == null) { delete obj.deviceChanging; return; }
+
+ // Get the node and change it if needed
+ db.Get(obj.dbNodeKey, function (err, nodes) { // TODO: THIS IS A BIG RACE CONDITION HERE, WE NEED TO FIX THAT. If this call is made twice at the same time on the same device, data will be missed.
+ if ((nodes == null) || (nodes.length != 1)) { delete obj.deviceChanging; return; }
+ const device = nodes[0];
+ if (device.agent) {
+ var changes = [], change = 0, log = 0;
+
+ // Check if anything changes
+ if (command.name && (typeof command.name == 'string') && (command.name != device.name)) { change = 1; log = 1; device.name = command.name; changes.push('name'); }
+ if ((command.caps != null) && (device.agent.core != command.value)) { if ((command.value == null) && (device.agent.core != null)) { delete device.agent.core; } else { device.agent.core = command.value; } change = 1; } // Don't save this as an event to the db.
+ if ((command.caps != null) && ((device.agent.caps & 0xFFFFFFE7) != (command.caps & 0xFFFFFFE7))) { device.agent.caps = ((device.agent.caps & 24) + (command.caps & 0xFFFFFFE7)); change = 1; } // Allow Javascript on the agent to change all capabilities except console and javascript support, Don't save this as an event to the db.
+ if ((command.osdesc != null) && (typeof command.osdesc == 'string') && (device.osdesc != command.osdesc)) { device.osdesc = command.osdesc; change = 1; changes.push('os desc'); } // Don't save this as an event to the db.
+ if ((typeof command.root == 'boolean') && (command.root !== device.agent.root)) { change = 1; device.agent.root = command.root; }
+ if (device.ip != obj.remoteaddr) { device.ip = obj.remoteaddr; change = 1; }
+ if (command.intelamt) {
+ if (!device.intelamt) { device.intelamt = {}; }
+ if ((command.intelamt.Versions != null) && (typeof command.intelamt.Versions == 'object')) {
+ if ((command.intelamt.Versions.AMT != null) && (typeof command.intelamt.Versions.AMT == 'string') && (command.intelamt.Versions.AMT.length < 12) && (device.intelamt.ver != command.intelamt.Versions.AMT)) { changes.push('AMT version'); device.intelamt.ver = command.intelamt.Versions.AMT; change = 1; log = 1; }
+ if ((command.intelamt.Versions.Sku != null) && (typeof command.intelamt.Versions.Sku == 'string')) {
+ const sku = parseInt(command.intelamt.Versions.Sku);
+ if (device.intelamt.sku !== sku) { device.intelamt.sku = sku; change = 1; log = 1; }
+ }
+ }
+ if ((command.intelamt.ProvisioningState != null) && (typeof command.intelamt.ProvisioningState == 'number') && (device.intelamt.state != command.intelamt.ProvisioningState)) { changes.push('AMT state'); device.intelamt.state = command.intelamt.ProvisioningState; change = 1; log = 1; }
+ if ((command.intelamt.Flags != null) && (typeof command.intelamt.Flags == 'number') && (device.intelamt.flags != command.intelamt.Flags)) {
+ if (device.intelamt.flags) { changes.push('AMT flags (' + device.intelamt.flags + ' --> ' + command.intelamt.Flags + ')'); } else { changes.push('AMT flags (' + command.intelamt.Flags + ')'); }
+ device.intelamt.flags = command.intelamt.Flags; change = 1; log = 1;
+ }
+ if ((command.intelamt.UUID != null) && (typeof command.intelamt.UUID == 'string') && (device.intelamt.uuid != command.intelamt.UUID)) { changes.push('AMT uuid'); device.intelamt.uuid = command.intelamt.UUID; change = 1; log = 1; }
+ }
+ if (command.av != null) { // Antivirus
+ if (!device.av) { device.av = []; }
+ if (JSON.stringify(device.av) != JSON.stringify(command.av)) { /*changes.push('AV status');*/ device.av = command.av; change = 1; log = 1; }
+ }
+ if (command.wsc != null) { // Windows Security Center
+ if (!device.wsc) { device.wsc = {}; }
+ if (JSON.stringify(device.wsc) != JSON.stringify(command.wsc)) { /*changes.push('Windows Security Center status');*/ device.wsc = command.wsc; change = 1; log = 1; }
+ }
+ if (command.defender != null) { // Defender For Windows Server
+ if (!device.defender) { device.defender = {}; }
+ if (JSON.stringify(device.defender) != JSON.stringify(command.defender)) { /*changes.push('Defender status');*/ device.defender = command.defender; change = 1; log = 1; }
+ }
+ if (command.lastbootuptime != null) { // Last Boot Up Time
+ if (!device.lastbootuptime) { device.lastbootuptime = ""; }
+ if (device.lastbootuptime != command.lastbootuptime) { /*changes.push('Last Boot Up Time');*/ device.lastbootuptime = command.lastbootuptime; change = 1; log = 1; }
+ }
+
+ // Push Messaging Token
+ if ((command.pmt != null) && (typeof command.pmt == 'string') && (device.pmt != command.pmt)) {
+ if (typeof device.pmt == 'string') { db.Remove('pmt_' + device.pmt); }
+ device.pmt = command.pmt;
+ change = 1; // Don't save this change as an event to the db, so no log=1.
+ parent.removePmtFromAllOtherNodes(device); // We need to make sure to remove this push messaging token from any other device on this server, all domains included.
+ }
+
+ if ((command.users != null) && (Array.isArray(command.users)) && (device.users != command.users)) { device.users = command.users; change = 1; } // Don't save this to the db.
+ if ((command.lusers != null) && (Array.isArray(command.lusers)) && (device.lusers != command.lusers)) { device.lusers = command.lusers; change = 1; } // Don't save this to the db.
+ if ((mesh.mtype == 2) && (!args.wanonly)) {
+ // In WAN mode, the hostname of a computer is not important. Don't log hostname changes.
+ if (device.host != obj.remoteaddr) { device.host = obj.remoteaddr; change = 1; changes.push('host'); }
+ // TODO: Check that the agent has an interface that is the same as the one we got this websocket connection on. Only set if we have a match.
+ }
+
+ // Remove old volumes and BitLocker data, this is part of sysinfo.
+ delete device.volumes;
+
+ // If there are changes, event the new device
+ if (change == 1) {
+ // Do some clean up if needed, these values should not be in the database.
+ if (device.conn != null) { delete device.conn; }
+ if (device.pwr != null) { delete device.pwr; }
+ if (device.agct != null) { delete device.agct; }
+ if (device.cict != null) { delete device.cict; }
+
+ // Save to the database
+ db.Set(device);
+
+ // Event the node change
+ var event = { etype: 'node', action: 'changenode', nodeid: obj.dbNodeKey, domain: domain.id, node: parent.CloneSafeNode(device) };
+ if (changes.length > 0) { event.msg = 'Changed device ' + device.name + ' from group ' + mesh.name + ': ' + changes.join(', '); }
+ if ((log == 0) || ((obj.agentInfo) && (obj.agentInfo.capabilities) && (obj.agentInfo.capabilities & 0x20)) || (changes.length == 0)) { event.nolog = 1; } // If this is a temporary device, don't log changes
+ if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(device.meshid, [obj.dbNodeKey]), obj, event);
+ }
+
+ // Device change is done.
+ delete obj.deviceChanging;
+ }
+ });
+ }
+
+ // Change the current core information string and event it
+ function ChangeAgentLocationInfo(command) {
+ if (obj.agentInfo.capabilities & 0x40) return;
+ if ((command == null) || (command == null)) { return; } // Safety, should never happen.
+
+ // Check that the mesh exists
+ const mesh = parent.meshes[obj.dbMeshKey];
+ if (mesh == null) return;
+
+ // If the device is pending a change, hold.
+ if (obj.deviceChanging === true) { setTimeout(function () { ChangeAgentLocationInfo(command); }, 100); return; }
+ obj.deviceChanging = true;
+
+ // Get the node and change it if needed
+ db.Get(obj.dbNodeKey, function (err, nodes) {
+ if ((nodes == null) || (nodes.length != 1)) { delete obj.deviceChanging; return; }
+ const device = nodes[0];
+ if (device.agent) {
+ var changes = [], change = 0;
+
+ // Check if anything changes
+ if ((command.publicip) && (device.publicip != command.publicip)) { device.publicip = command.publicip; change = 1; changes.push('public ip'); }
+ if ((command.iploc) && (device.iploc != command.iploc)) { device.iploc = command.iploc; change = 1; changes.push('ip location'); }
+
+ // If there are changes, save and event
+ if (change == 1) {
+ // Do some clean up if needed, these values should not be in the database.
+ if (device.conn != null) { delete device.conn; }
+ if (device.pwr != null) { delete device.pwr; }
+ if (device.agct != null) { delete device.agct; }
+ if (device.cict != null) { delete device.cict; }
+
+ // Save the device
+ db.Set(device);
+
+ // Event the node change
+ var event = { etype: 'node', action: 'changenode', nodeid: obj.dbNodeKey, domain: domain.id, node: parent.CloneSafeNode(device), msgid: 59, msgArgs: [device.name, mesh.name, changes.join(', ')], msg: 'Changed device ' + device.name + ' from group ' + mesh.name + ': ' + changes.join(', ') };
+ if (obj.agentInfo.capabilities & 0x20) { event.nolog = 1; } // If this is a temporary device, don't log changes
+ if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(device.meshid, [obj.dbNodeKey]), obj, event);
+ }
+ }
+
+ // Done changing the device
+ delete obj.deviceChanging;
+ });
+ }
+
+ // Update the mesh agent tab in the database
+ function ChangeAgentTag(tag) {
+ if ((obj.agentInfo == null) || (obj.agentInfo.capabilities & 0x40)) return;
+ if ((tag != null) && (tag.length == 0)) { tag = null; }
+
+ // If the device is pending a change, hold.
+ if (obj.deviceChanging === true) {
+ var func = function ChangeAgentTagFunc() { ChangeAgentCoreInfo(ChangeAgentTagFunc.tag); }
+ func.tag = tag;
+ setTimeout(func, 100);
+ return;
+ }
+ obj.deviceChanging = true;
+
+ // Get the node and change it if needed
+ db.Get(obj.dbNodeKey, function (err, nodes) {
+ if ((nodes == null) || (nodes.length != 1)) { delete obj.deviceChanging; return; }
+ const device = nodes[0];
+ if (device.agent) {
+ // Parse the agent tag
+ var agentTag = null, serverName = null, serverDesc = null, serverTags = null;
+ if (tag != null) {
+ var taglines = tag.split('\r\n').join('\n').split('\r').join('\n').split('\n');
+ for (var i in taglines) {
+ var tagline = taglines[i].trim();
+ if (tagline.length > 0) {
+ if (tagline.startsWith('~')) {
+ if (tagline.startsWith('~ServerName:') && (tagline.length > 12) && (serverName == null)) { serverName = tagline.substring(12).trim(); }
+ if (tagline.startsWith('~ServerDesc:') && (tagline.length > 12) && (serverDesc == null)) { serverDesc = tagline.substring(12).trim(); }
+ if (tagline.startsWith('~ServerTags:') && (tagline.length > 12) && (serverTags == null)) { serverTags = tagline.substring(12).split(','); for (var j in serverTags) { serverTags[j] = serverTags[j].trim(); } }
+ } else { if (agentTag == null) { agentTag = tagline; } }
+ }
+ }
+ }
+
+ // Set the agent tag
+ var changes = false;
+ if (device.agent.tag != agentTag) { device.agent.tag = agentTag; if ((device.agent.tag == null) || (device.agent.tag == '')) { delete device.agent.tag; } changes = true; }
+ if (domain.agenttag != null) {
+ // Set the device's server name
+ if ((serverName != null) && (domain.agenttag.servername === 1) && (device.name != serverName)) { device.name = serverName; changes = true; }
+
+ // Set the device's server description
+ if ((serverDesc != null) && (domain.agenttag.serverdesc === 1) && (device.desc != serverDesc)) { device.desc = serverDesc; changes = true; }
+
+ // Set the device's server description if there is no description
+ if ((serverDesc != null) && (domain.agenttag.serverdesc === 2) && (device.desc != serverDesc) && ((device.desc == null) || (device.desc == ''))) { device.desc = serverDesc; changes = true; }
+
+ if ((serverTags != null) && (domain.agenttag.servertags != null) && (domain.agenttag.servertags != 0)) {
+ // Sort the tags
+ serverTags.sort();
+
+ // Stringify the tags
+ var st2 = '', st1 = serverTags.join(',');
+ if (device.tags != null) { st2 = device.tags.join(','); }
+
+ // Set the device's server tags
+ if ((domain.agenttag.servertags === 1) && (st1 != st2)) { device.tags = serverTags; changes = true; }
+
+ // Set the device's server tags if there are not tags
+ if ((domain.agenttag.servertags === 2) && (st2 == '')) { device.tags = serverTags; changes = true; }
+
+ // Append to device's server tags
+ if ((domain.agenttag.servertags === 3) && (st1 != st2)) {
+ if (device.tags == null) { device.tags = []; }
+ for (var i in serverTags) { if (device.tags.indexOf(serverTags[i]) == -1) { device.tags.push(serverTags[i]); } }
+ device.tags.sort();
+ changes = true;
+ }
+ }
+ }
+
+ if (changes == true) {
+ // Do some clean up if needed, these values should not be in the database.
+ if (device.conn != null) { delete device.conn; }
+ if (device.pwr != null) { delete device.pwr; }
+ if (device.agct != null) { delete device.agct; }
+ if (device.cict != null) { delete device.cict; }
+
+ // Update the device
+ db.Set(device);
+
+ // Event the node change
+ var event = { etype: 'node', action: 'changenode', nodeid: obj.dbNodeKey, domain: domain.id, node: parent.CloneSafeNode(device), nolog: 1 };
+ if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
+ parent.parent.DispatchEvent(parent.CreateMeshDispatchTargets(device.meshid, [obj.dbNodeKey]), obj, event);
+ }
+ }
+
+ // Done changing the device
+ delete obj.deviceChanging;
+ });
+ }
+
+ // Check if we need to update this agent, return true if agent binary update required.
+ // Return 0 is no update needed, 1 update using native system, 2 update using meshcore system
+ function compareAgentBinaryHash(agentExeInfo, agentHash) {
+ // If this is a temporary agent and the server is set to not update temporary agents, don't update the agent.
+ if ((obj.agentInfo.capabilities & 0x20) && (args.temporaryagentupdate === false)) return 0;
+ // If we are testing the agent update system, always return true
+ if ((args.agentupdatetest === true) || (args.agentupdatetest === 1)) return 1;
+ if (args.agentupdatetest === 2) return 2;
+ // If the hash matches or is null, no update required.
+ if ((agentExeInfo.hash == agentHash) || (agentHash == '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0')) return 0;
+ // If this is a macOS x86 or ARM agent type and it matched the universal binary, no update required.
+ if ((agentExeInfo.id == 16) || (agentExeInfo.id == 29)) {
+ if (domain.meshAgentBinaries && domain.meshAgentBinaries[10005]) {
+ if (domain.meshAgentBinaries[10005].hash == agentHash) return 0;
+ } else {
+ if (parent.parent.meshAgentBinaries[10005].hash == agentHash) return 0;
+ }
+ }
+
+ // No match, update the agent.
+ if (args.agentupdatesystem === 2) return 2; // If set, force a meshcore update.
+ if (agentExeInfo.id == 3) return 2; // Due to a bug in Windows 7 SP1 environement variable exec, we always update 32bit Windows agent using MeshCore for now. Upcoming agent will have a fix for this.
+ // NOTE: Windows agents with no commit dates may have bad native update system, so use meshcore system instead.
+ // NOTE: Windows agents with commit date prior to 1612740413000 did not kill all "meshagent.exe" processes and update could fail as a result executable being locked, meshcore system will do this.
+ if (((obj.AgentCommitDate == null) || (obj.AgentCommitDate < 1612740413000)) && ((agentExeInfo.id == 3) || (agentExeInfo.id == 4))) return 2; // For older Windows agents, use the meshcore update technique.
+ return 1; // By default, use the native update technique.
+ }
+
+ // Request that the core dump file on this agent be uploaded to the server
+ obj.RequestCoreDump = function (agenthashhex, corehashhex) {
+ if (agenthashhex.length > 16) { agenthashhex = agenthashhex.substring(0, 16); }
+ const cookie = parent.parent.encodeCookie({ a: 'aft', b: 'coredump', c: obj.agentInfo.agentId + '-' + agenthashhex + '-' + obj.nodeid + '.dmp' }, parent.parent.loginCookieEncryptionKey);
+ obj.send('{"action":"msg","type":"tunnel","value":"*/' + (((domain.dns == null) && (domain.id != '')) ? (domain.id + '/') : '') + 'agenttransfer.ashx?c=' + cookie + '","rights":"4294967295"}');
+ }
+
+ // Return true if we need to ignore the agent hash check
+ function isIgnoreHashCheck() {
+ if ((args.ignoreagenthashcheck === true) || (domain.ignoreagenthashcheck === true)) return true;
+
+ // Check site wide exceptions
+ if (Array.isArray(args.ignoreagenthashcheck)) {
+ for (var i = 0; i < args.ignoreagenthashcheck.length; i++) {
+ if (require('ipcheck').match(obj.remoteaddr, args.ignoreagenthashcheck[i])) return true;
+ }
+ }
+
+ // Check domain wide exceptions
+ if (Array.isArray(domain.ignoreagenthashcheck)) {
+ for (var i = 0; i < domain.ignoreagenthashcheck.length; i++) {
+ if (require('ipcheck').match(obj.remoteaddr, domain.ignoreagenthashcheck[i])) return true;
+ }
+ }
+
+ return false;
+ }
+
+ // Generate a random Intel AMT password
+ function checkAmtPassword(p) { return (p.length > 7) && (/\d/.test(p)) && (/[a-z]/.test(p)) && (/[A-Z]/.test(p)) && (/\W/.test(p)); }
+ function getRandomAmtPassword() { var p; do { p = Buffer.from(parent.crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); } while (checkAmtPassword(p) == false); return p; }
+
+ return obj;
+};
From fe117e52f7f091c8f33efd0aef476404992c7233 Mon Sep 17 00:00:00 2001
From: stephannn <38951729+stephannn@users.noreply.github.com>
Date: Mon, 13 Oct 2025 15:28:56 +0200
Subject: [PATCH 3/8] Add files via upload
---
db.js | 8455 ++++++++++++++++++++++++++++-----------------------------
1 file changed, 4224 insertions(+), 4231 deletions(-)
diff --git a/db.js b/db.js
index bde1afa76a..05a1d487d4 100644
--- a/db.js
+++ b/db.js
@@ -1,4231 +1,4224 @@
-/**
-* @description MeshCentral database module
-* @author Ylian Saint-Hilaire
-* @copyright Intel Corporation 2018-2022
-* @license Apache-2.0
-* @version v0.0.2
-*/
-
-/*xjslint node: true */
-/*xjslint plusplus: true */
-/*xjslint maxlen: 256 */
-/*jshint node: true */
-/*jshint strict: false */
-/*jshint esversion: 6 */
-"use strict";
-
-//
-// Construct Meshcentral database object
-//
-// The default database is NeDB
-// https://github.com/louischatriot/nedb
-//
-// Alternativety, MongoDB can be used
-// https://www.mongodb.com/
-// Just run with --mongodb [connectionstring], where the connection string is documented here: https://docs.mongodb.com/manual/reference/connection-string/
-// The default collection is "meshcentral", but you can override it using --mongodbcol [collection]
-//
-module.exports.CreateDB = function (parent, func) {
- var obj = {};
- var Datastore = null;
- var expireEventsSeconds = (60 * 60 * 24 * 20); // By default, expire events after 20 days (1728000). (Seconds * Minutes * Hours * Days)
- var expirePowerEventsSeconds = (60 * 60 * 24 * 10); // By default, expire power events after 10 days (864000). (Seconds * Minutes * Hours * Days)
- var expireServerStatsSeconds = (60 * 60 * 24 * 30); // By default, expire server stats after 30 days (2592000). (Seconds * Minutes * Hours * Days)
- const common = require('./common.js');
- const path = require('path');
- const fs = require('fs');
- const DB_NEDB = 1, DB_MONGOJS = 2, DB_MONGODB = 3,DB_MARIADB = 4, DB_MYSQL = 5, DB_POSTGRESQL = 6, DB_ACEBASE = 7, DB_SQLITE = 8;
- const DB_LIST = ['None', 'NeDB', 'MongoJS', 'MongoDB', 'MariaDB', 'MySQL', 'PostgreSQL', 'AceBase', 'SQLite']; //for the info command
- let databaseName = 'meshcentral';
- let datapathParentPath = path.dirname(parent.datapath);
- let datapathFoldername = path.basename(parent.datapath);
- const SQLITE_AUTOVACUUM = ['none', 'full', 'incremental'];
- const SQLITE_SYNCHRONOUS = ['off', 'normal', 'full', 'extra'];
- obj.sqliteConfig = {
- maintenance: '',
- startupVacuum: false,
- autoVacuum: 'full',
- incrementalVacuum: 100,
- journalMode: 'delete',
- journalSize: 4096000,
- synchronous: 'full',
- };
- obj.performingBackup = false;
- const BACKUPFAIL_ZIPCREATE = 0x0001;
- const BACKUPFAIL_ZIPMODULE = 0x0010;
- const BACKUPFAIL_DBDUMP = 0x0100;
- obj.backupStatus = 0x0;
- obj.newAutoBackupFile = null;
- obj.newDBDumpFile = null;
- obj.identifier = null;
- obj.dbKey = null;
- obj.dbRecordsEncryptKey = null;
- obj.dbRecordsDecryptKey = null;
- obj.changeStream = false;
- obj.pluginsActive = ((parent.config) && (parent.config.settings) && (parent.config.settings.plugins != null) && (parent.config.settings.plugins != false) && ((typeof parent.config.settings.plugins != 'object') || (parent.config.settings.plugins.enabled != false)));
- obj.dbCounters = {
- fileSet: 0,
- fileRemove: 0,
- powerSet: 0,
- eventsSet: 0
- }
-
- // MongoDB bulk operations state
- if (parent.config.settings.mongodbbulkoperations) {
- // Added counters
- obj.dbCounters.fileSetPending = 0;
- obj.dbCounters.fileSetBulk = 0;
- obj.dbCounters.fileRemovePending = 0;
- obj.dbCounters.fileRemoveBulk = 0;
- obj.dbCounters.powerSetPending = 0;
- obj.dbCounters.powerSetBulk = 0;
- obj.dbCounters.eventsSetPending = 0;
- obj.dbCounters.eventsSetBulk = 0;
-
- /// Added bulk accumulators
- obj.filePendingGet = null;
- obj.filePendingGets = null;
- obj.filePendingRemove = null;
- obj.filePendingRemoves = null;
- obj.filePendingSet = false;
- obj.filePendingSets = null;
- obj.filePendingCb = null;
- obj.filePendingCbs = null;
- obj.powerFilePendingSet = false;
- obj.powerFilePendingSets = null;
- obj.powerFilePendingCb = null;
- obj.powerFilePendingCbs = null;
- obj.eventsFilePendingSet = false;
- obj.eventsFilePendingSets = null;
- obj.eventsFilePendingCb = null;
- obj.eventsFilePendingCbs = null;
- }
-
- obj.SetupDatabase = function (func) {
- // Check if the database unique identifier is present
- // This is used to check that in server peering mode, everyone is using the same database.
- obj.Get('DatabaseIdentifier', function (err, docs) {
- if (err != null) { parent.debug('db', 'ERROR (Get DatabaseIdentifier): ' + err); }
- if ((err == null) && (docs.length == 1) && (docs[0].value != null)) {
- obj.identifier = docs[0].value;
- } else {
- obj.identifier = Buffer.from(require('crypto').randomBytes(48), 'binary').toString('hex');
- obj.Set({ _id: 'DatabaseIdentifier', value: obj.identifier });
- }
- });
-
- // Load database schema version and check if we need to update
- obj.Get('SchemaVersion', function (err, docs) {
- if (err != null) { parent.debug('db', 'ERROR (Get SchemaVersion): ' + err); }
- var ver = 0;
- if ((err == null) && (docs.length == 1)) { ver = docs[0].value; }
- if (ver == 1) { console.log('This is an unsupported beta 1 database, delete it to create a new one.'); process.exit(0); }
-
- // TODO: Any schema upgrades here...
- obj.Set({ _id: 'SchemaVersion', value: 2 });
-
- func(ver);
- });
- };
-
- // Perform database maintenance
- obj.maintenance = function () {
- parent.debug('db', 'Entering database maintenance');
- if (obj.databaseType == DB_NEDB) { // NeDB will not remove expired records unless we try to access them. This will force the removal.
- obj.eventsfile.remove({ time: { '$lt': new Date(Date.now() - (expireEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
- obj.powerfile.remove({ time: { '$lt': new Date(Date.now() - (expirePowerEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
- obj.serverstatsfile.remove({ time: { '$lt': new Date(Date.now() - (expireServerStatsSeconds * 1000)) } }, { multi: true }); // Force delete older events
- } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) { // MariaDB or MySQL
- sqlDbQuery('DELETE FROM events WHERE time < ?', [new Date(Date.now() - (expireEventsSeconds * 1000))], function (doc, err) { }); // Delete events older than expireEventsSeconds
- sqlDbQuery('DELETE FROM power WHERE time < ?', [new Date(Date.now() - (expirePowerEventsSeconds * 1000))], function (doc, err) { }); // Delete events older than expirePowerSeconds
- sqlDbQuery('DELETE FROM serverstats WHERE expire < ?', [new Date()], function (doc, err) { }); // Delete events where expiration date is in the past
- sqlDbQuery('DELETE FROM smbios WHERE expire < ?', [new Date()], function (doc, err) { }); // Delete events where expiration date is in the past
- } else if (obj.databaseType == DB_ACEBASE) { // AceBase
- //console.log('Performing AceBase maintenance');
- obj.file.query('events').filter('time', '<', new Date(Date.now() - (expireEventsSeconds * 1000))).remove().then(function () {
- obj.file.query('stats').filter('time', '<', new Date(Date.now() - (expireServerStatsSeconds * 1000))).remove().then(function () {
- obj.file.query('power').filter('time', '<', new Date(Date.now() - (expirePowerEventsSeconds * 1000))).remove().then(function () {
- //console.log('AceBase maintenance done');
- });
- });
- });
- } else if (obj.databaseType == DB_SQLITE) { // SQLite3
- //sqlite does not return rows affected for INSERT, UPDATE or DELETE statements, see https://www.sqlite.org/pragma.html#pragma_count_changes
- obj.file.serialize(function () {
- obj.file.run('DELETE FROM events WHERE time < ?', [new Date(Date.now() - (expireEventsSeconds * 1000))]);
- obj.file.run('DELETE FROM power WHERE time < ?', [new Date(Date.now() - (expirePowerEventsSeconds * 1000))]);
- obj.file.run('DELETE FROM serverstats WHERE expire < ?', [new Date()]);
- obj.file.run('DELETE FROM smbios WHERE expire < ?', [new Date()]);
- obj.file.exec(obj.sqliteConfig.maintenance, function (err) {
- if (err) {console.log('Maintenance error: ' + err.message)};
- if (parent.config.settings.debug) {
- sqliteGetPragmas(['freelist_count', 'page_size', 'page_count', 'cache_size' ], function (pragma, pragmaValue) {
- parent.debug('db', 'SQLite Maintenance: ' + pragma + '=' + pragmaValue);
- });
- };
- });
- });
- }
- obj.removeInactiveDevices();
- }
-
- // Remove inactive devices
- obj.removeInactiveDevices = function (showall, cb) {
- // Get a list of domains and what their inactive device removal setting is
- var removeInactiveDevicesPerDomain = {}, minRemoveInactiveDevicesPerDomain = {}, minRemoveInactiveDevice = 9999;
- for (var i in parent.config.domains) {
- if (typeof parent.config.domains[i].autoremoveinactivedevices == 'number') {
- var v = parent.config.domains[i].autoremoveinactivedevices;
- if ((v >= 1) && (v <= 2000)) {
- if (v < minRemoveInactiveDevice) { minRemoveInactiveDevice = v; }
- removeInactiveDevicesPerDomain[i] = v;
- minRemoveInactiveDevicesPerDomain[i] = v;
- }
- }
- }
-
- // Check if any device groups have a inactive device removal setting
- for (var i in parent.webserver.meshes) {
- if (typeof parent.webserver.meshes[i].expireDevs == 'number') {
- var v = parent.webserver.meshes[i].expireDevs;
- if ((v >= 1) && (v <= 2000)) {
- if (v < minRemoveInactiveDevice) { minRemoveInactiveDevice = v; }
- if ((minRemoveInactiveDevicesPerDomain[parent.webserver.meshes[i].domain] == null) || (minRemoveInactiveDevicesPerDomain[parent.webserver.meshes[i].domain] > v)) {
- minRemoveInactiveDevicesPerDomain[parent.webserver.meshes[i].domain] = v;
- }
- } else {
- delete parent.webserver.meshes[i].expireDevs;
- }
- }
- }
-
- // If there are no such settings for any domain, we can exit now.
- if (minRemoveInactiveDevice == 9999) { if (cb) { cb("No device removal policy set, nothing to do."); } return; }
- const now = Date.now();
-
- // For each domain with a inactive device removal setting, get a list of last device connections
- for (var domainid in minRemoveInactiveDevicesPerDomain) {
- obj.GetAllTypeNoTypeField('lastconnect', domainid, function (err, docs) {
- if ((err != null) || (docs == null)) return;
- for (var j in docs) {
- const days = Math.floor((now - docs[j].time) / 86400000); // Calculate the number of inactive days
- var expireDays = -1;
- if (removeInactiveDevicesPerDomain[docs[j].domain]) { expireDays = removeInactiveDevicesPerDomain[docs[j].domain]; }
- const mesh = parent.webserver.meshes[docs[j].meshid];
- if (mesh && (typeof mesh.expireDevs == 'number')) { expireDays = mesh.expireDevs; }
- var remove = false;
- if (expireDays > 0) {
- if (expireDays < days) { remove = true; }
- if (cb) { if (showall || remove) { cb(docs[j]._id.substring(2) + ', ' + days + ' days, expire ' + expireDays + ' days' + (remove ? ', removing' : '')); } }
- if (remove) {
- // Check if this device is connected right now
- const nodeid = docs[j]._id.substring(2);
- const conn = parent.GetConnectivityState(nodeid);
- if (conn == null) {
- // Remove the device
- obj.Get(nodeid, function (err, docs) {
- if (err != null) return;
- if ((docs == null) || (docs.length != 1)) { obj.Remove('lc' + nodeid); return; } // Remove last connect time
- const node = docs[0];
-
- // Delete this node including network interface information, events and timeline
- obj.Remove(node._id); // Remove node with that id
- obj.Remove('if' + node._id); // Remove interface information
- obj.Remove('nt' + node._id); // Remove notes
- obj.Remove('lc' + node._id); // Remove last connect time
- obj.Remove('si' + node._id); // Remove system information
- obj.Remove('al' + node._id); // Remove error log last time
- if (obj.RemoveSMBIOS) { obj.RemoveSMBIOS(node._id); } // Remove SMBios data
- obj.RemoveAllNodeEvents(node._id); // Remove all events for this node
- obj.removeAllPowerEventsForNode(node._id); // Remove all power events for this node
- if (typeof node.pmt == 'string') { obj.Remove('pmt_' + node.pmt); } // Remove Push Messaging Token
- obj.Get('ra' + node._id, function (err, nodes) {
- if ((nodes != null) && (nodes.length == 1)) { obj.Remove('da' + nodes[0].daid); } // Remove diagnostic agent to real agent link
- obj.Remove('ra' + node._id); // Remove real agent to diagnostic agent link
- });
-
- // Remove any user node links
- if (node.links != null) {
- for (var i in node.links) {
- if (i.startsWith('user/')) {
- var cuser = parent.webserver.users[i];
- if ((cuser != null) && (cuser.links != null) && (cuser.links[node._id] != null)) {
- // Remove the user link & save the user
- delete cuser.links[node._id];
- if (Object.keys(cuser.links).length == 0) { delete cuser.links; }
- obj.SetUser(cuser);
-
- // Notify user change
- var targets = ['*', 'server-users', cuser._id];
- var event = { etype: 'user', userid: cuser._id, username: cuser.name, action: 'accountchange', msgid: 86, msgArgs: [cuser.name], msg: 'Removed user device rights for ' + cuser.name, domain: node.domain, account: parent.webserver.CloneSafeUser(cuser) };
- if (obj.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
- parent.DispatchEvent(targets, obj, event);
- }
- } else if (i.startsWith('ugrp/')) {
- var cusergroup = parent.userGroups[i];
- if ((cusergroup != null) && (cusergroup.links != null) && (cusergroup.links[node._id] != null)) {
- // Remove the user link & save the user
- delete cusergroup.links[node._id];
- if (Object.keys(cusergroup.links).length == 0) { delete cusergroup.links; }
- obj.Set(cusergroup);
-
- // Notify user change
- var targets = ['*', 'server-users', cusergroup._id];
- var event = { etype: 'ugrp', ugrpid: cusergroup._id, name: cusergroup.name, desc: cusergroup.desc, action: 'usergroupchange', links: cusergroup.links, msgid: 163, msgArgs: [node.name, cusergroup.name], msg: 'Removed device ' + node.name + ' from user group ' + cusergroup.name };
- if (obj.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
- parent.DispatchEvent(targets, obj, event);
- }
- }
- }
- }
-
- // Event node deletion
- var meshname = '(unknown)';
- if ((parent.webserver.meshes[node.meshid] != null) && (parent.webserver.meshes[node.meshid].name != null)) { meshname = parent.webserver.meshes[node.meshid].name; }
- var event = { etype: 'node', action: 'removenode', nodeid: node._id, msgid: 87, msgArgs: [node.name, meshname], msg: 'Removed device ' + node.name + ' from device group ' + meshname, domain: node.domain };
- // TODO: We can't use the changeStream for node delete because we will not know the meshid the device was in.
- //if (obj.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to remove the node. Another event will come.
- parent.DispatchEvent(parent.webserver.CreateNodeDispatchTargets(node.meshid, node._id), obj, event);
- });
- }
- }
- }
- }
- });
- }
- }
-
- // Remove all reference to a domain from the database
- obj.removeDomain = function (domainName, func) {
- var pendingCalls;
- // Remove all events, power events and SMBIOS data from the main collection. They are all in seperate collections now.
- if (obj.databaseType == DB_ACEBASE) {
- // AceBase
- pendingCalls = 3;
- obj.file.query('meshcentral').filter('domain', '==', domainName).remove().then(function () { if (--pendingCalls == 0) { func(); } });
- obj.file.query('events').filter('domain', '==', domainName).remove().then(function () { if (--pendingCalls == 0) { func(); } });
- obj.file.query('power').filter('domain', '==', domainName).remove().then(function () { if (--pendingCalls == 0) { func(); } });
- } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL) || (obj.databaseType == DB_POSTGRESQL)) {
- // MariaDB, MySQL or PostgreSQL
- pendingCalls = 2;
- sqlDbQuery('DELETE FROM main WHERE domain = $1', [domainName], function () { if (--pendingCalls == 0) { func(); } });
- sqlDbQuery('DELETE FROM events WHERE domain = $1', [domainName], function () { if (--pendingCalls == 0) { func(); } });
- } else if (obj.databaseType == DB_MONGODB) {
- // MongoDB
- pendingCalls = 3;
- obj.file.deleteMany({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
- obj.eventsfile.deleteMany({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
- obj.powerfile.deleteMany({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
- } else {
- // NeDB or MongoJS
- pendingCalls = 3;
- obj.file.remove({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
- obj.eventsfile.remove({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
- obj.powerfile.remove({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
- }
- }
-
- obj.cleanup = function (func) {
- // TODO: Remove all mesh links to invalid users
- // TODO: Remove all meshes that dont have any links
-
- // Remove all events, power events and SMBIOS data from the main collection. They are all in seperate collections now.
- if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL) || (obj.databaseType == DB_POSTGRESQL)) {
- // MariaDB, MySQL or PostgreSQL
- obj.RemoveAllOfType('event', function () { });
- obj.RemoveAllOfType('power', function () { });
- obj.RemoveAllOfType('smbios', function () { });
- } else if (obj.databaseType == DB_MONGODB) {
- // MongoDB
- obj.file.deleteMany({ type: 'event' }, { multi: true });
- obj.file.deleteMany({ type: 'power' }, { multi: true });
- obj.file.deleteMany({ type: 'smbios' }, { multi: true });
- } else if ((obj.databaseType == DB_NEDB) || (obj.databaseType == DB_MONGOJS)) {
- // NeDB or MongoJS
- obj.file.remove({ type: 'event' }, { multi: true });
- obj.file.remove({ type: 'power' }, { multi: true });
- obj.file.remove({ type: 'smbios' }, { multi: true });
- }
-
- // List of valid identifiers
- var validIdentifiers = {}
-
- // Load all user groups
- obj.GetAllType('ugrp', function (err, docs) {
- if (err != null) { parent.debug('db', 'ERROR (GetAll user): ' + err); }
- if ((err == null) && (docs.length > 0)) {
- for (var i in docs) {
- // Add this as a valid user identifier
- validIdentifiers[docs[i]._id] = 1;
- }
- }
-
- // Fix all of the creating & login to ticks by seconds, not milliseconds.
- obj.GetAllType('user', function (err, docs) {
- if (err != null) { parent.debug('db', 'ERROR (GetAll user): ' + err); }
- if ((err == null) && (docs.length > 0)) {
- for (var i in docs) {
- var fixed = false;
-
- // Add this as a valid user identifier
- validIdentifiers[docs[i]._id] = 1;
-
- // Fix email address capitalization
- if (docs[i].email && (docs[i].email != docs[i].email.toLowerCase())) {
- docs[i].email = docs[i].email.toLowerCase(); fixed = true;
- }
-
- // Fix account creation
- if (docs[i].creation) {
- if (docs[i].creation > 1300000000000) { docs[i].creation = Math.floor(docs[i].creation / 1000); fixed = true; }
- if ((docs[i].creation % 1) != 0) { docs[i].creation = Math.floor(docs[i].creation); fixed = true; }
- }
-
- // Fix last account login
- if (docs[i].login) {
- if (docs[i].login > 1300000000000) { docs[i].login = Math.floor(docs[i].login / 1000); fixed = true; }
- if ((docs[i].login % 1) != 0) { docs[i].login = Math.floor(docs[i].login); fixed = true; }
- }
-
- // Fix last password change
- if (docs[i].passchange) {
- if (docs[i].passchange > 1300000000000) { docs[i].passchange = Math.floor(docs[i].passchange / 1000); fixed = true; }
- if ((docs[i].passchange % 1) != 0) { docs[i].passchange = Math.floor(docs[i].passchange); fixed = true; }
- }
-
- // Fix subscriptions
- if (docs[i].subscriptions != null) { delete docs[i].subscriptions; fixed = true; }
-
- // Save the user if needed
- if (fixed) { obj.Set(docs[i]); }
- }
-
- // Remove all objects that have a "meshid" that no longer points to a valid mesh.
- // Fix any incorrectly escaped user identifiers
- obj.GetAllType('mesh', function (err, docs) {
- if (err != null) { parent.debug('db', 'ERROR (GetAll mesh): ' + err); }
- var meshlist = [];
- if ((err == null) && (docs.length > 0)) {
- for (var i in docs) {
- var meshChange = false;
- docs[i] = common.unEscapeLinksFieldName(docs[i]);
- meshlist.push(docs[i]._id);
-
- // Make sure all mesh types are number type, if not, fix it.
- if (typeof docs[i].mtype == 'string') { docs[i].mtype = parseInt(docs[i].mtype); meshChange = true; }
-
- // If the device group is deleted, remove any invite codes
- if (docs[i].deleted && docs[i].invite) { delete docs[i].invite; meshChange = true; }
-
- // Take a look at the links
- if (docs[i].links != null) {
- for (var j in docs[i].links) {
- if (validIdentifiers[j] == null) {
- // This identifier is not known, let see if we can fix it.
- var xid = j, xid2 = common.unEscapeFieldName(xid);
- while ((xid != xid2) && (validIdentifiers[xid2] == null)) { xid = xid2; xid2 = common.unEscapeFieldName(xid2); }
- if (validIdentifiers[xid2] == 1) {
- //console.log('Fixing id: ' + j + ' to ' + xid2);
- docs[i].links[xid2] = docs[i].links[j];
- delete docs[i].links[j];
- meshChange = true;
- } else {
- // TODO: here, we may want to clean up links to users and user groups that do not exist anymore.
- //console.log('Unknown id: ' + j);
- }
- }
- }
- }
-
- // Save the updated device group if needed
- if (meshChange) { obj.Set(docs[i]); }
- }
- }
- if (obj.databaseType == DB_SQLITE) {
- // SQLite
-
- } else if (obj.databaseType == DB_ACEBASE) {
- // AceBase
-
- } else if (obj.databaseType == DB_POSTGRESQL) {
- // Postgres
- sqlDbQuery('DELETE FROM Main WHERE ((extra != NULL) AND (extra LIKE (\'mesh/%\')) AND (extra != ANY ($1)))', [meshlist], function (err, response) { });
- } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
- // MariaDB
- sqlDbQuery('DELETE FROM Main WHERE (extra LIKE ("mesh/%") AND (extra NOT IN ?)', [meshlist], function (err, response) { });
- } else if (obj.databaseType == DB_MONGODB) {
- // MongoDB
- obj.file.deleteMany({ meshid: { $exists: true, $nin: meshlist } }, { multi: true });
- } else {
- // NeDB or MongoJS
- obj.file.remove({ meshid: { $exists: true, $nin: meshlist } }, { multi: true });
- }
-
- // We are done
- validIdentifiers = null;
- if (func) { func(); }
- });
- }
- });
- });
- };
-
- // Get encryption key
- obj.getEncryptDataKey = function (password, salt, iterations) {
- if (typeof password != 'string') return null;
- let key;
- try {
- key = parent.crypto.pbkdf2Sync(password, salt, iterations, 32, 'sha384');
- } catch (ex) {
- // If this previous call fails, it's probably because older pbkdf2 did not specify the hashing function, just use the default.
- key = parent.crypto.pbkdf2Sync(password, salt, iterations, 32);
- }
- return key
- }
-
- // Encrypt data
- obj.encryptData = function (password, plaintext) {
- let encryptionVersion = 0x01;
- let iterations = 100000
- const iv = parent.crypto.randomBytes(16);
- var key = obj.getEncryptDataKey(password, iv, iterations);
- if (key == null) return null;
- const aes = parent.crypto.createCipheriv('aes-256-gcm', key, iv);
- var ciphertext = aes.update(plaintext);
- let versionbuf = Buffer.allocUnsafe(2);
- versionbuf.writeUInt16BE(encryptionVersion);
- let iterbuf = Buffer.allocUnsafe(4);
- iterbuf.writeUInt32BE(iterations);
- let encryptedBuf = aes.final();
- ciphertext = Buffer.concat([versionbuf, iterbuf, aes.getAuthTag(), iv, ciphertext, encryptedBuf]);
- return ciphertext.toString('base64');
- }
-
- // Decrypt data
- obj.decryptData = function (password, ciphertext) {
- // Adding an encryption version lets us avoid try catching in the future
- let ciphertextBytes = Buffer.from(ciphertext, 'base64');
- let encryptionVersion = ciphertextBytes.readUInt16BE(0);
- try {
- switch (encryptionVersion) {
- case 0x01:
- let iterations = ciphertextBytes.readUInt32BE(2);
- let authTag = ciphertextBytes.slice(6, 22);
- const iv = ciphertextBytes.slice(22, 38);
- const data = ciphertextBytes.slice(38);
- let key = obj.getEncryptDataKey(password, iv, iterations);
- if (key == null) return null;
- const aes = parent.crypto.createDecipheriv('aes-256-gcm', key, iv);
- aes.setAuthTag(authTag);
- let plaintextBytes = Buffer.from(aes.update(data));
- plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
- return plaintextBytes;
- default:
- return obj.oldDecryptData(password, ciphertextBytes);
- }
- } catch (ex) { return obj.oldDecryptData(password, ciphertextBytes); }
- }
-
- // Encrypt data
- // The older encryption system uses CBC without integraty checking.
- // This method is kept only for testing
- obj.oldEncryptData = function (password, plaintext) {
- let key = parent.crypto.createHash('sha384').update(password).digest('raw').slice(0, 32);
- if (key == null) return null;
- const iv = parent.crypto.randomBytes(16);
- const aes = parent.crypto.createCipheriv('aes-256-cbc', key, iv);
- var ciphertext = aes.update(plaintext);
- ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
- return ciphertext.toString('base64');
- }
-
- // Decrypt data
- // The older encryption system uses CBC without integraty checking.
- // This method is kept only to convert the old encryption to the new one.
- obj.oldDecryptData = function (password, ciphertextBytes) {
- if (typeof password != 'string') return null;
- try {
- const iv = ciphertextBytes.slice(0, 16);
- const data = ciphertextBytes.slice(16);
- let key = parent.crypto.createHash('sha384').update(password).digest('raw').slice(0, 32);
- const aes = parent.crypto.createDecipheriv('aes-256-cbc', key, iv);
- let plaintextBytes = Buffer.from(aes.update(data));
- plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
- return plaintextBytes;
- } catch (ex) { return null; }
- }
-
- // Get the number of records in the database for various types, this is the slow NeDB way.
- // WARNING: This is a terrible query for database performance. Only do this when needed. This query will look at almost every document in the database.
- obj.getStats = function (func) {
- if (obj.databaseType == DB_ACEBASE) {
- // AceBase
- // TODO
- } else if (obj.databaseType == DB_POSTGRESQL) {
- // PostgreSQL
- // TODO
- } else if (obj.databaseType == DB_MYSQL) {
- // MySQL
- // TODO
- } else if (obj.databaseType == DB_MARIADB) {
- // MariaDB
- // TODO
- } else if (obj.databaseType == DB_MONGODB) {
- // MongoDB
- obj.file.aggregate([{ "$group": { _id: "$type", count: { $sum: 1 } } }]).toArray(function (err, docs) {
- var counters = {}, totalCount = 0;
- if (err == null) { for (var i in docs) { if (docs[i]._id != null) { counters[docs[i]._id] = docs[i].count; totalCount += docs[i].count; } } }
- func(counters);
- });
- } else if (obj.databaseType == DB_MONGOJS) {
- // MongoJS
- obj.file.aggregate([{ "$group": { _id: "$type", count: { $sum: 1 } } }], function (err, docs) {
- var counters = {}, totalCount = 0;
- if (err == null) { for (var i in docs) { if (docs[i]._id != null) { counters[docs[i]._id] = docs[i].count; totalCount += docs[i].count; } } }
- func(counters);
- });
- } else if (obj.databaseType == DB_NEDB) {
- // NeDB version
- obj.file.count({ type: 'node' }, function (err, nodeCount) {
- obj.file.count({ type: 'mesh' }, function (err, meshCount) {
- obj.file.count({ type: 'user' }, function (err, userCount) {
- obj.file.count({ type: 'sysinfo' }, function (err, sysinfoCount) {
- obj.file.count({ type: 'note' }, function (err, noteCount) {
- obj.file.count({ type: 'iploc' }, function (err, iplocCount) {
- obj.file.count({ type: 'ifinfo' }, function (err, ifinfoCount) {
- obj.file.count({ type: 'cfile' }, function (err, cfileCount) {
- obj.file.count({ type: 'lastconnect' }, function (err, lastconnectCount) {
- obj.file.count({}, function (err, totalCount) {
- func({ node: nodeCount, mesh: meshCount, user: userCount, sysinfo: sysinfoCount, iploc: iplocCount, note: noteCount, ifinfo: ifinfoCount, cfile: cfileCount, lastconnect: lastconnectCount, total: totalCount });
- });
- });
- });
- });
- });
- });
- });
- });
- });
- });
- }
- }
-
- // This is used to rate limit a number of operation per day. Returns a startValue each new days, but you can substract it and save the value in the db.
- obj.getValueOfTheDay = function (id, startValue, func) { obj.Get(id, function (err, docs) { var date = new Date(), t = date.toLocaleDateString(); if ((err == null) && (docs.length == 1)) { var r = docs[0]; if (r.day == t) { func({ _id: id, value: r.value, day: t }); return; } } func({ _id: id, value: startValue, day: t }); }); };
- obj.escapeBase64 = function escapeBase64(val) { return (val.replace(/\+/g, '@').replace(/\//g, '$')); }
-
- // Encrypt an database object
- obj.performRecordEncryptionRecode = function (func) {
- var count = 0;
- obj.GetAllType('user', function (err, docs) {
- if (err != null) { parent.debug('db', 'ERROR (performRecordEncryptionRecode): ' + err); }
- if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } }
- obj.GetAllType('node', function (err, docs) {
- if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } }
- obj.GetAllType('mesh', function (err, docs) {
- if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } }
- if (obj.databaseType == DB_NEDB) { // If we are using NeDB, compact the database.
- obj.file.compactDatafile();
- obj.file.on('compaction.done', function () { func(count); }); // It's important to wait for compaction to finish before exit, otherwise NeDB may corrupt.
- } else {
- func(count); // For all other databases, normal exit.
- }
- });
- });
- });
- }
-
- // Encrypt an database object
- function performTypedRecordDecrypt(data) {
- if ((data == null) || (obj.dbRecordsDecryptKey == null) || (typeof data != 'object')) return data;
- for (var i in data) {
- if ((data[i] == null) || (typeof data[i] != 'object')) continue;
- data[i] = performPartialRecordDecrypt(data[i]);
- if ((data[i].intelamt != null) && (typeof data[i].intelamt == 'object') && (data[i].intelamt._CRYPT)) { data[i].intelamt = performPartialRecordDecrypt(data[i].intelamt); }
- if ((data[i].amt != null) && (typeof data[i].amt == 'object') && (data[i].amt._CRYPT)) { data[i].amt = performPartialRecordDecrypt(data[i].amt); }
- if ((data[i].kvm != null) && (typeof data[i].kvm == 'object') && (data[i].kvm._CRYPT)) { data[i].kvm = performPartialRecordDecrypt(data[i].kvm); }
- }
- return data;
- }
-
- // Encrypt an database object
- function performTypedRecordEncrypt(data) {
- if (obj.dbRecordsEncryptKey == null) return data;
- if (data.type == 'user') { return performPartialRecordEncrypt(Clone(data), ['otpkeys', 'otphkeys', 'otpsecret', 'salt', 'hash', 'oldpasswords']); }
- else if ((data.type == 'node') && (data.ssh || data.rdp || data.intelamt)) {
- var xdata = Clone(data);
- if (data.ssh || data.rdp) { xdata = performPartialRecordEncrypt(xdata, ['ssh', 'rdp']); }
- if (data.intelamt) { xdata.intelamt = performPartialRecordEncrypt(xdata.intelamt, ['pass', 'mpspass']); }
- return xdata;
- }
- else if ((data.type == 'mesh') && (data.amt || data.kvm)) {
- var xdata = Clone(data);
- if (data.amt) { xdata.amt = performPartialRecordEncrypt(xdata.amt, ['password']); }
- if (data.kvm) { xdata.kvm = performPartialRecordEncrypt(xdata.kvm, ['pass']); }
- return xdata;
- }
- return data;
- }
-
- // Encrypt an object and return a buffer.
- function performPartialRecordEncrypt(plainobj, encryptNames) {
- if (typeof plainobj != 'object') return plainobj;
- var enc = {}, enclen = 0;
- for (var i in encryptNames) { if (plainobj[encryptNames[i]] != null) { enclen++; enc[encryptNames[i]] = plainobj[encryptNames[i]]; delete plainobj[encryptNames[i]]; } }
- if (enclen > 0) { plainobj._CRYPT = performRecordEncrypt(enc); } else { delete plainobj._CRYPT; }
- return plainobj;
- }
-
- // Encrypt an object and return a buffer.
- function performPartialRecordDecrypt(plainobj) {
- if ((typeof plainobj != 'object') || (plainobj._CRYPT == null)) return plainobj;
- var enc = performRecordDecrypt(plainobj._CRYPT);
- if (enc != null) { for (var i in enc) { plainobj[i] = enc[i]; } }
- delete plainobj._CRYPT;
- return plainobj;
- }
-
- // Encrypt an object and return a base64.
- function performRecordEncrypt(plainobj) {
- if (obj.dbRecordsEncryptKey == null) return null;
- const iv = parent.crypto.randomBytes(12);
- const aes = parent.crypto.createCipheriv('aes-256-gcm', obj.dbRecordsEncryptKey, iv);
- var ciphertext = aes.update(JSON.stringify(plainobj));
- var cipherfinal = aes.final();
- ciphertext = Buffer.concat([iv, aes.getAuthTag(), ciphertext, cipherfinal]);
- return ciphertext.toString('base64');
- }
-
- // Takes a base64 and return an object.
- function performRecordDecrypt(ciphertext) {
- if (obj.dbRecordsDecryptKey == null) return null;
- const ciphertextBytes = Buffer.from(ciphertext, 'base64');
- const iv = ciphertextBytes.slice(0, 12);
- const data = ciphertextBytes.slice(28);
- const aes = parent.crypto.createDecipheriv('aes-256-gcm', obj.dbRecordsDecryptKey, iv);
- aes.setAuthTag(ciphertextBytes.slice(12, 28));
- var plaintextBytes, r;
- try {
- plaintextBytes = Buffer.from(aes.update(data));
- plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
- r = JSON.parse(plaintextBytes.toString());
- } catch (e) { throw "Incorrect DbRecordsDecryptKey/DbRecordsEncryptKey or invalid database _CRYPT data: " + e; }
- return r;
- }
-
- // Clone an object (TODO: Make this more efficient)
- function Clone(v) { return JSON.parse(JSON.stringify(v)); }
-
- // Read expiration time from configuration file
- if (typeof parent.args.dbexpire == 'object') {
- if (typeof parent.args.dbexpire.events == 'number') { expireEventsSeconds = parent.args.dbexpire.events; }
- if (typeof parent.args.dbexpire.powerevents == 'number') { expirePowerEventsSeconds = parent.args.dbexpire.powerevents; }
- if (typeof parent.args.dbexpire.statsevents == 'number') { expireServerStatsSeconds = parent.args.dbexpire.statsevents; }
- }
-
- // If a DB record encryption key is provided, perform database record encryption
- if ((typeof parent.args.dbrecordsencryptkey == 'string') && (parent.args.dbrecordsencryptkey.length != 0)) {
- // Hash the database password into a AES256 key and setup encryption and decryption.
- obj.dbRecordsEncryptKey = obj.dbRecordsDecryptKey = parent.crypto.createHash('sha384').update(parent.args.dbrecordsencryptkey).digest('raw').slice(0, 32);
- }
-
- // If a DB record decryption key is provided, perform database record decryption
- if ((typeof parent.args.dbrecordsdecryptkey == 'string') && (parent.args.dbrecordsdecryptkey.length != 0)) {
- // Hash the database password into a AES256 key and setup encryption and decryption.
- obj.dbRecordsDecryptKey = parent.crypto.createHash('sha384').update(parent.args.dbrecordsdecryptkey).digest('raw').slice(0, 32);
- }
-
-
- function createTablesIfNotExist(dbname) {
- var useDatabase = 'USE ' + dbname;
- sqlDbQuery(useDatabase, null, function (err, docs) {
- if (err != null) {
- console.log("Unable to connect to database: " + err);
- process.exit();
- }
- if (err == null) {
- parent.debug('db', 'Checking tables...');
- sqlDbBatchExec([
- 'CREATE TABLE IF NOT EXISTS main (id VARCHAR(256) NOT NULL, type CHAR(32), domain CHAR(64), extra CHAR(255), extraex CHAR(255), doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))',
- 'CREATE TABLE IF NOT EXISTS events (id INT NOT NULL AUTO_INCREMENT, time DATETIME, domain CHAR(64), action CHAR(255), nodeid CHAR(255), userid CHAR(255), doc JSON, PRIMARY KEY(id), CHECK(json_valid(doc)))',
- 'CREATE TABLE IF NOT EXISTS eventids (fkid INT NOT NULL, target CHAR(255), CONSTRAINT fk_eventid FOREIGN KEY (fkid) REFERENCES events (id) ON DELETE CASCADE ON UPDATE RESTRICT)',
- 'CREATE TABLE IF NOT EXISTS serverstats (time DATETIME, expire DATETIME, doc JSON, PRIMARY KEY(time), CHECK (json_valid(doc)))',
- 'CREATE TABLE IF NOT EXISTS power (id INT NOT NULL AUTO_INCREMENT, time DATETIME, nodeid CHAR(255), doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))',
- 'CREATE TABLE IF NOT EXISTS smbios (id CHAR(255), time DATETIME, expire DATETIME, doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))',
- 'CREATE TABLE IF NOT EXISTS plugin (id INT NOT NULL AUTO_INCREMENT, doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))'
- ], function (err) {
- parent.debug('db', 'Checking indexes...');
- sqlDbExec('CREATE INDEX ndxtypedomainextra ON main (type, domain, extra)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxextra ON main (extra)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxextraex ON main (extraex)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxeventstime ON events(time)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxeventsusername ON events(domain, userid, time)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxeventsdomainnodeidtime ON events(domain, nodeid, time)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxeventids ON eventids(target)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxserverstattime ON serverstats (time)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxserverstatexpire ON serverstats (expire)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxpowernodeidtime ON power (nodeid, time)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxsmbiostime ON smbios (time)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxsmbiosexpire ON smbios (expire)', null, function (err, response) { });
- setupFunctions(func);
- });
- }
- });
- }
-
- if (parent.args.sqlite3) {
- // SQLite3 database setup
- obj.databaseType = DB_SQLITE;
- const sqlite3 = require('sqlite3');
- let configParams = parent.config.settings.sqlite3;
- if (typeof configParams == 'string') {databaseName = configParams} else {databaseName = configParams.name ? configParams.name : 'meshcentral';};
- obj.sqliteConfig.startupVacuum = configParams.startupvacuum ? configParams.startupvacuum : false;
- obj.sqliteConfig.autoVacuum = configParams.autovacuum ? configParams.autovacuum.toLowerCase() : 'incremental';
- obj.sqliteConfig.incrementalVacuum = configParams.incrementalvacuum ? configParams.incrementalvacuum : 100;
- obj.sqliteConfig.journalMode = configParams.journalmode ? configParams.journalmode.toLowerCase() : 'delete';
- //allowed modes, 'none' excluded because not usefull for this app, maybe also remove 'memory'?
- if (!(['delete', 'truncate', 'persist', 'memory', 'wal'].includes(obj.sqliteConfig.journalMode))) { obj.sqliteConfig.journalMode = 'delete'};
- obj.sqliteConfig.journalSize = configParams.journalsize ? configParams.journalsize : 409600;
- //wal can use the more performant 'normal' mode, see https://www.sqlite.org/pragma.html#pragma_synchronous
- obj.sqliteConfig.synchronous = (obj.sqliteConfig.journalMode == 'wal') ? 'normal' : 'full';
- if (obj.sqliteConfig.journalMode == 'wal') {obj.sqliteConfig.maintenance += 'PRAGMA wal_checkpoint(PASSIVE);'};
- if (obj.sqliteConfig.autoVacuum == 'incremental') {obj.sqliteConfig.maintenance += 'PRAGMA incremental_vacuum(' + obj.sqliteConfig.incrementalVacuum + ');'};
- obj.sqliteConfig.maintenance += 'PRAGMA optimize;';
-
- parent.debug('db', 'SQlite config options: ' + JSON.stringify(obj.sqliteConfig, null, 4));
- if (obj.sqliteConfig.journalMode == 'memory') { console.log('[WARNING] journal_mode=memory: this can lead to database corruption if there is a crash during a transaction. See https://www.sqlite.org/pragma.html#pragma_journal_mode') };
- //.cached not usefull
- obj.file = new sqlite3.Database(path.join(parent.datapath, databaseName + '.sqlite'), sqlite3.OPEN_READWRITE, function (err) {
- if (err && (err.code == 'SQLITE_CANTOPEN')) {
- // Database needs to be created
- obj.file = new sqlite3.Database(path.join(parent.datapath, databaseName + '.sqlite'), function (err) {
- if (err) { console.log("SQLite Error: " + err); process.exit(1); }
- obj.file.exec(`
- CREATE TABLE main (id VARCHAR(256) PRIMARY KEY NOT NULL, type CHAR(32), domain CHAR(64), extra CHAR(255), extraex CHAR(255), doc JSON);
- CREATE TABLE events(id INTEGER PRIMARY KEY, time TIMESTAMP, domain CHAR(64), action CHAR(255), nodeid CHAR(255), userid CHAR(255), doc JSON);
- CREATE TABLE eventids(fkid INT NOT NULL, target CHAR(255), CONSTRAINT fk_eventid FOREIGN KEY (fkid) REFERENCES events (id) ON DELETE CASCADE ON UPDATE RESTRICT);
- CREATE TABLE serverstats (time TIMESTAMP PRIMARY KEY, expire TIMESTAMP, doc JSON);
- CREATE TABLE power (id INTEGER PRIMARY KEY, time TIMESTAMP, nodeid CHAR(255), doc JSON);
- CREATE TABLE smbios (id CHAR(255) PRIMARY KEY, time TIMESTAMP, expire TIMESTAMP, doc JSON);
- CREATE TABLE plugin (id INTEGER PRIMARY KEY, doc JSON);
- CREATE INDEX ndxtypedomainextra ON main (type, domain, extra);
- CREATE INDEX ndxextra ON main (extra);
- CREATE INDEX ndxextraex ON main (extraex);
- CREATE INDEX ndxeventstime ON events(time);
- CREATE INDEX ndxeventsusername ON events(domain, userid, time);
- CREATE INDEX ndxeventsdomainnodeidtime ON events(domain, nodeid, time);
- CREATE INDEX ndxeventids ON eventids(target);
- CREATE INDEX ndxserverstattime ON serverstats (time);
- CREATE INDEX ndxserverstatexpire ON serverstats (expire);
- CREATE INDEX ndxpowernodeidtime ON power (nodeid, time);
- CREATE INDEX ndxsmbiostime ON smbios (time);
- CREATE INDEX ndxsmbiosexpire ON smbios (expire);
- `, function (err) {
- // Completed DB creation of SQLite3
- sqliteSetOptions(func);
- //setupFunctions could be put in the sqliteSetupOptions, but left after it for clarity
- setupFunctions(func);
- }
- );
- });
- return;
- } else if (err) { console.log("SQLite Error: " + err); process.exit(0); }
-
- //for existing db's
- sqliteSetOptions();
- //setupFunctions could be put in the sqliteSetupOptions, but left after it for clarity
- setupFunctions(func);
- });
- } else if (parent.args.acebase) {
- // AceBase database setup
- obj.databaseType = DB_ACEBASE;
- const { AceBase } = require('acebase');
- // For information on AceBase sponsor: https://github.com/appy-one/acebase/discussions/100
- obj.file = new AceBase('meshcentral', { sponsor: ((typeof parent.args.acebase == 'object') && (parent.args.acebase.sponsor)), logLevel: 'error', storage: { path: parent.datapath } });
- // Get all the databases ready
- obj.file.ready(function () {
- // Create AceBase indexes
- obj.file.indexes.create('meshcentral', 'type', { include: ['domain', 'meshid'] });
- obj.file.indexes.create('meshcentral', 'email');
- obj.file.indexes.create('meshcentral', 'meshid');
- obj.file.indexes.create('meshcentral', 'intelamt.uuid');
- obj.file.indexes.create('events', 'userid', { include: ['action'] });
- obj.file.indexes.create('events', 'domain', { include: ['nodeid', 'time'] });
- obj.file.indexes.create('events', 'ids', { include: ['time'] });
- obj.file.indexes.create('events', 'time');
- obj.file.indexes.create('power', 'nodeid', { include: ['time'] });
- obj.file.indexes.create('power', 'time');
- obj.file.indexes.create('stats', 'time');
- obj.file.indexes.create('stats', 'expire');
- // Completed setup of AceBase
- setupFunctions(func);
- });
- } else if (parent.args.mariadb || parent.args.mysql) {
- var connectinArgs = (parent.args.mariadb) ? parent.args.mariadb : parent.args.mysql;
- if (typeof connectinArgs == 'string') {
- const parts = connectinArgs.split(/[:@/]+/);
- var connectionObject = {
- "user": parts[1],
- "password": parts[2],
- "host": parts[3],
- "port": parts[4],
- "database": parts[5]
- };
- var dbname = (connectionObject.database != null) ? connectionObject.database : 'meshcentral';
- } else {
- var dbname = (connectinArgs.database != null) ? connectinArgs.database : 'meshcentral';
-
- // Including the db name in the connection obj will cause a connection faliure if it does not exist
- var connectionObject = Clone(connectinArgs);
- delete connectionObject.database;
-
- try {
- if (connectinArgs.ssl) {
- if (connectinArgs.ssl.dontcheckserveridentity == true) { connectionObject.ssl.checkServerIdentity = function (name, cert) { return undefined; } };
- if (connectinArgs.ssl.cacertpath) { connectionObject.ssl.ca = [require('fs').readFileSync(connectinArgs.ssl.cacertpath, 'utf8')]; }
- if (connectinArgs.ssl.clientcertpath) { connectionObject.ssl.cert = [require('fs').readFileSync(connectinArgs.ssl.clientcertpath, 'utf8')]; }
- if (connectinArgs.ssl.clientkeypath) { connectionObject.ssl.key = [require('fs').readFileSync(connectinArgs.ssl.clientkeypath, 'utf8')]; }
- }
- } catch (ex) {
- console.log('Error loading SQL Connector certificate: ' + ex);
- process.exit();
- }
- }
-
- if (parent.args.mariadb) {
- // Use MariaDB
- obj.databaseType = DB_MARIADB;
- var tempDatastore = require('mariadb').createPool(connectionObject);
- tempDatastore.getConnection().then(function (conn) {
- conn.query('CREATE DATABASE IF NOT EXISTS ' + dbname).then(function (result) {
- conn.release();
- }).catch(function (ex) { console.log('Auto-create database failed: ' + ex); });
- }).catch(function (ex) { console.log('Auto-create database failed: ' + ex); });
- setTimeout(function () { tempDatastore.end(); }, 2000);
-
- connectionObject.database = dbname;
- Datastore = require('mariadb').createPool(connectionObject);
- createTablesIfNotExist(dbname);
- } else if (parent.args.mysql) {
- // Use MySQL
- obj.databaseType = DB_MYSQL;
- var tempDatastore = require('mysql2').createPool(connectionObject);
- tempDatastore.query('CREATE DATABASE IF NOT EXISTS ' + dbname, function (error) {
- if (error != null) {
- console.log('Auto-create database failed: ' + error);
- }
- connectionObject.database = dbname;
- Datastore = require('mysql2').createPool(connectionObject);
- createTablesIfNotExist(dbname);
- });
- setTimeout(function () { tempDatastore.end(); }, 2000);
- }
- } else if (parent.args.postgres) {
- // Postgres SQL
- let connectinArgs = parent.args.postgres;
- connectinArgs.database = (databaseName = (connectinArgs.database != null) ? connectinArgs.database : 'meshcentral');
-
- let DatastoreTest;
- obj.databaseType = DB_POSTGRESQL;
- const { Client } = require('pg');
- Datastore = new Client(connectinArgs);
- //Connect to and check pg db first to check if own db exists. Otherwise errors out on 'database does not exist'
- connectinArgs.database = 'postgres';
- DatastoreTest = new Client(connectinArgs);
- DatastoreTest.connect();
- connectinArgs.database = databaseName; //put the name back for backupconfig info
- DatastoreTest.query('SELECT 1 FROM pg_catalog.pg_database WHERE datname = $1', [databaseName], function (err, res) { // check database exists first before creating
- if (res.rowCount != 0) { // database exists now check tables exists
- DatastoreTest.end();
- Datastore.connect();
- Datastore.query('SELECT doc FROM main WHERE id = $1', ['DatabaseIdentifier'], function (err, res) {
- if (err == null) {
- (res.rowCount ==0) ? postgreSqlCreateTables(func) : setupFunctions(func)
- } else
- if (err.code == '42P01') { //42P01 = undefined table, https://www.postgresql.org/docs/current/errcodes-appendix.html
- postgreSqlCreateTables(func);
- } else {
- console.log('Postgresql database exists, other error: ', err.message); process.exit(0);
- };
- });
- } else { // If not present, create the tables and indexes
- //not needed, just use a create db statement: const pgtools = require('pgtools');
- DatastoreTest.query('CREATE DATABASE '+ databaseName + ';', [], function (err, res) {
- if (err == null) {
- // Create the tables and indexes
- DatastoreTest.end();
- Datastore.connect();
- postgreSqlCreateTables(func);
- } else {
- console.log('Postgresql database create error: ', err.message);
- process.exit(0);
- }
- });
- }
- });
- } else if (parent.args.mongodb) {
- // Use MongoDB
- obj.databaseType = DB_MONGODB;
-
- // If running an older NodeJS version, TextEncoder/TextDecoder is required
- if (global.TextEncoder == null) { global.TextEncoder = require('util').TextEncoder; }
- if (global.TextDecoder == null) { global.TextDecoder = require('util').TextDecoder; }
-
- require('mongodb').MongoClient.connect(parent.args.mongodb, { useNewUrlParser: true, useUnifiedTopology: true, enableUtf8Validation: false }, function (err, client) {
- if (err != null) { console.log("Unable to connect to database: " + err); process.exit(); return; }
- Datastore = client;
- parent.debug('db', 'Connected to MongoDB database...');
-
- // Get the database name and setup the database client
- var dbname = 'meshcentral';
- if (parent.args.mongodbname) { dbname = parent.args.mongodbname; }
- const dbcollectionname = (parent.args.mongodbcol) ? (parent.args.mongodbcol) : 'meshcentral';
- const db = client.db(dbname);
-
- // Check the database version
- db.admin().serverInfo(function (err, info) {
- if ((err != null) || (info == null) || (info.versionArray == null) || (Array.isArray(info.versionArray) == false) || (info.versionArray.length < 2) || (typeof info.versionArray[0] != 'number') || (typeof info.versionArray[1] != 'number')) {
- console.log('WARNING: Unable to check MongoDB version.');
- } else {
- if ((info.versionArray[0] < 3) || ((info.versionArray[0] == 3) && (info.versionArray[1] < 6))) {
- // We are running with mongoDB older than 3.6, this is not good.
- parent.addServerWarning("Current version of MongoDB (" + info.version + ") is too old, please upgrade to MongoDB 3.6 or better.", true);
- }
- }
- });
-
- // Setup MongoDB main collection and indexes
- obj.file = db.collection(dbcollectionname);
- obj.file.indexes(function (err, indexes) {
- // Check if we need to reset indexes
- var indexesByName = {}, indexCount = 0;
- for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
- if ((indexCount != 5) || (indexesByName['TypeDomainMesh1'] == null) || (indexesByName['TypeDomainRnameIndex1'] == null) || (indexesByName['Email1'] == null) || (indexesByName['Mesh1'] == null) || (indexesByName['AmtUuid1'] == null)) {
- console.log('Resetting main indexes...');
- obj.file.dropIndexes(function (err) {
- obj.file.createIndex({ type: 1, domain: 1, meshid: 1 }, { sparse: 1, name: 'TypeDomainMesh1' }); // Speeds up GetAllTypeNoTypeField() and GetAllTypeNoTypeFieldMeshFiltered()
- obj.file.createIndex({ type: 1, domain: 1, rname: 1 }, { sparse: 1, name: "TypeDomainRnameIndex1" }); // Speeds up
- obj.file.createIndex({ email: 1 }, { sparse: 1, name: 'Email1' }); // Speeds up GetUserWithEmail() and GetUserWithVerifiedEmail()
- obj.file.createIndex({ meshid: 1 }, { sparse: 1, name: 'Mesh1' }); // Speeds up RemoveMesh()
- obj.file.createIndex({ 'intelamt.uuid': 1 }, { sparse: 1, name: 'AmtUuid1' }); // Speeds up getAmtUuidMeshNode()
- });
- }
- });
-
- // Setup the changeStream on the MongoDB main collection if possible
- if (parent.args.mongodbchangestream == true) {
- obj.dbCounters.changeStream = { change: 0, update: 0, insert: 0, delete: 0 };
- if (typeof obj.file.watch != 'function') {
- console.log('WARNING: watch() is not a function, MongoDB ChangeStream not supported.');
- } else {
- obj.fileChangeStream = obj.file.watch([{ $match: { $or: [{ 'fullDocument.type': { $in: ['node', 'mesh', 'user', 'ugrp'] } }, { 'operationType': 'delete' }] } }], { fullDocument: 'updateLookup' });
- obj.fileChangeStream.on('change', function (change) {
- obj.dbCounters.changeStream.change++;
- if ((change.operationType == 'update') || (change.operationType == 'replace')) {
- obj.dbCounters.changeStream.update++;
- switch (change.fullDocument.type) {
- case 'node': { dbNodeChange(change, false); break; } // A node has changed
- case 'mesh': { dbMeshChange(change, false); break; } // A device group has changed
- case 'user': { dbUserChange(change, false); break; } // A user account has changed
- case 'ugrp': { dbUGrpChange(change, false); break; } // A user account has changed
- }
- } else if (change.operationType == 'insert') {
- obj.dbCounters.changeStream.insert++;
- switch (change.fullDocument.type) {
- case 'node': { dbNodeChange(change, true); break; } // A node has added
- case 'mesh': { dbMeshChange(change, true); break; } // A device group has created
- case 'user': { dbUserChange(change, true); break; } // A user account has created
- case 'ugrp': { dbUGrpChange(change, true); break; } // A user account has created
- }
- } else if (change.operationType == 'delete') {
- obj.dbCounters.changeStream.delete++;
- if ((change.documentKey == null) || (change.documentKey._id == null)) return;
- var splitId = change.documentKey._id.split('/');
- switch (splitId[0]) {
- case 'node': {
- //Not Good: Problem here is that we don't know what meshid the node belonged to before the delete.
- //parent.DispatchEvent(['*', node.meshid], obj, { etype: 'node', action: 'removenode', nodeid: change.documentKey._id, domain: splitId[1] });
- break;
- }
- case 'mesh': {
- parent.DispatchEvent(['*', change.documentKey._id], obj, { etype: 'mesh', action: 'deletemesh', meshid: change.documentKey._id, domain: splitId[1] });
- break;
- }
- case 'user': {
- //Not Good: This is not a perfect user removal because we don't know what groups the user was in.
- //parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', action: 'accountremove', userid: change.documentKey._id, domain: splitId[1], username: splitId[2] });
- break;
- }
- case 'ugrp': {
- parent.DispatchEvent(['*', change.documentKey._id], obj, { etype: 'ugrp', action: 'deleteusergroup', ugrpid: change.documentKey._id, domain: splitId[1] });
- break;
- }
- }
- }
- });
- obj.changeStream = true;
- }
- }
-
- // Setup MongoDB events collection and indexes
- obj.eventsfile = db.collection('events'); // Collection containing all events
- obj.eventsfile.indexes(function (err, indexes) {
- // Check if we need to reset indexes
- var indexesByName = {}, indexCount = 0;
- for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
- if ((indexCount != 5) || (indexesByName['UseridAction1'] == null) || (indexesByName['DomainNodeTime1'] == null) || (indexesByName['IdsAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
- // Reset all indexes
- console.log("Resetting events indexes...");
- obj.eventsfile.dropIndexes(function (err) {
- obj.eventsfile.createIndex({ userid: 1, action: 1 }, { sparse: 1, name: 'UseridAction1' });
- obj.eventsfile.createIndex({ domain: 1, nodeid: 1, time: -1 }, { sparse: 1, name: 'DomainNodeTime1' });
- obj.eventsfile.createIndex({ ids: 1, time: -1 }, { sparse: 1, name: 'IdsAndTime1' });
- obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
- });
- } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireEventsSeconds) {
- // Reset the timeout index
- console.log("Resetting events expire index...");
- obj.eventsfile.dropIndex('ExpireTime1', function (err) {
- obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
- });
- }
- });
-
- // Setup MongoDB power events collection and indexes
- obj.powerfile = db.collection('power'); // Collection containing all power events
- obj.powerfile.indexes(function (err, indexes) {
- // Check if we need to reset indexes
- var indexesByName = {}, indexCount = 0;
- for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
- if ((indexCount != 3) || (indexesByName['NodeIdAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
- // Reset all indexes
- console.log("Resetting power events indexes...");
- obj.powerfile.dropIndexes(function (err) {
- // Create all indexes
- obj.powerfile.createIndex({ nodeid: 1, time: 1 }, { sparse: 1, name: 'NodeIdAndTime1' });
- obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
- });
- } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expirePowerEventsSeconds) {
- // Reset the timeout index
- console.log("Resetting power events expire index...");
- obj.powerfile.dropIndex('ExpireTime1', function (err) {
- // Reset the expire power events index
- obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
- });
- }
- });
-
- // Setup MongoDB smbios collection, no indexes needed
- obj.smbiosfile = db.collection('smbios'); // Collection containing all smbios information
-
- // Setup MongoDB server stats collection
- obj.serverstatsfile = db.collection('serverstats'); // Collection of server stats
- obj.serverstatsfile.indexes(function (err, indexes) {
- // Check if we need to reset indexes
- var indexesByName = {}, indexCount = 0;
- for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
- if ((indexCount != 3) || (indexesByName['ExpireTime1'] == null)) {
- // Reset all indexes
- console.log("Resetting server stats indexes...");
- obj.serverstatsfile.dropIndexes(function (err) {
- // Create all indexes
- obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
- obj.serverstatsfile.createIndex({ 'expire': 1 }, { expireAfterSeconds: 0, name: 'ExpireTime2' }); // Auto-expire events
- });
- } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireServerStatsSeconds) {
- // Reset the timeout index
- console.log("Resetting server stats expire index...");
- obj.serverstatsfile.dropIndex('ExpireTime1', function (err) {
- // Reset the expire server stats index
- obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
- });
- }
- });
-
- // Setup plugin info collection
- if (obj.pluginsActive) { obj.pluginsfile = db.collection('plugins'); }
-
- setupFunctions(func); // Completed setup of MongoDB
- });
- } else if (parent.args.xmongodb) {
- // Use MongoJS, this is the old system.
- obj.databaseType = DB_MONGOJS;
- Datastore = require('mongojs');
- var db = Datastore(parent.args.xmongodb);
- var dbcollection = 'meshcentral';
- if (parent.args.mongodbcol) { dbcollection = parent.args.mongodbcol; }
-
- // Setup MongoDB main collection and indexes
- obj.file = db.collection(dbcollection);
- obj.file.getIndexes(function (err, indexes) {
- // Check if we need to reset indexes
- var indexesByName = {}, indexCount = 0;
- for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
- if ((indexCount != 5) || (indexesByName['TypeDomainMesh1'] == null) || (indexesByName['TypeDomainRnameIndex1'] == null) || (indexesByName['Email1'] == null) || (indexesByName['Mesh1'] == null) || (indexesByName['AmtUuid1'] == null)) {
- console.log("Resetting main indexes...");
- obj.file.dropIndexes(function (err) {
- obj.file.createIndex({ type: 1, domain: 1, meshid: 1 }, { sparse: 1, name: 'TypeDomainMesh1' }); // Speeds up GetAllTypeNoTypeField() and GetAllTypeNoTypeFieldMeshFiltered()
- obj.file.createIndex({ type: 1, domain: 1, rname: 1 }, { sparse: 1, name: "TypeDomainRnameIndex1" });
- obj.file.createIndex({ email: 1 }, { sparse: 1, name: 'Email1' }); // Speeds up GetUserWithEmail() and GetUserWithVerifiedEmail()
- obj.file.createIndex({ meshid: 1 }, { sparse: 1, name: 'Mesh1' }); // Speeds up RemoveMesh()
- obj.file.createIndex({ 'intelamt.uuid': 1 }, { sparse: 1, name: 'AmtUuid1' }); // Speeds up getAmtUuidMeshNode()
- });
- }
- });
-
- // Setup MongoDB events collection and indexes
- obj.eventsfile = db.collection('events'); // Collection containing all events
- obj.eventsfile.getIndexes(function (err, indexes) {
- // Check if we need to reset indexes
- var indexesByName = {}, indexCount = 0;
- for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
- if ((indexCount != 5) || (indexesByName['UseridAction1'] == null) || (indexesByName['DomainNodeTime1'] == null) || (indexesByName['IdsAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
- // Reset all indexes
- console.log("Resetting events indexes...");
- obj.eventsfile.dropIndexes(function (err) {
- obj.eventsfile.createIndex({ userid: 1, action: 1 }, { sparse: 1, name: 'UseridAction1' });
- obj.eventsfile.createIndex({ domain: 1, nodeid: 1, time: -1 }, { sparse: 1, name: 'DomainNodeTime1' });
- obj.eventsfile.createIndex({ ids: 1, time: -1 }, { sparse: 1, name: 'IdsAndTime1' });
- obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
- });
- } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireEventsSeconds) {
- // Reset the timeout index
- console.log("Resetting events expire index...");
- obj.eventsfile.dropIndex('ExpireTime1', function (err) {
- obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
- });
- }
- });
-
- // Setup MongoDB power events collection and indexes
- obj.powerfile = db.collection('power'); // Collection containing all power events
- obj.powerfile.getIndexes(function (err, indexes) {
- // Check if we need to reset indexes
- var indexesByName = {}, indexCount = 0;
- for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
- if ((indexCount != 3) || (indexesByName['NodeIdAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
- // Reset all indexes
- console.log("Resetting power events indexes...");
- obj.powerfile.dropIndexes(function (err) {
- // Create all indexes
- obj.powerfile.createIndex({ nodeid: 1, time: 1 }, { sparse: 1, name: 'NodeIdAndTime1' });
- obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
- });
- } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expirePowerEventsSeconds) {
- // Reset the timeout index
- console.log("Resetting power events expire index...");
- obj.powerfile.dropIndex('ExpireTime1', function (err) {
- // Reset the expire power events index
- obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
- });
- }
- });
-
- // Setup MongoDB smbios collection, no indexes needed
- obj.smbiosfile = db.collection('smbios'); // Collection containing all smbios information
-
- // Setup MongoDB server stats collection
- obj.serverstatsfile = db.collection('serverstats'); // Collection of server stats
- obj.serverstatsfile.getIndexes(function (err, indexes) {
- // Check if we need to reset indexes
- var indexesByName = {}, indexCount = 0;
- for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
- if ((indexCount != 3) || (indexesByName['ExpireTime1'] == null)) {
- // Reset all indexes
- console.log("Resetting server stats indexes...");
- obj.serverstatsfile.dropIndexes(function (err) {
- // Create all indexes
- obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
- obj.serverstatsfile.createIndex({ 'expire': 1 }, { expireAfterSeconds: 0, name: 'ExpireTime2' }); // Auto-expire events
- });
- } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireServerStatsSeconds) {
- // Reset the timeout index
- console.log("Resetting server stats expire index...");
- obj.serverstatsfile.dropIndex('ExpireTime1', function (err) {
- // Reset the expire server stats index
- obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
- });
- }
- });
-
- // Setup plugin info collection
- if (obj.pluginsActive) { obj.pluginsfile = db.collection('plugins'); }
-
- setupFunctions(func); // Completed setup of MongoJS
- } else {
- // Use NeDB (The default)
- obj.databaseType = DB_NEDB;
- try { Datastore = require('@seald-io/nedb'); } catch (ex) { } // This is the NeDB with Node 23 support.
- if (Datastore == null) {
- try { Datastore = require('@yetzt/nedb'); } catch (ex) { } // This is the NeDB with fixed security dependencies.
- if (Datastore == null) { Datastore = require('nedb'); } // So not to break any existing installations, if the old NeDB is present, use it.
- }
- var datastoreOptions = { filename: parent.getConfigFilePath('meshcentral.db'), autoload: true };
-
- // If a DB encryption key is provided, perform database encryption
- if ((typeof parent.args.dbencryptkey == 'string') && (parent.args.dbencryptkey.length != 0)) {
- // Hash the database password into a AES256 key and setup encryption and decryption.
- obj.dbKey = parent.crypto.createHash('sha384').update(parent.args.dbencryptkey).digest('raw').slice(0, 32);
- datastoreOptions.afterSerialization = function (plaintext) {
- const iv = parent.crypto.randomBytes(16);
- const aes = parent.crypto.createCipheriv('aes-256-cbc', obj.dbKey, iv);
- var ciphertext = aes.update(plaintext);
- ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
- return ciphertext.toString('base64');
- }
- datastoreOptions.beforeDeserialization = function (ciphertext) {
- const ciphertextBytes = Buffer.from(ciphertext, 'base64');
- const iv = ciphertextBytes.slice(0, 16);
- const data = ciphertextBytes.slice(16);
- const aes = parent.crypto.createDecipheriv('aes-256-cbc', obj.dbKey, iv);
- var plaintextBytes = Buffer.from(aes.update(data));
- plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
- return plaintextBytes.toString();
- }
- }
-
- // Start NeDB main collection and setup indexes
- obj.file = new Datastore(datastoreOptions);
- obj.file.setAutocompactionInterval(86400000); // Compact once a day
- obj.file.ensureIndex({ fieldName: 'type' });
- obj.file.ensureIndex({ fieldName: 'domain' });
- obj.file.ensureIndex({ fieldName: 'meshid', sparse: true });
- obj.file.ensureIndex({ fieldName: 'nodeid', sparse: true });
- obj.file.ensureIndex({ fieldName: 'email', sparse: true });
-
- // Setup the events collection and setup indexes
- obj.eventsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-events.db'), autoload: true, corruptAlertThreshold: 1 });
- obj.eventsfile.setAutocompactionInterval(86400000); // Compact once a day
- obj.eventsfile.ensureIndex({ fieldName: 'ids' }); // TODO: Not sure if this is a good index, this is a array field.
- obj.eventsfile.ensureIndex({ fieldName: 'nodeid', sparse: true });
- obj.eventsfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: expireEventsSeconds });
- obj.eventsfile.remove({ time: { '$lt': new Date(Date.now() - (expireEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
-
- // Setup the power collection and setup indexes
- obj.powerfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-power.db'), autoload: true, corruptAlertThreshold: 1 });
- obj.powerfile.setAutocompactionInterval(86400000); // Compact once a day
- obj.powerfile.ensureIndex({ fieldName: 'nodeid' });
- obj.powerfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: expirePowerEventsSeconds });
- obj.powerfile.remove({ time: { '$lt': new Date(Date.now() - (expirePowerEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
-
- // Setup the SMBIOS collection, for NeDB we don't setup SMBIOS since NeDB will corrupt the database. Remove any existing ones.
- //obj.smbiosfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-smbios.db'), autoload: true, corruptAlertThreshold: 1 });
- fs.unlink(parent.getConfigFilePath('meshcentral-smbios.db'), function () { });
-
- // Setup the server stats collection and setup indexes
- obj.serverstatsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-stats.db'), autoload: true, corruptAlertThreshold: 1 });
- obj.serverstatsfile.setAutocompactionInterval(86400000); // Compact once a day
- obj.serverstatsfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: expireServerStatsSeconds });
- obj.serverstatsfile.ensureIndex({ fieldName: 'expire', expireAfterSeconds: 0 }); // Auto-expire events
- obj.serverstatsfile.remove({ time: { '$lt': new Date(Date.now() - (expireServerStatsSeconds * 1000)) } }, { multi: true }); // Force delete older events
-
- // Setup plugin info collection
- if (obj.pluginsActive) {
- obj.pluginsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-plugins.db'), autoload: true });
- obj.pluginsfile.setAutocompactionInterval(86400000); // Compact once a day
- }
-
- setupFunctions(func); // Completed setup of NeDB
- }
-
- function sqliteSetOptions(func) {
- //get current auto_vacuum mode for comparison
- obj.file.get('PRAGMA auto_vacuum;', function(err, current){
- let pragma = 'PRAGMA journal_mode=' + obj.sqliteConfig.journalMode + ';' +
- 'PRAGMA synchronous='+ obj.sqliteConfig.synchronous + ';' +
- 'PRAGMA journal_size_limit=' + obj.sqliteConfig.journalSize + ';' +
- 'PRAGMA auto_vacuum=' + obj.sqliteConfig.autoVacuum + ';' +
- 'PRAGMA incremental_vacuum=' + obj.sqliteConfig.incrementalVacuum + ';' +
- 'PRAGMA optimize=0x10002;';
- //check new autovacuum mode, if changing from or to 'none', a VACUUM needs to be done to activate it. See https://www.sqlite.org/pragma.html#pragma_auto_vacuum
- if ( obj.sqliteConfig.startupVacuum
- || (current.auto_vacuum == 0 && obj.sqliteConfig.autoVacuum !='none')
- || (current.auto_vacuum != 0 && obj.sqliteConfig.autoVacuum =='none'))
- {
- pragma += 'VACUUM;';
- };
- parent.debug ('db', 'Config statement: ' + pragma);
-
- obj.file.exec( pragma,
- function (err) {
- if (err) { parent.debug('db', 'Config pragma error: ' + (err.message)) };
- sqliteGetPragmas(['journal_mode', 'journal_size_limit', 'freelist_count', 'auto_vacuum', 'page_size', 'wal_autocheckpoint', 'synchronous'], function (pragma, pragmaValue) {
- parent.debug('db', 'PRAGMA: ' + pragma + '=' + pragmaValue);
- });
- });
- });
- //setupFunctions(func);
- }
-
- function sqliteGetPragmas (pragmas, func){
- //pragmas can only be gotting one by one
- pragmas.forEach (function (pragma) {
- obj.file.get('PRAGMA ' + pragma + ';', function(err, res){
- if (pragma == 'auto_vacuum') { res[pragma] = SQLITE_AUTOVACUUM[res[pragma]] };
- if (pragma == 'synchronous') { res[pragma] = SQLITE_SYNCHRONOUS[res[pragma]] };
- if (func) { func (pragma, res[pragma]); }
- });
- });
- }
- // Create the PostgreSQL tables
- function postgreSqlCreateTables(func) {
- // Database was created, create the tables
- parent.debug('db', 'Creating tables...');
- sqlDbBatchExec([
- 'CREATE TABLE IF NOT EXISTS main (id VARCHAR(256) PRIMARY KEY NOT NULL, type CHAR(32), domain CHAR(64), extra CHAR(255), extraex CHAR(255), doc JSON)',
- 'CREATE TABLE IF NOT EXISTS events(id SERIAL PRIMARY KEY, time TIMESTAMP, domain CHAR(64), action CHAR(255), nodeid CHAR(255), userid CHAR(255), doc JSON)',
- 'CREATE TABLE IF NOT EXISTS eventids(fkid INT NOT NULL, target CHAR(255), CONSTRAINT fk_eventid FOREIGN KEY (fkid) REFERENCES events (id) ON DELETE CASCADE ON UPDATE RESTRICT)',
- 'CREATE TABLE IF NOT EXISTS serverstats (time TIMESTAMP PRIMARY KEY, expire TIMESTAMP, doc JSON)',
- 'CREATE TABLE IF NOT EXISTS power (id SERIAL PRIMARY KEY, time TIMESTAMP, nodeid CHAR(255), doc JSON)',
- 'CREATE TABLE IF NOT EXISTS smbios (id CHAR(255) PRIMARY KEY, time TIMESTAMP, expire TIMESTAMP, doc JSON)',
- 'CREATE TABLE IF NOT EXISTS plugin (id SERIAL PRIMARY KEY, doc JSON)'
- ], function (results) {
- parent.debug('db', 'Creating indexes...');
- sqlDbExec('CREATE INDEX ndxtypedomainextra ON main (type, domain, extra)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxextra ON main (extra)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxextraex ON main (extraex)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxeventstime ON events(time)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxeventsusername ON events(domain, userid, time)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxeventsdomainnodeidtime ON events(domain, nodeid, time)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxeventids ON eventids(target)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxserverstattime ON serverstats (time)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxserverstatexpire ON serverstats (expire)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxpowernodeidtime ON power (nodeid, time)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxsmbiostime ON smbios (time)', null, function (err, response) { });
- sqlDbExec('CREATE INDEX ndxsmbiosexpire ON smbios (expire)', null, function (err, response) { });
- setupFunctions(func);
- });
- }
-
- // Check the object names for a "."
- function checkObjectNames(r, tag) {
- if (typeof r != 'object') return;
- for (var i in r) {
- if (i.indexOf('.') >= 0) { throw ('BadDbName (' + tag + '): ' + JSON.stringify(r)); }
- checkObjectNames(r[i], tag);
- }
- }
-
- // Query the database
- function sqlDbQuery(query, args, func, debug) {
- if (obj.databaseType == DB_SQLITE) { // SQLite
- if (args == null) { args = []; }
- obj.file.all(query, args, function (err, docs) {
- if (err != null) { console.log(query, args, err, docs); }
- if (docs != null) {
- for (var i in docs) {
- if (typeof docs[i].doc == 'string') {
- try { docs[i] = JSON.parse(docs[i].doc); } catch (ex) {
- console.log(query, args, docs[i]);
- }
- }
- }
- }
- if (func) { func(err, docs); }
- });
- } else if (obj.databaseType == DB_MARIADB) { // MariaDB
- Datastore.getConnection()
- .then(function (conn) {
- conn.query(query, args)
- .then(function (rows) {
- conn.release();
- var docs = [];
- for (var i in rows) {
- if (rows[i].doc) {
- docs.push(performTypedRecordDecrypt((typeof rows[i].doc == 'object') ? rows[i].doc : JSON.parse(rows[i].doc)));
- } else if ((rows.length == 1) && (rows[i]['COUNT(doc)'] != null)) {
- // This is a SELECT COUNT() operation
- docs = parseInt(rows[i]['COUNT(doc)']);
- }
- }
- if (func) try { func(null, docs); } catch (ex) { console.log('SQLERR1', ex); }
- })
- .catch(function (err) { conn.release(); if (func) try { func(err); } catch (ex) { console.log('SQLERR2', ex); } });
- }).catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log('SQLERR3', ex); } } });
- } else if (obj.databaseType == DB_MYSQL) { // MySQL
- Datastore.query(query, args, function (error, results, fields) {
- if (error != null) {
- if (func) try { func(error); } catch (ex) { console.log('SQLERR4', ex); }
- } else {
- var docs = [];
- for (var i in results) {
- if (results[i].doc) {
- if (typeof results[i].doc == 'string') {
- docs.push(JSON.parse(results[i].doc));
- } else {
- docs.push(results[i].doc);
- }
- } else if ((results.length == 1) && (results[i]['COUNT(doc)'] != null)) {
- // This is a SELECT COUNT() operation
- docs = results[i]['COUNT(doc)'];
- }
- }
- if (func) { try { func(null, docs); } catch (ex) { console.log('SQLERR5', ex); } }
- }
- });
- } else if (obj.databaseType == DB_POSTGRESQL) { // Postgres SQL
- Datastore.query(query, args, function (error, results) {
- if (error != null) {
- if (func) try { func(error); } catch (ex) { console.log('SQLERR4', ex); }
- } else {
- var docs = [];
- if ((results.command == 'INSERT') && (results.rows != null) && (results.rows.length == 1)) { docs = results.rows[0]; }
- else if (results.command == 'SELECT') {
- for (var i in results.rows) {
- if (results.rows[i].doc) {
- if (typeof results.rows[i].doc == 'string') {
- docs.push(JSON.parse(results.rows[i].doc));
- } else {
- docs.push(results.rows[i].doc);
- }
- } else if (results.rows[i].count && (results.rows.length == 1)) {
- // This is a SELECT COUNT() operation
- docs = parseInt(results.rows[i].count);
- }
- }
- }
- if (func) { try { func(null, docs, results); } catch (ex) { console.log('SQLERR5', ex); } }
- }
- });
- }
- }
-
- // Exec on the database
- function sqlDbExec(query, args, func) {
- if (obj.databaseType == DB_MARIADB) { // MariaDB
- Datastore.getConnection()
- .then(function (conn) {
- conn.query(query, args)
- .then(function (rows) {
- conn.release();
- if (func) try { func(null, rows[0]); } catch (ex) { console.log(ex); }
- })
- .catch(function (err) { conn.release(); if (func) try { func(err); } catch (ex) { console.log(ex); } });
- }).catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } });
- } else if ((obj.databaseType == DB_MYSQL) || (obj.databaseType == DB_POSTGRESQL)) { // MySQL or Postgres SQL
- Datastore.query(query, args, function (error, results, fields) {
- if (func) try { func(error, results ? results[0] : null); } catch (ex) { console.log(ex); }
- });
- }
- }
-
- // Execute a batch of commands on the database
- function sqlDbBatchExec(queries, func) {
- if (obj.databaseType == DB_MARIADB) { // MariaDB
- Datastore.getConnection()
- .then(function (conn) {
- var Promises = [];
- for (var i in queries) { if (typeof queries[i] == 'string') { Promises.push(conn.query(queries[i])); } else { Promises.push(conn.query(queries[i][0], queries[i][1])); } }
- Promise.all(Promises)
- .then(function (rows) { conn.release(); if (func) { try { func(null); } catch (ex) { console.log(ex); } } })
- .catch(function (err) { conn.release(); if (func) { try { func(err); } catch (ex) { console.log(ex); } } });
- })
- .catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } });
- } else if (obj.databaseType == DB_MYSQL) { // MySQL
- Datastore.getConnection(function(err, connection) {
- if (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } return; }
- var Promises = [];
- for (var i in queries) { if (typeof queries[i] == 'string') { Promises.push(connection.promise().query(queries[i])); } else { Promises.push(connection.promise().query(queries[i][0], queries[i][1])); } }
- Promise.all(Promises)
- .then(function (error, results, fields) { connection.release(); if (func) { try { func(error, results); } catch (ex) { console.log(ex); } } })
- .catch(function (error, results, fields) { connection.release(); if (func) { try { func(error); } catch (ex) { console.log(ex); } } });
- });
- } else if (obj.databaseType == DB_POSTGRESQL) { // Postgres
- var Promises = [];
- for (var i in queries) { if (typeof queries[i] == 'string') { Promises.push(Datastore.query(queries[i])); } else { Promises.push(Datastore.query(queries[i][0], queries[i][1])); } }
- Promise.all(Promises)
- .then(function (error, results, fields) { if (func) { try { func(error, results); } catch (ex) { console.log(ex); } } })
- .catch(function (error, results, fields) { if (func) { try { func(error); } catch (ex) { console.log(ex); } } });
- }
- }
-
- function setupFunctions(func) {
- if (obj.databaseType == DB_SQLITE) {
- // Database actions on the main collection. SQLite3: https://www.linode.com/docs/guides/getting-started-with-nodejs-sqlite/
- obj.Set = function (value, func) {
- obj.dbCounters.fileSet++;
- var extra = null, extraex = null;
- value = common.escapeLinksFieldNameEx(value);
- if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
- if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
- if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
- sqlDbQuery('INSERT INTO main VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET type = $2, domain = $3, extra = $4, extraex = $5, doc = $6;', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, JSON.stringify(performTypedRecordEncrypt(value))], func);
- }
- obj.SetRaw = function (value, func) {
- obj.dbCounters.fileSet++;
- var extra = null, extraex = null;
- if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
- if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
- if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
- sqlDbQuery('INSERT INTO main VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET type = $2, domain = $3, extra = $4, extraex = $5, doc = $6;', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, JSON.stringify(performTypedRecordEncrypt(value))], func);
- }
- obj.Get = function (_id, func) {
- sqlDbQuery('SELECT doc FROM main WHERE id = $1', [_id], function (err, docs) {
- if ((docs != null) && (docs.length > 0)) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- }
- obj.GetAll = function (func) {
- sqlDbQuery('SELECT domain, doc FROM main', null, function (err, docs) {
- if ((docs != null) && (docs.length > 0)) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- }
- obj.GetHash = function (id, func) {
- sqlDbQuery('SELECT doc FROM main WHERE id = $1', [id], function (err, docs) {
- if ((docs != null) && (docs.length > 0)) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- }
- obj.GetAllTypeNoTypeField = function (type, domain, func) {
- sqlDbQuery('SELECT doc FROM main WHERE type = $1 AND domain = $2', [type, domain], function (err, docs) {
- if ((docs != null) && (docs.length > 0)) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- };
- obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
- if (limit == 0) { limit = -1; } // In SQLite, no limit is -1
- if (id && (id != '')) {
- sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra IN (' + dbMergeSqlArray(meshes) + ')) LIMIT $4 OFFSET $5', [id, type, domain, limit, skip], function (err, docs) {
- if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- } else {
- if (extrasids == null) {
- sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra IN (' + dbMergeSqlArray(meshes) + ')) LIMIT $3 OFFSET $4', [type, domain, limit, skip], function (err, docs) {
- if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- } else {
- sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra IN (' + dbMergeSqlArray(meshes) + ')) OR (id IN (' + dbMergeSqlArray(extrasids) + '))) LIMIT $3 OFFSET $4', [type, domain, limit, skip], function (err, docs) {
- if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- }
- }
- };
- obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
- if (id && (id != '')) {
- sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra IN (' + dbMergeSqlArray(meshes) + '))', [id, type, domain], function (err, docs) {
- func(err, (err == null) ? docs[0]['COUNT(doc)'] : null);
- });
- } else {
- if (extrasids == null) {
- sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (type = $1) AND (domain = $2) AND (extra IN (' + dbMergeSqlArray(meshes) + '))', [type, domain], function (err, docs) {
- func(err, (err == null) ? docs[0]['COUNT(doc)'] : null);
- });
- } else {
- sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (type = $1) AND (domain = $2) AND ((extra IN (' + dbMergeSqlArray(meshes) + ')) OR (id IN (' + dbMergeSqlArray(extrasids) + ')))', [type, domain], function (err, docs) {
- func(err, (err == null) ? docs[0]['COUNT(doc)'] : null);
- });
- }
- }
- };
- obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
- if (id && (id != '')) {
- sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra IN (' + dbMergeSqlArray(nodes) + '))', [id, type, domain], function (err, docs) {
- if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- } else {
- sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra IN (' + dbMergeSqlArray(nodes) + '))', [type, domain], function (err, docs) {
- if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- }
- };
- obj.GetAllType = function (type, func) {
- sqlDbQuery('SELECT doc FROM main WHERE type = $1', [type], function (err, docs) {
- if (docs != null) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- }
- obj.GetAllIdsOfType = function (ids, domain, type, func) {
- sqlDbQuery('SELECT doc FROM main WHERE (id IN (' + dbMergeSqlArray(ids) + ')) AND domain = $1 AND type = $2', [domain, type], function (err, docs) {
- if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- }
- obj.GetUserWithEmail = function (domain, email, func) {
- sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extra = $2', [domain, 'email/' + email], function (err, docs) {
- if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- }
- obj.GetUserWithVerifiedEmail = function (domain, email, func) {
- sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extra = $2', [domain, 'email/' + email], function (err, docs) {
- if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, performTypedRecordDecrypt(docs));
- });
- }
- obj.GetNodeByComputerName = function (domain, rname, func) {
- sqlDbQuery('SELECT doc FROM main WHERE type = $1 AND domain = $2 AND JSON_EXTRACT(doc, "$.rname") = $3 ORDER BY lastbootuptime',
- ['node', domain, rname], function (err, docs) {
- func(err, performTypedRecordDecrypt(docs));
- });
- };
- obj.Remove = function (id, func) { sqlDbQuery('DELETE FROM main WHERE id = $1', [id], func); };
- obj.RemoveAll = function (func) { sqlDbQuery('DELETE FROM main', null, func); };
- obj.RemoveAllOfType = function (type, func) { sqlDbQuery('DELETE FROM main WHERE type = $1', [type], func); };
- obj.InsertMany = function (data, func) { var pendingOps = 0; for (var i in data) { pendingOps++; obj.SetRaw(data[i], function () { if (--pendingOps == 0) { func(); } }); } }; // Insert records directly, no link escaping
- obj.RemoveMeshDocuments = function (id, func) { sqlDbQuery('DELETE FROM main WHERE extra = $1', [id], function () { sqlDbQuery('DELETE FROM main WHERE id = $1', ['nt' + id], func); }); };
- obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
- obj.DeleteDomain = function (domain, func) { sqlDbQuery('DELETE FROM main WHERE domain = $1', [domain], func); };
- obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
- obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
- obj.getLocalAmtNodes = function (func) {
- sqlDbQuery('SELECT doc FROM main WHERE (type = \'node\') AND (extraex IS NULL)', null, function (err, docs) {
- if (docs != null) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- var r = []; if (err == null) { for (var i in docs) { if (docs[i].host != null && docs[i].intelamt != null) { r.push(docs[i]); } } } func(err, r);
- });
- };
- obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) {
- sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extraex = $2', [domainid, 'uuid/' + uuid], function (err, docs) {
- if (docs != null) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
- func(err, docs);
- });
- };
- obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { sqlDbExec('SELECT COUNT(id) FROM main WHERE domain = $1 AND type = $2', [domainid, type], function (err, response) { func((response['COUNT(id)'] == null) || (response['COUNT(id)'] > max), response['COUNT(id)']) }); } }
-
- // Database actions on the events collection
- obj.GetAllEvents = function (func) {
- sqlDbQuery('SELECT doc FROM events', null, func);
- };
- obj.StoreEvent = function (event, func) {
- obj.dbCounters.eventsSet++;
- sqlDbQuery('INSERT INTO events VALUES (NULL, $1, $2, $3, $4, $5, $6) RETURNING id', [event.time, ((typeof event.domain == 'string') ? event.domain : null), event.action, event.nodeid ? event.nodeid : null, event.userid ? event.userid : null, JSON.stringify(event)], function (err, docs) {
- if(func){ func(); }
- if ((err == null) && (docs[0].id)) {
- for (var i in event.ids) {
- if (event.ids[i] != '*') {
- obj.pendingTransfer++;
- sqlDbQuery('INSERT INTO eventids VALUES ($1, $2)', [docs[0].id, event.ids[i]], function(){ if(func){ func(); } });
- }
- }
- }
- });
- };
- obj.GetEvents = function (ids, domain, filter, func) {
- var query = "SELECT doc FROM events ";
- var dataarray = [domain];
- if (ids.indexOf('*') >= 0) {
- query = query + "WHERE (domain = $1";
- if (filter != null) {
- query = query + " AND action = $2";
- dataarray.push(filter);
- }
- query = query + ") ORDER BY time DESC";
- } else {
- query = query + 'JOIN eventids ON id = fkid WHERE (domain = $1 AND (target IN (' + dbMergeSqlArray(ids) + '))';
- if (filter != null) {
- query = query + " AND action = $2";
- dataarray.push(filter);
- }
- query = query + ") GROUP BY id ORDER BY time DESC ";
- }
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
- var query = "SELECT doc FROM events ";
- var dataarray = [domain];
- if (ids.indexOf('*') >= 0) {
- query = query + "WHERE (domain = $1";
- if (filter != null) {
- query = query + " AND action = $2) ORDER BY time DESC LIMIT $3";
- dataarray.push(filter);
- } else {
- query = query + ") ORDER BY time DESC LIMIT $2";
- }
- } else {
- query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND (target IN (" + dbMergeSqlArray(ids) + "))";
- if (filter != null) {
- query = query + " AND action = $2) GROUP BY id ORDER BY time DESC LIMIT $3";
- dataarray.push(filter);
- } else {
- query = query + ") GROUP BY id ORDER BY time DESC LIMIT $2";
- }
- }
- dataarray.push(limit);
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetUserEvents = function (ids, domain, userid, filter, func) {
- var query = "SELECT doc FROM events ";
- var dataarray = [domain, userid];
- if (ids.indexOf('*') >= 0) {
- query = query + "WHERE (domain = $1 AND userid = $2";
- if (filter != null) {
- query = query + " AND action = $3";
- dataarray.push(filter);
- }
- query = query + ") ORDER BY time DESC";
- } else {
- query = query + 'JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target IN (' + dbMergeSqlArray(ids) + '))';
- if (filter != null) {
- query = query + " AND action = $3";
- dataarray.push(filter);
- }
- query = query + ") GROUP BY id ORDER BY time DESC";
- }
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
- var query = "SELECT doc FROM events ";
- var dataarray = [domain, userid];
- if (ids.indexOf('*') >= 0) {
- query = query + "WHERE (domain = $1 AND userid = $2";
- if (filter != null) {
- query = query + " AND action = $3) ORDER BY time DESC LIMIT $4";
- dataarray.push(filter);
- } else {
- query = query + ") ORDER BY time DESC LIMIT $3";
- }
- } else {
- query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target IN (" + dbMergeSqlArray(ids) + "))";
- if (filter != null) {
- query = query + " AND action = $3) GROUP BY id ORDER BY time DESC LIMIT $4";
- dataarray.push(filter);
- } else {
- query = query + ") GROUP BY id ORDER BY time DESC LIMIT $3";
- }
- }
- dataarray.push(limit);
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
- if (ids.indexOf('*') >= 0) {
- sqlDbQuery('SELECT doc FROM events WHERE ((domain = $1) AND (time BETWEEN $2 AND $3)) ORDER BY time', [domain, start, end], func);
- } else {
- sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE ((domain = $1) AND (target IN (' + dbMergeSqlArray(ids) + ')) AND (time BETWEEN $2 AND $3)) GROUP BY id ORDER BY time', [domain, start, end], func);
- }
- };
- //obj.GetUserLoginEvents = function (domain, userid, func) { } // TODO
- obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
- var query = "SELECT doc FROM events WHERE (nodeid = $1 AND domain = $2";
- var dataarray = [nodeid, domain];
- if (filter != null) {
- query = query + " AND action = $3) ORDER BY time DESC LIMIT $4";
- dataarray.push(filter);
- } else {
- query = query + ") ORDER BY time DESC LIMIT $3";
- }
- dataarray.push(limit);
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
- var query = "SELECT doc FROM events WHERE (nodeid = $1) AND (domain = $2) AND ((userid = $3) OR (userid IS NULL)) ";
- var dataarray = [nodeid, domain, userid];
- if (filter != null) {
- query = query + "AND (action = $4) ORDER BY time DESC LIMIT $5";
- dataarray.push(filter);
- } else {
- query = query + "ORDER BY time DESC LIMIT $4";
- }
- dataarray.push(limit);
- sqlDbQuery(query, dataarray, func);
- };
- obj.RemoveAllEvents = function (domain) { sqlDbQuery('DELETE FROM events', null, function (err, docs) { }); };
- obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = $1 AND nodeid = $2', [domain, nodeid], function (err, docs) { }); };
- obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = $1 AND userid = $2', [domain, userid], function (err, docs) { }); };
- obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) { sqlDbQuery('SELECT COUNT(*) FROM events WHERE action = \'authfail\' AND domain = $1 AND userid = $2 AND time > $3', [domainid, userid, lastlogin], function (err, response) { func(err == null ? response[0]['COUNT(*)'] : 0); }); }
-
- // Database actions on the power collection
- obj.getAllPower = function (func) { sqlDbQuery('SELECT doc FROM power', null, func); };
- obj.storePowerEvent = function (event, multiServer, func) { obj.dbCounters.powerSet++; if (multiServer != null) { event.server = multiServer.serverid; } sqlDbQuery('INSERT INTO power VALUES (NULL, $1, $2, $3)', [event.time, event.nodeid ? event.nodeid : null, JSON.stringify(event)], func); };
- obj.getPowerTimeline = function (nodeid, func) { sqlDbQuery('SELECT doc FROM power WHERE ((nodeid = $1) OR (nodeid = \'*\')) ORDER BY time ASC', [nodeid], func); };
- obj.removeAllPowerEvents = function () { sqlDbQuery('DELETE FROM power', null, function (err, docs) { }); };
- obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; sqlDbQuery('DELETE FROM power WHERE nodeid = $1', [nodeid], function (err, docs) { }); };
-
- // Database actions on the SMBIOS collection
- obj.GetAllSMBIOS = function (func) { sqlDbQuery('SELECT doc FROM smbios', null, func); };
- obj.SetSMBIOS = function (smbios, func) { var expire = new Date(smbios.time); expire.setMonth(expire.getMonth() + 6); sqlDbQuery('INSERT INTO smbios VALUES ($1, $2, $3, $4) ON CONFLICT (id) DO UPDATE SET time = $2, expire = $3, doc = $4', [smbios._id, smbios.time, expire, JSON.stringify(smbios)], func); };
- obj.RemoveSMBIOS = function (id) { sqlDbQuery('DELETE FROM smbios WHERE id = $1', [id], function (err, docs) { }); };
- obj.GetSMBIOS = function (id, func) { sqlDbQuery('SELECT doc FROM smbios WHERE id = $1', [id], func); };
-
- // Database actions on the Server Stats collection
- obj.SetServerStats = function (data, func) { sqlDbQuery('INSERT INTO serverstats VALUES ($1, $2, $3) ON CONFLICT (time) DO UPDATE SET expire = $2, doc = $3', [data.time, data.expire, JSON.stringify(data)], func); };
- obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); sqlDbQuery('SELECT doc FROM serverstats WHERE time > $1', [t], func); }; // TODO: Expire old entries
-
- // Read a configuration file from the database
- obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
-
- // Write a configuration file to the database
- obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
-
- // List all configuration files
- obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM main WHERE type = "cfile" ORDER BY id', func); }
-
- // Get database information (TODO: Complete this)
- obj.getDbStats = function (func) {
- obj.stats = { c: 4 };
- sqlDbQuery('SELECT COUNT(*) FROM main', null, function (err, response) { obj.stats.meshcentral = (err == null ? response[0]['COUNT(*)'] : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- sqlDbQuery('SELECT COUNT(*) FROM serverstats', null, function (err, response) { obj.stats.serverstats = (err == null ? response[0]['COUNT(*)'] : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- sqlDbQuery('SELECT COUNT(*) FROM power', null, function (err, response) { obj.stats.power = (err == null ? response[0]['COUNT(*)'] : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- sqlDbQuery('SELECT COUNT(*) FROM smbios', null, function (err, response) { obj.stats.smbios = (err == null ? response[0]['COUNT(*)'] : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- }
-
- // Plugin operations
- if (obj.pluginsActive) {
- obj.addPlugin = function (plugin, func) { sqlDbQuery('INSERT INTO plugin VALUES (NULL, $1)', [JSON.stringify(plugin)], func); }; // Add a plugin
- obj.getPlugins = function (func) { sqlDbQuery('SELECT JSON_INSERT(doc, "$._id", id) as doc FROM plugin', null, func); }; // Get all plugins
- obj.getPlugin = function (id, func) { sqlDbQuery('SELECT JSON_INSERT(doc, "$._id", id) as doc FROM plugin WHERE id = $1', [id], func); }; // Get plugin
- obj.deletePlugin = function (id, func) { sqlDbQuery('DELETE FROM plugin WHERE id = $1', [id], func); }; // Delete plugin
- obj.setPluginStatus = function (id, status, func) { sqlDbQuery('UPDATE plugin SET doc=JSON_SET(doc,"$.status",$1) WHERE id=$2', [status,id], func); };
- obj.updatePlugin = function (id, args, func) { delete args._id; sqlDbQuery('UPDATE plugin SET doc=json_patch(doc,$1) WHERE id=$2', [JSON.stringify(args),id], func); };
- }
- } else if (obj.databaseType == DB_ACEBASE) {
- // Database actions on the main collection. AceBase: https://github.com/appy-one/acebase
- obj.Set = function (data, func) {
- data = common.escapeLinksFieldNameEx(data);
- var xdata = performTypedRecordEncrypt(data);
- obj.dbCounters.fileSet++;
- obj.file.ref('meshcentral').child(encodeURIComponent(xdata._id)).set(common.aceEscapeFieldNames(xdata)).then(function (ref) { if (func) { func(); } })
- };
- obj.Get = function (id, func) {
- obj.file.ref('meshcentral').child(encodeURIComponent(id)).get(function (snapshot) {
- if (snapshot.exists()) { func(null, performTypedRecordDecrypt([common.aceUnEscapeFieldNames(snapshot.val())])); } else { func(null, []); }
- });
- };
- obj.GetAll = function (func) {
- obj.file.ref('meshcentral').get(function(snapshot) {
- const val = snapshot.val();
- const docs = Object.keys(val).map(function(key) { return val[key]; });
- func(null, common.aceUnEscapeAllFieldNames(docs));
- });
- };
- obj.GetHash = function (id, func) {
- obj.file.ref('meshcentral').child(encodeURIComponent(id)).get({ include: ['hash'] }, function (snapshot) {
- if (snapshot.exists()) { func(null, snapshot.val()); } else { func(null, null); }
- });
- };
- obj.GetAllTypeNoTypeField = function (type, domain, func) {
- obj.file.query('meshcentral').filter('type', '==', type).filter('domain', '==', domain).get({ exclude: ['type'] }, function (snapshots) {
- const docs = [];
- for (var i in snapshots) { const x = snapshots[i].val(); docs.push(x); }
- func(null, common.aceUnEscapeAllFieldNames(docs));
- });
- }
- obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
- if (meshes.length == 0) { func(null, []); return; }
- var query = obj.file.query('meshcentral').skip(skip).take(limit).filter('type', '==', type).filter('domain', '==', domain);
- if (id) { query = query.filter('_id', '==', id); }
- if (extrasids == null) {
- query = query.filter('meshid', 'in', meshes);
- query.get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); });
- } else {
- // TODO: This is a slow query as we did not find a filter-or-filter, so we query everything and filter manualy.
- query.get(function (snapshots) {
- const docs = [];
- for (var i in snapshots) { const x = snapshots[i].val(); if ((extrasids.indexOf(x._id) >= 0) || (meshes.indexOf(x.meshid) >= 0)) { docs.push(x); } }
- func(null, performTypedRecordDecrypt(docs));
- });
- }
- };
- obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
- var query = obj.file.query('meshcentral').filter('type', '==', type).filter('domain', '==', domain).filter('nodeid', 'in', nodes);
- if (id) { query = query.filter('_id', '==', id); }
- query.get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); });
- };
- obj.GetAllType = function (type, func) {
- obj.file.query('meshcentral').filter('type', '==', type).get(function (snapshots) {
- const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); }
- func(null, common.aceUnEscapeAllFieldNames(performTypedRecordDecrypt(docs)));
- });
- };
- obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.query('meshcentral').filter('_id', 'in', ids).filter('domain', '==', domain).filter('type', '==', type).get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
- obj.GetUserWithEmail = function (domain, email, func) { obj.file.query('meshcentral').filter('type', '==', 'user').filter('domain', '==', domain).filter('email', '==', email).get({ exclude: ['type'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
- obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.query('meshcentral').filter('type', '==', 'user').filter('domain', '==', domain).filter('email', '==', email).filter('emailVerified', '==', true).get({ exclude: ['type'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
- obj.GetNodeByComputerName = function (domain, rname, func) { obj.file.query('meshcentral').filter('type', '==', 'node').filter('domain', '==', domain).filter('rname', '==', rname).get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
- obj.Remove = function (id, func) { obj.file.ref('meshcentral').child(encodeURIComponent(id)).remove().then(function () { if (func) { func(); } }); };
- obj.RemoveAll = function (func) { obj.file.query('meshcentral').remove().then(function () { if (func) { func(); } }); };
- obj.RemoveAllOfType = function (type, func) { obj.file.query('meshcentral').filter('type', '==', type).remove().then(function () { if (func) { func(); } }); };
- obj.InsertMany = function (data, func) { var r = {}; for (var i in data) { const ref = obj.file.ref('meshcentral').child(encodeURIComponent(data[i]._id)); r[ref.key] = common.aceEscapeFieldNames(data[i]); } obj.file.ref('meshcentral').set(r).then(function (ref) { func(); }); }; // Insert records directly, no link escaping
- obj.RemoveMeshDocuments = function (id) { obj.file.query('meshcentral').filter('meshid', '==', id).remove(); obj.file.ref('meshcentral').child(encodeURIComponent('nt' + id)).remove(); };
- obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
- obj.DeleteDomain = function (domain, func) { obj.file.query('meshcentral').filter('domain', '==', domain).remove().then(function () { if (func) { func(); } }); };
- obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
- obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
- obj.getLocalAmtNodes = function (func) { obj.file.query('meshcentral').filter('type', '==', 'node').filter('host', 'exists').filter('host', '!=', null).filter('intelamt', 'exists').get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
- obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { obj.file.query('meshcentral').filter('type', '==', 'node').filter('domain', '==', domainid).filter('mtype', '!=', mtype).filter('intelamt.uuid', '==', uuid).get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
- obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { obj.file.query('meshcentral').filter('type', '==', type).filter('domain', '==', domainid).get({ snapshots: false }, function (snapshots) { func((snapshots.length > max), snapshots.length); }); } }
-
- // Database actions on the events collection
- obj.GetAllEvents = function (func) {
- obj.file.ref('events').get(function (snapshot) {
- const val = snapshot.val();
- const docs = Object.keys(val).map(function(key) { return val[key]; });
- func(null, docs);
- })
- };
- obj.StoreEvent = function (event, func) {
- if (typeof event.account == 'object') { event = Object.assign({}, event); event.account = common.aceEscapeFieldNames(event.account); }
- obj.dbCounters.eventsSet++;
- obj.file.ref('events').push(event).then(function (userRef) { if (func) { func(); } });
- };
- obj.GetEvents = function (ids, domain, filter, func) {
- // This request is slow since we have not found a .filter() that will take two arrays and match a single item.
- if (filter != null) {
- obj.file.query('events').filter('domain', '==', domain).filter('action', '==', filter).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) {
- const docs = [];
- for (var i in snapshots) {
- const doc = snapshots[i].val();
- if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue;
- var found = false;
- for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches
- if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); }
- }
- func(null, docs);
- });
- } else {
- obj.file.query('events').filter('domain', '==', domain).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) {
- const docs = [];
- for (var i in snapshots) {
- const doc = snapshots[i].val();
- if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue;
- var found = false;
- for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches
- if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); }
- }
- func(null, docs);
- });
- }
- };
- obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
- // This request is slow since we have not found a .filter() that will take two arrays and match a single item.
- // TODO: Request a new AceBase feature for a 'array:contains-one-of' filter:
- // obj.file.indexes.create('events', 'ids', { type: 'array' });
- // db.query('events').filter('ids', 'array:contains-one-of', ids)
- if (filter != null) {
- obj.file.query('events').filter('domain', '==', domain).filter('action', '==', filter).take(limit).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) {
- const docs = [];
- for (var i in snapshots) {
- const doc = snapshots[i].val();
- if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue;
- var found = false;
- for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches
- if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); }
- }
- func(null, docs);
- });
- } else {
- obj.file.query('events').filter('domain', '==', domain).take(limit).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) {
- const docs = [];
- for (var i in snapshots) {
- const doc = snapshots[i].val();
- if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue;
- var found = false;
- for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches
- if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); }
- }
- func(null, docs);
- });
- }
- };
- obj.GetUserEvents = function (ids, domain, userid, filter, func) {
- if (filter != null) {
- obj.file.query('events').filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).filter('action', '==', filter).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
- } else {
- obj.file.query('events').filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
- }
- };
- obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
- if (filter != null) {
- obj.file.query('events').take(limit).filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).filter('action', '==', filter).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
- } else {
- obj.file.query('events').take(limit).filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
- }
- };
- obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
- obj.file.query('events').filter('domain', '==', domain).filter('ids', 'in', ids).filter('msgid', 'in', msgids).filter('time', 'between', [start, end]).sort('time', false).get({ exclude: ['type', '_id', 'domain', 'node'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
- };
- obj.GetUserLoginEvents = function (domain, userid, func) {
- obj.file.query('events').filter('domain', '==', domain).filter('action', 'in', ['authfail', 'login']).filter('userid', '==', userid).filter('msgArgs', 'exists').sort('time', false).get({ include: ['action', 'time', 'msgid', 'msgArgs', 'tokenName'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
- };
- obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
- if (filter != null) {
- obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).filter('action', '==', filter).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
- } else {
- obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
- }
- };
- obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
- if (filter != null) {
- obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).filter('userid', '==', userid).filter('action', '==', filter).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
- } else {
- obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).filter('userid', '==', userid).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
- }
- obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).filter('userid', '==', userid).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
- };
- obj.RemoveAllEvents = function (domain) {
- obj.file.query('events').filter('domain', '==', domain).remove().then(function () { if (func) { func(); } });;
- };
- obj.RemoveAllNodeEvents = function (domain, nodeid) {
- if ((domain == null) || (nodeid == null)) return;
- obj.file.query('events').filter('domain', '==', domain).filter('nodeid', '==', nodeid).remove().then(function () { if (func) { func(); } });;
- };
- obj.RemoveAllUserEvents = function (domain, userid) {
- if ((domain == null) || (userid == null)) return;
- obj.file.query('events').filter('domain', '==', domain).filter('userid', '==', userid).remove().then(function () { if (func) { func(); } });;
- };
- obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) {
- obj.file.query('events').filter('domain', '==', domainid).filter('userid', '==', userid).filter('time', '>', lastlogin).sort('time', false).get({ snapshots: false }, function (snapshots) { func(null, snapshots.length); });
- }
-
- // Database actions on the power collection
- obj.getAllPower = function (func) {
- obj.file.ref('power').get(function (snapshot) {
- const val = snapshot.val();
- const docs = Object.keys(val).map(function(key) { return val[key]; });
- func(null, docs);
- });
- };
- obj.storePowerEvent = function (event, multiServer, func) {
- if (multiServer != null) { event.server = multiServer.serverid; }
- obj.file.ref('power').push(event).then(function (userRef) { if (func) { func(); } });
- };
- obj.getPowerTimeline = function (nodeid, func) {
- obj.file.query('power').filter('nodeid', 'in', ['*', nodeid]).sort('time').get({ exclude: ['_id', 'nodeid', 's'] }, function (snapshots) {
- const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs);
- });
- };
- obj.removeAllPowerEvents = function () {
- obj.file.ref('power').remove().then(function () { if (func) { func(); } });
- };
- obj.removeAllPowerEventsForNode = function (nodeid) {
- if (nodeid == null) return;
- obj.file.query('power').filter('nodeid', '==', nodeid).remove().then(function () { if (func) { func(); } });
- };
-
- // Database actions on the SMBIOS collection
- if (obj.smbiosfile != null) {
- obj.GetAllSMBIOS = function (func) {
- obj.file.ref('smbios').get(function (snapshot) {
- const val = snapshot.val();
- const docs = Object.keys(val).map(function(key) { return val[key]; });
- func(null, docs);
- });
- };
- obj.SetSMBIOS = function (smbios, func) {
- obj.file.ref('meshcentral/' + encodeURIComponent(smbios._id)).set(smbios).then(function (ref) { if (func) { func(); } })
- };
- obj.RemoveSMBIOS = function (id) {
- obj.file.query('smbios').filter('_id', '==', id).remove().then(function () { if (func) { func(); } });
- };
- obj.GetSMBIOS = function (id, func) {
- obj.file.query('smbios').filter('_id', '==', id).get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
- };
- }
-
- // Database actions on the Server Stats collection
- obj.SetServerStats = function (data, func) {
- obj.file.ref('stats').push(data).then(function (userRef) { if (func) { func(); } });
- };
- obj.GetServerStats = function (hours, func) {
- var t = new Date();
- t.setTime(t.getTime() - (60 * 60 * 1000 * hours));
- obj.file.query('stats').filter('time', '>', t).get({ exclude: ['_id', 'cpu'] }, function (snapshots) {
- const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs);
- });
- };
-
- // Read a configuration file from the database
- obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
-
- // Write a configuration file to the database
- obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
-
- // List all configuration files
- obj.listConfigFiles = function (func) {
- obj.file.query('meshcentral').filter('type', '==', 'cfile').sort('_id').get(function (snapshots) {
- const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs);
- });
- }
-
- // Get database information
- obj.getDbStats = function (func) {
- obj.stats = { c: 5 };
- obj.file.ref('meshcentral').count().then(function (count) { obj.stats.meshcentral = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- obj.file.ref('events').count().then(function (count) { obj.stats.events = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- obj.file.ref('power').count().then(function (count) { obj.stats.power = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- obj.file.ref('smbios').count().then(function (count) { obj.stats.smbios = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- obj.file.ref('stats').count().then(function (count) { obj.stats.serverstats = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- }
-
- // Plugin operations
- if (obj.pluginsActive) {
- obj.addPlugin = function (plugin, func) { plugin.type = 'plugin'; obj.file.ref('plugin').child(encodeURIComponent(plugin._id)).set(plugin).then(function (ref) { if (func) { func(); } }) }; // Add a plugin
- obj.getPlugins = function (func) {
- obj.file.ref('plugin').get({ exclude: ['type'] }, function (snapshot) {
- const val = snapshot.val();
- const docs = Object.keys(val).map(function(key) { return val[key]; }).sort(function(a, b) { return a.name < b.name ? -1 : 1 });
- func(null, docs);
- });
- }; // Get all plugins
- obj.getPlugin = function (id, func) { obj.file.query('plugin').filter('_id', '==', id).get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); }); }; // Get plugin
- obj.deletePlugin = function (id, func) { obj.file.ref('plugin').child(encodeURIComponent(id)).remove().then(function () { if (func) { func(); } }); }; // Delete plugin
- obj.setPluginStatus = function (id, status, func) { obj.file.ref('plugin').child(encodeURIComponent(id)).update({ status: status }).then(function (ref) { if (func) { func(); } }) };
- obj.updatePlugin = function (id, args, func) { delete args._id; obj.file.ref('plugin').child(encodeURIComponent(id)).set(args).then(function (ref) { if (func) { func(); } }) };
- }
- } else if (obj.databaseType == DB_POSTGRESQL) {
- // Database actions on the main collection (Postgres)
- obj.Set = function (value, func) {
- obj.dbCounters.fileSet++;
- var extra = null, extraex = null;
- value = common.escapeLinksFieldNameEx(value);
- if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
- if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
- if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
- sqlDbQuery('INSERT INTO main VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET type = $2, domain = $3, extra = $4, extraex = $5, doc = $6;', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, performTypedRecordEncrypt(value)], func);
- }
- obj.SetRaw = function (value, func) {
- obj.dbCounters.fileSet++;
- var extra = null, extraex = null;
- if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
- if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
- if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
- sqlDbQuery('INSERT INTO main VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET type = $2, domain = $3, extra = $4, extraex = $5, doc = $6;', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, performTypedRecordEncrypt(value)], func);
- }
- obj.Get = function (_id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = $1', [_id], function (err, docs) { if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); } func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetAll = function (func) { sqlDbQuery('SELECT domain, doc FROM main', null, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetHash = function (id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = $1', [id], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetAllTypeNoTypeField = function (type, domain, func) { sqlDbQuery('SELECT doc FROM main WHERE type = $1 AND domain = $2', [type, domain], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
- if (limit == 0) { limit = 0xFFFFFFFF; }
- if (id && (id != '')) {
- sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra = ANY ($4)) LIMIT $5 OFFSET $6', [id, type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
- } else {
- if (extrasids == null) {
- sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3)) LIMIT $4 OFFSET $5', [type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }, true);
- } else {
- sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra = ANY ($3)) OR (id = ANY ($4))) LIMIT $5 OFFSET $6', [type, domain, meshes, extrasids, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
- }
- }
- };
- obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
- if (id && (id != '')) {
- sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra = ANY ($4))', [id, type, domain, meshes], function (err, docs) { func(err, docs); });
- } else {
- if (extrasids == null) {
- sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3))', [type, domain, meshes], function (err, docs) { func(err, docs); }, true);
- } else {
- sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (type = $1) AND (domain = $2) AND ((extra = ANY ($3)) OR (id = ANY ($4)))', [type, domain, meshes, extrasids], function (err, docs) { func(err, docs); });
- }
- }
- };
- obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
- if (id && (id != '')) {
- sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra = ANY ($4))', [id, type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
- } else {
- sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3))', [type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
- }
- };
- obj.GetAllType = function (type, func) { sqlDbQuery('SELECT doc FROM main WHERE type = $1', [type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetAllIdsOfType = function (ids, domain, type, func) { sqlDbQuery('SELECT doc FROM main WHERE (id = ANY ($1)) AND domain = $2 AND type = $3', [ids, domain, type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetUserWithEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extra = $2', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetUserWithVerifiedEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extra = $2', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetNodeByComputerName = function (domain, rname, func) { sqlDbQuery('SELECT doc FROM main WHERE type = "$1" AND domain = $2 AND doc->>\'rname\' = $3 ORDER BY lastbootuptime', ['node', domain, rname], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });};
- obj.Remove = function (id, func) { sqlDbQuery('DELETE FROM main WHERE id = $1', [id], func); };
- obj.RemoveAll = function (func) { sqlDbQuery('DELETE FROM main', null, func); };
- obj.RemoveAllOfType = function (type, func) { sqlDbQuery('DELETE FROM main WHERE type = $1', [type], func); };
- obj.InsertMany = function (data, func) { var pendingOps = 0; for (var i in data) { pendingOps++; obj.SetRaw(data[i], function () { if (--pendingOps == 0) { func(); } }); } }; // Insert records directly, no link escaping
- obj.RemoveMeshDocuments = function (id, func) { sqlDbQuery('DELETE FROM main WHERE extra = $1', [id], function () { sqlDbQuery('DELETE FROM main WHERE id = $1', ['nt' + id], func); }); };
- obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
- obj.DeleteDomain = function (domain, func) { sqlDbQuery('DELETE FROM main WHERE domain = $1', [domain], func); };
- obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
- obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
- obj.getLocalAmtNodes = function (func) { sqlDbQuery('SELECT doc FROM main WHERE (type = \'node\') AND (extraex IS NULL)', null, function (err, docs) { var r = []; if (err == null) { for (var i in docs) { if (docs[i].host != null && docs[i].intelamt != null) { r.push(docs[i]); } } } func(err, r); }); };
- obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extraex = $2', [domainid, 'uuid/' + uuid], func); };
- obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { sqlDbExec('SELECT COUNT(id) FROM main WHERE domain = $1 AND type = $2', [domainid, type], function (err, response) { func((response['COUNT(id)'] == null) || (response['COUNT(id)'] > max), response['COUNT(id)']) }); } }
-
- // Database actions on the events collection
- obj.GetAllEvents = function (func) { sqlDbQuery('SELECT doc FROM events', null, func); };
- obj.StoreEvent = function (event, func) {
- obj.dbCounters.eventsSet++;
- sqlDbQuery('INSERT INTO events VALUES (DEFAULT, $1, $2, $3, $4, $5, $6) RETURNING id', [event.time, ((typeof event.domain == 'string') ? event.domain : null), event.action, event.nodeid ? event.nodeid : null, event.userid ? event.userid : null, event], function (err, docs) {
- if(func){ func(); }
- if (docs.id) {
- for (var i in event.ids) {
- if (event.ids[i] != '*') {
- obj.pendingTransfer++;
- sqlDbQuery('INSERT INTO eventids VALUES ($1, $2)', [docs.id, event.ids[i]], function(){ if(func){ func(); } });
- }
- }
- }
- });
- };
- obj.GetEvents = function (ids, domain, filter, func) {
- var query = "SELECT doc FROM events ";
- var dataarray = [domain];
- if (ids.indexOf('*') >= 0) {
- query = query + "WHERE (domain = $1";
- if (filter != null) {
- query = query + " AND action = $2";
- dataarray.push(filter);
- }
- query = query + ") ORDER BY time DESC";
- } else {
- query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND (target = ANY ($2))";
- dataarray.push(ids);
- if (filter != null) {
- query = query + " AND action = $3";
- dataarray.push(filter);
- }
- query = query + ") GROUP BY id ORDER BY time DESC";
- }
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
- var query = "SELECT doc FROM events ";
- var dataarray = [domain];
- if (ids.indexOf('*') >= 0) {
- query = query + "WHERE (domain = $1";
- if (filter != null) {
- query = query + " AND action = $2) ORDER BY time DESC LIMIT $3";
- dataarray.push(filter);
- } else {
- query = query + ") ORDER BY time DESC LIMIT $2";
- }
- } else {
- if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND (target = ANY ($2))";
- dataarray.push(ids);
- if (filter != null) {
- query = query + " AND action = $3) ORDER BY time DESC LIMIT $4";
- dataarray.push(filter);
- } else {
- query = query + ") ORDER BY time DESC LIMIT $3";
- }
- }
- dataarray.push(limit);
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetUserEvents = function (ids, domain, userid, filter, func) {
- var query = "SELECT doc FROM events ";
- var dataarray = [domain, userid];
- if (ids.indexOf('*') >= 0) {
- query = query + "WHERE (domain = $1 AND userid = $2";
- if (filter != null) {
- query = query + " AND action = $3";
- dataarray.push(filter);
- }
- query = query + ") ORDER BY time DESC";
- } else {
- if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target = ANY ($3))";
- dataarray.push(ids);
- if (filter != null) {
- query = query + " AND action = $4";
- dataarray.push(filter);
- }
- query = query + ") GROUP BY id ORDER BY time DESC";
- }
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
- var query = "SELECT doc FROM events ";
- var dataarray = [domain, userid];
- if (ids.indexOf('*') >= 0) {
- query = query + "WHERE (domain = $1 AND userid = $2";
- if (filter != null) {
- query = query + " AND action = $3) ORDER BY time DESC LIMIT $4 ";
- dataarray.push(filter);
- } else {
- query = query + ") ORDER BY time DESC LIMIT $3";
- }
- } else {
- if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target = ANY ($3))";
- dataarray.push(ids);
- if (filter != null) {
- query = query + " AND action = $4) GROUP BY id ORDER BY time DESC LIMIT $5";
- dataarray.push(filter);
- } else {
- query = query + ") GROUP BY id ORDER BY time DESC LIMIT $4";
- }
- }
- dataarray.push(limit);
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
- if (ids.indexOf('*') >= 0) {
- sqlDbQuery('SELECT doc FROM events WHERE ((domain = $1) AND (time BETWEEN $2 AND $3)) ORDER BY time', [domain, start, end], func);
- } else {
- sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE ((domain = $1) AND (target = ANY ($2)) AND (time BETWEEN $3 AND $4)) GROUP BY id ORDER BY time', [domain, ids, start, end], func);
- }
- };
- //obj.GetUserLoginEvents = function (domain, userid, func) { } // TODO
- obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
- var query = "SELECT doc FROM events WHERE (nodeid = $1 AND domain = $2";
- var dataarray = [nodeid, domain];
- if (filter != null) {
- query = query + " AND action = $3) ORDER BY time DESC LIMIT $4";
- dataarray.push(filter);
- } else {
- query = query + ") ORDER BY time DESC LIMIT $3";
- }
- dataarray.push(limit);
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
- var query = "SELECT doc FROM events WHERE (nodeid = $1 AND domain = $2 AND ((userid = $3) OR (userid IS NULL))";
- var dataarray = [nodeid, domain, userid];
- if (filter != null) {
- query = query + " AND action = $4) ORDER BY time DESC LIMIT $5";
- dataarray.push(filter);
- } else {
- query = query + ") ORDER BY time DESC LIMIT $4";
- }
- dataarray.push(limit);
- sqlDbQuery(query, dataarray, func);
- };
- obj.RemoveAllEvents = function (domain) { sqlDbQuery('DELETE FROM events', null, function (err, docs) { }); };
- obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = $1 AND nodeid = $2', [domain, nodeid], function (err, docs) { }); };
- obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = $1 AND userid = $2', [domain, userid], function (err, docs) { }); };
- obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) { sqlDbQuery('SELECT COUNT(*) FROM events WHERE action = \'authfail\' AND domain = $1 AND userid = $2 AND time > $3', [domainid, userid, lastlogin], function (err, response, raw) { func(err == null ? parseInt(raw.rows[0].count) : 0); }); }
-
- // Database actions on the power collection
- obj.getAllPower = function (func) { sqlDbQuery('SELECT doc FROM power', null, func); };
- obj.storePowerEvent = function (event, multiServer, func) { obj.dbCounters.powerSet++; if (multiServer != null) { event.server = multiServer.serverid; } sqlDbQuery('INSERT INTO power VALUES (DEFAULT, $1, $2, $3)', [event.time, event.nodeid ? event.nodeid : null, event], func); };
- obj.getPowerTimeline = function (nodeid, func) { sqlDbQuery('SELECT doc FROM power WHERE ((nodeid = $1) OR (nodeid = \'*\')) ORDER BY time ASC', [nodeid], func); };
- obj.removeAllPowerEvents = function () { sqlDbQuery('DELETE FROM power', null, function (err, docs) { }); };
- obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; sqlDbQuery('DELETE FROM power WHERE nodeid = $1', [nodeid], function (err, docs) { }); };
-
- // Database actions on the SMBIOS collection
- obj.GetAllSMBIOS = function (func) { sqlDbQuery('SELECT doc FROM smbios', null, func); };
- obj.SetSMBIOS = function (smbios, func) { var expire = new Date(smbios.time); expire.setMonth(expire.getMonth() + 6); sqlDbQuery('INSERT INTO smbios VALUES ($1, $2, $3, $4) ON CONFLICT (id) DO UPDATE SET time = $2, expire = $3, doc = $4', [smbios._id, smbios.time, expire, smbios], func); };
- obj.RemoveSMBIOS = function (id) { sqlDbQuery('DELETE FROM smbios WHERE id = $1', [id], function (err, docs) { }); };
- obj.GetSMBIOS = function (id, func) { sqlDbQuery('SELECT doc FROM smbios WHERE id = $1', [id], func); };
-
- // Database actions on the Server Stats collection
- obj.SetServerStats = function (data, func) { sqlDbQuery('INSERT INTO serverstats VALUES ($1, $2, $3) ON CONFLICT (time) DO UPDATE SET expire = $2, doc = $3', [data.time, data.expire, data], func); };
- obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); sqlDbQuery('SELECT doc FROM serverstats WHERE time > $1', [t], func); }; // TODO: Expire old entries
-
- // Read a configuration file from the database
- obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
-
- // Write a configuration file to the database
- obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
-
- // List all configuration files
- obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM main WHERE type = "cfile" ORDER BY id', func); }
-
- // Get database information (TODO: Complete this)
- obj.getDbStats = function (func) {
- obj.stats = { c: 4 };
- sqlDbQuery('SELECT COUNT(*) FROM main', null, function (err, response, raw) { obj.stats.meshcentral = (err == null ? parseInt(raw.rows[0].count) : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- sqlDbQuery('SELECT COUNT(*) FROM serverstats', null, function (err, response, raw) { obj.stats.serverstats = (err == null ? parseInt(raw.rows[0].count) : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- sqlDbQuery('SELECT COUNT(*) FROM power', null, function (err, response, raw) { obj.stats.power = (err == null ? parseInt(raw.rows[0].count) : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- sqlDbQuery('SELECT COUNT(*) FROM smbios', null, function (err, response, raw) { obj.stats.smbios = (err == null ? parseInt(raw.rows[0].count) : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- }
-
- // Plugin operations
- if (obj.pluginsActive) {
- obj.addPlugin = function (plugin, func) { sqlDbQuery('INSERT INTO plugin VALUES (DEFAULT, $1)', [plugin], func); }; // Add a plugin
- obj.getPlugins = function (func) { sqlDbQuery("SELECT doc::jsonb || ('{\"_id\":' || plugin.id || '}')::jsonb as doc FROM plugin", null, func); }; // Get all plugins
- obj.getPlugin = function (id, func) { sqlDbQuery("SELECT doc::jsonb || ('{\"_id\":' || plugin.id || '}')::jsonb as doc FROM plugin WHERE id = $1", [id], func); }; // Get plugin
- obj.deletePlugin = function (id, func) { sqlDbQuery('DELETE FROM plugin WHERE id = $1', [id], func); }; // Delete plugin
- obj.setPluginStatus = function (id, status, func) { sqlDbQuery("UPDATE plugin SET doc= jsonb_set(doc::jsonb,'{status}',$1) WHERE id=$2", [status,id], func); };
- obj.updatePlugin = function (id, args, func) { delete args._id; sqlDbQuery('UPDATE plugin SET doc= doc::jsonb || ($1) WHERE id=$2', [args,id], func); };
- }
- } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
- // Database actions on the main collection (MariaDB or MySQL)
- obj.Set = function (value, func) {
- obj.dbCounters.fileSet++;
- var extra = null, extraex = null;
- value = common.escapeLinksFieldNameEx(value);
- if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
- if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
- if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
- sqlDbQuery('REPLACE INTO main VALUE (?, ?, ?, ?, ?, ?)', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, JSON.stringify(performTypedRecordEncrypt(value))], func);
- }
- obj.SetRaw = function (value, func) {
- obj.dbCounters.fileSet++;
- var extra = null, extraex = null;
- if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
- if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
- if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
- sqlDbQuery('REPLACE INTO main VALUE (?, ?, ?, ?, ?, ?)', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, JSON.stringify(performTypedRecordEncrypt(value))], func);
- }
- obj.Get = function (_id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = ?', [_id], function (err, docs) { if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); } func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetAll = function (func) { sqlDbQuery('SELECT domain, doc FROM main', null, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetHash = function (id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = ?', [id], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetAllTypeNoTypeField = function (type, domain, func) { sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ?', [type, domain], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
- if (limit == 0) { limit = 0xFFFFFFFF; }
- if ((meshes == null) || (meshes.length == 0)) { meshes = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- if ((extrasids == null) || (extrasids.length == 0)) { extrasids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- if (id && (id != '')) {
- sqlDbQuery('SELECT doc FROM main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?) LIMIT ? OFFSET ?', [id, type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
- } else {
- sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND (extra IN (?) OR id IN (?)) LIMIT ? OFFSET ?', [type, domain, meshes, extrasids, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
- }
- };
- obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
- if ((meshes == null) || (meshes.length == 0)) { meshes = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- if ((extrasids == null) || (extrasids.length == 0)) { extrasids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- if (id && (id != '')) {
- sqlDbQuery('SELECT COUNT(doc) FROM main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?)', [id, type, domain, meshes], function (err, docs) { func(err, docs); });
- } else {
- sqlDbQuery('SELECT COUNT(doc) FROM main WHERE type = ? AND domain = ? AND (extra IN (?) OR id IN (?))', [type, domain, meshes, extrasids], function (err, docs) { func(err, docs); });
- }
- };
- obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
- if ((nodes == null) || (nodes.length == 0)) { nodes = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- if (id && (id != '')) {
- sqlDbQuery('SELECT doc FROM main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?)', [id, type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
- } else {
- sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND extra IN (?)', [type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
- }
- };
- obj.GetAllType = function (type, func) { sqlDbQuery('SELECT doc FROM main WHERE type = ?', [type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetAllIdsOfType = function (ids, domain, type, func) {
- if ((ids == null) || (ids.length == 0)) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- sqlDbQuery('SELECT doc FROM main WHERE id IN (?) AND domain = ? AND type = ?', [ids, domain, type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
- }
- obj.GetUserWithEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = ? AND extra = ?', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetUserWithVerifiedEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = ? AND extra = ?', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetNodeByComputerName = function (domain, rname, func) { sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND JSON_EXTRACT(doc, "$.rname") = ? ORDER BY lastbootuptime', ['node', domain, rname], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.Remove = function (id, func) { sqlDbQuery('DELETE FROM main WHERE id = ?', [id], func); };
- obj.RemoveAll = function (func) { sqlDbQuery('DELETE FROM main', null, func); };
- obj.RemoveAllOfType = function (type, func) { sqlDbQuery('DELETE FROM main WHERE type = ?', [type], func); };
- obj.InsertMany = function (data, func) { var pendingOps = 0; for (var i in data) { pendingOps++; obj.SetRaw(data[i], function () { if (--pendingOps == 0) { func(); } }); } }; // Insert records directly, no link escaping
- obj.RemoveMeshDocuments = function (id, func) { sqlDbQuery('DELETE FROM main WHERE extra = ?', [id], function () { sqlDbQuery('DELETE FROM main WHERE id = ?', ['nt' + id], func); } ); };
- obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
- obj.DeleteDomain = function (domain, func) { sqlDbQuery('DELETE FROM main WHERE domain = ?', [domain], func); };
- obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
- obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
- obj.getLocalAmtNodes = function (func) { sqlDbQuery('SELECT doc FROM main WHERE (type = "node") AND (extraex IS NULL)', null, function (err, docs) { var r = []; if (err == null) { for (var i in docs) { if (docs[i].host != null && docs[i].intelamt != null) { r.push(docs[i]); } } } func(err, r); }); };
- obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = ? AND extraex = ?', [domainid, 'uuid/' + uuid], func); };
- obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { sqlDbExec('SELECT COUNT(id) FROM main WHERE domain = ? AND type = ?', [domainid, type], function (err, response) { func((response['COUNT(id)'] == null) || (response['COUNT(id)'] > max), response['COUNT(id)']) }); } }
-
- // Database actions on the events collection
- obj.GetAllEvents = function (func) { sqlDbQuery('SELECT doc FROM events', null, func); };
- obj.StoreEvent = function (event, func) {
- obj.dbCounters.eventsSet++;
- var batchQuery = [['INSERT INTO events VALUE (?, ?, ?, ?, ?, ?, ?)', [null, event.time, ((typeof event.domain == 'string') ? event.domain : null), event.action, event.nodeid ? event.nodeid : null, event.userid ? event.userid : null, JSON.stringify(event)]]];
- for (var i in event.ids) { if (event.ids[i] != '*') { batchQuery.push(['INSERT INTO eventids VALUE (LAST_INSERT_ID(), ?)', [event.ids[i]]]); } }
- sqlDbBatchExec(batchQuery, function (err, docs) { if (func != null) { func(err, docs); } });
- };
- obj.GetEvents = function (ids, domain, filter, func) {
- var query = "SELECT doc FROM events ";
- var dataarray = [domain];
- if (ids.indexOf('*') >= 0) {
- query = query + "WHERE (domain = ?";
- if (filter != null) {
- query = query + " AND action = ?";
- dataarray.push(filter);
- }
- query = query + ") ORDER BY time DESC";
- } else {
- if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND target IN (?)";
- dataarray.push(ids);
- if (filter != null) {
- query = query + " AND action = ?";
- dataarray.push(filter);
- }
- query = query + ") GROUP BY id ORDER BY time DESC";
- }
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
- var query = "SELECT doc FROM events ";
- var dataarray = [domain];
- if (ids.indexOf('*') >= 0) {
- query = query + "WHERE (domain = ?";
- if (filter != null) {
- query = query + " AND action = ? ";
- dataarray.push(filter);
- }
- query = query + ") ORDER BY time DESC LIMIT ?";
- } else {
- if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND target IN (?)";
- dataarray.push(ids);
- if (filter != null) {
- query = query + " AND action = ?";
- dataarray.push(filter);
- }
- query = query + ") GROUP BY id ORDER BY time DESC LIMIT ?";
- }
- dataarray.push(limit);
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetUserEvents = function (ids, domain, userid, filter, func) {
- var query = "SELECT doc FROM events ";
- var dataarray = [domain, userid];
- if (ids.indexOf('*') >= 0) {
- query = query + "WHERE (domain = ? AND userid = ?";
- if (filter != null) {
- query = query + " AND action = ?";
- dataarray.push(filter);
- }
- query = query + ") ORDER BY time DESC";
- } else {
- if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND userid = ? AND target IN (?)";
- dataarray.push(ids);
- if (filter != null) {
- query = query + " AND action = ?";
- dataarray.push(filter);
- }
- query = query + ") GROUP BY id ORDER BY time DESC";
- }
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
- var query = "SELECT doc FROM events ";
- var dataarray = [domain, userid];
- if (ids.indexOf('*') >= 0) {
- query = query + "WHERE (domain = ? AND userid = ?";
- if (filter != null) {
- query = query + " AND action = ?";
- dataarray.push(filter);
- }
- query = query + ") ORDER BY time DESC LIMIT ?";
- } else {
- if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND userid = ? AND target IN (?)";
- dataarray.push(ids);
- if (filter != null) {
- query = query + " AND action = ?";
- dataarray.push(filter);
- }
- query = query + ") GROUP BY id ORDER BY time DESC LIMIT ?";
- }
- dataarray.push(limit);
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
- if (ids.indexOf('*') >= 0) {
- sqlDbQuery('SELECT doc FROM events WHERE ((domain = ?) AND (time BETWEEN ? AND ?)) ORDER BY time', [domain, start, end], func);
- } else {
- if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
- sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE ((domain = ?) AND (target IN (?)) AND (time BETWEEN ? AND ?)) GROUP BY id ORDER BY time', [domain, ids, start, end], func);
- }
- };
- //obj.GetUserLoginEvents = function (domain, userid, func) { } // TODO
- obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
- var query = "SELECT doc FROM events WHERE (nodeid = ? AND domain = ?";
- var dataarray = [nodeid, domain];
- if (filter != null) {
- query = query + " AND action = ?) ORDER BY time DESC LIMIT ?";
- dataarray.push(filter);
- } else {
- query = query + ") ORDER BY time DESC LIMIT ?";
- }
- dataarray.push(limit);
- sqlDbQuery(query, dataarray, func);
- };
- obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
- var query = "SELECT doc FROM events WHERE (nodeid = ? AND domain = ? AND ((userid = ?) OR (userid IS NULL))";
- var dataarray = [nodeid, domain, userid];
- if (filter != null) {
- query = query + " AND action = ?) ORDER BY time DESC LIMIT ?";
- dataarray.push(filter);
- } else {
- query = query + ") ORDER BY time DESC LIMIT ?";
- }
- dataarray.push(limit);
- sqlDbQuery(query, dataarray, func);
- };
- obj.RemoveAllEvents = function (domain) { sqlDbQuery('DELETE FROM events', null, function (err, docs) { }); };
- obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = ? AND nodeid = ?', [domain, nodeid], function (err, docs) { }); };
- obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = ? AND userid = ?', [domain, userid], function (err, docs) { }); };
- obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) { sqlDbExec('SELECT COUNT(id) FROM events WHERE action = "authfail" AND domain = ? AND userid = ? AND time > ?', [domainid, userid, lastlogin], function (err, response) { func(err == null ? response['COUNT(id)'] : 0); }); }
-
- // Database actions on the power collection
- obj.getAllPower = function (func) { sqlDbQuery('SELECT doc FROM power', null, func); };
- obj.storePowerEvent = function (event, multiServer, func) { obj.dbCounters.powerSet++; if (multiServer != null) { event.server = multiServer.serverid; } sqlDbQuery('INSERT INTO power VALUE (?, ?, ?, ?)', [null, event.time, event.nodeid ? event.nodeid : null, JSON.stringify(event)], func); };
- obj.getPowerTimeline = function (nodeid, func) { sqlDbQuery('SELECT doc FROM power WHERE ((nodeid = ?) OR (nodeid = "*")) ORDER BY time ASC', [nodeid], func); };
- obj.removeAllPowerEvents = function () { sqlDbQuery('DELETE FROM power', null, function (err, docs) { }); };
- obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; sqlDbQuery('DELETE FROM power WHERE nodeid = ?', [nodeid], function (err, docs) { }); };
-
- // Database actions on the SMBIOS collection
- obj.GetAllSMBIOS = function (func) { sqlDbQuery('SELECT doc FROM smbios', null, func); };
- obj.SetSMBIOS = function (smbios, func) { var expire = new Date(smbios.time); expire.setMonth(expire.getMonth() + 6); sqlDbQuery('REPLACE INTO smbios VALUE (?, ?, ?, ?)', [smbios._id, smbios.time, expire, JSON.stringify(smbios)], func); };
- obj.RemoveSMBIOS = function (id) { sqlDbQuery('DELETE FROM smbios WHERE id = ?', [id], function (err, docs) { }); };
- obj.GetSMBIOS = function (id, func) { sqlDbQuery('SELECT doc FROM smbios WHERE id = ?', [id], func); };
-
- // Database actions on the Server Stats collection
- obj.SetServerStats = function (data, func) { sqlDbQuery('REPLACE INTO serverstats VALUE (?, ?, ?)', [data.time, data.expire, JSON.stringify(data)], func); };
- obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); sqlDbQuery('SELECT doc FROM serverstats WHERE time > ?', [t], func); };
-
- // Read a configuration file from the database
- obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
-
- // Write a configuration file to the database
- obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
-
- // List all configuration files
- obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM main WHERE type = "cfile" ORDER BY id', func); }
-
- // Get database information (TODO: Complete this)
- obj.getDbStats = function (func) {
- obj.stats = { c: 4 };
- sqlDbExec('SELECT COUNT(id) FROM main', null, function (err, response) { obj.stats.meshcentral = Number(response['COUNT(id)']); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- sqlDbExec('SELECT COUNT(time) FROM serverstats', null, function (err, response) { obj.stats.serverstats = Number(response['COUNT(time)']); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- sqlDbExec('SELECT COUNT(id) FROM power', null, function (err, response) { obj.stats.power = Number(response['COUNT(id)']); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- sqlDbExec('SELECT COUNT(id) FROM smbios', null, function (err, response) { obj.stats.smbios = Number(response['COUNT(id)']); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
- }
-
- // Plugin operations
- if (obj.pluginsActive) {
- obj.addPlugin = function (plugin, func) { sqlDbQuery('INSERT INTO plugin VALUE (?, ?)', [null, JSON.stringify(plugin)], func); }; // Add a plugin
- obj.getPlugins = function (func) { sqlDbQuery('SELECT JSON_INSERT(doc, "$._id", id) as doc FROM plugin', null, func); }; // Get all plugins
- obj.getPlugin = function (id, func) { sqlDbQuery('SELECT JSON_INSERT(doc, "$._id", id) as doc FROM plugin WHERE id = ?', [id], func); }; // Get plugin
- obj.deletePlugin = function (id, func) { sqlDbQuery('DELETE FROM plugin WHERE id = ?', [id], func); }; // Delete plugin
- obj.setPluginStatus = function (id, status, func) { sqlDbQuery('UPDATE meshcentral.plugin SET doc=JSON_SET(doc,"$.status",?) WHERE id=?', [status,id], func); };
- obj.updatePlugin = function (id, args, func) { delete args._id; sqlDbQuery('UPDATE meshcentral.plugin SET doc=JSON_MERGE_PATCH(doc,?) WHERE id=?', [JSON.stringify(args),id], func); };
- }
- } else if (obj.databaseType == DB_MONGODB) {
- // Database actions on the main collection (MongoDB)
-
- // Bulk operations
- if (parent.config.settings.mongodbbulkoperations) {
- obj.Set = function (data, func) { // Fast Set operation using bulkWrite(), this is much faster then using replaceOne()
- if (obj.filePendingSet == false) {
- // Perform the operation now
- obj.dbCounters.fileSet++;
- obj.filePendingSet = true; obj.filePendingSets = null;
- if (func != null) { obj.filePendingCbs = [func]; }
- obj.file.bulkWrite([{ replaceOne: { filter: { _id: data._id }, replacement: performTypedRecordEncrypt(common.escapeLinksFieldNameEx(data)), upsert: true } }], fileBulkWriteCompleted);
- } else {
- // Add this operation to the pending list
- obj.dbCounters.fileSetPending++;
- if (obj.filePendingSets == null) { obj.filePendingSets = {} }
- obj.filePendingSets[data._id] = data;
- if (func != null) { if (obj.filePendingCb == null) { obj.filePendingCb = [func]; } else { obj.filePendingCb.push(func); } }
- }
- };
-
- obj.Get = function (id, func) { // Fast Get operation using a bulk find() to reduce round trips to the database.
- // Encode arguments into return function if any are present.
- var func2 = func;
- if (arguments.length > 2) {
- var parms = [func];
- for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
- var func2 = function _func2(arg1, arg2) {
- var userCallback = _func2.userArgs.shift();
- _func2.userArgs.unshift(arg2);
- _func2.userArgs.unshift(arg1);
- userCallback.apply(obj, _func2.userArgs);
- };
- func2.userArgs = parms;
- }
-
- if (obj.filePendingGets == null) {
- // No pending gets, perform the operation now.
- console.log("No pending gets, perform the operation now.");
- obj.filePendingGets = {};
- obj.filePendingGets[id] = [func2];
- obj.file.find({ _id: id }).toArray(fileBulkReadCompleted);
- } else {
- // Add get to pending list.
- console.log("Add get to pending list.");
- if (obj.filePendingGet == null) { obj.filePendingGet = {}; }
- if (obj.filePendingGet[id] == null) { obj.filePendingGet[id] = [func2]; } else { obj.filePendingGet[id].push(func2); }
- }
- };
- } else {
- obj.Set = function (data, func) {
- obj.dbCounters.fileSet++;
- data = common.escapeLinksFieldNameEx(data);
- obj.file.replaceOne({ _id: data._id }, performTypedRecordEncrypt(data), { upsert: true }, func);
- };
- obj.Get = function (id, func) {
- if (arguments.length > 2) {
- var parms = [func];
- for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
- var func2 = function _func2(arg1, arg2) {
- var userCallback = _func2.userArgs.shift();
- _func2.userArgs.unshift(arg2);
- _func2.userArgs.unshift(arg1);
- userCallback.apply(obj, _func2.userArgs);
- };
- func2.userArgs = parms;
- obj.file.find({ _id: id }).toArray(function (err, docs) {
- console.log("1");
- if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
- func2(err, performTypedRecordDecrypt(docs));
- });
- } else {
- //console.log("ID: " + id);
- obj.file.find({ _id: id }).toArray(function (err, docs) {
- //console.log("Docs: " + docs);
- if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
- func(err, performTypedRecordDecrypt(docs));
- });
- }
- };
- }
- obj.GetAll = function (func) { obj.file.find({}).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetHash = function (id, func) { obj.file.find({ _id: id }).project({ _id: 0, hash: 1 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }).project({ type: 0 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
- if (extrasids == null) {
- const x = { type: type, domain: domain, meshid: { $in: meshes } };
- if (id) { x._id = id; }
- var f = obj.file.find(x, { type: 0 });
- if (skip > 0) f = f.skip(skip); // Skip records
- if (limit > 0) f = f.limit(limit); // Limit records
- f.toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
- } else {
- const x = { type: type, domain: domain, $or: [ { meshid: { $in: meshes } }, { _id: { $in: extrasids } } ] };
- if (id) { x._id = id; }
- var f = obj.file.find(x, { type: 0 });
- if (skip > 0) f = f.skip(skip); // Skip records
- if (limit > 0) f = f.limit(limit); // Limit records
- f.toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
- }
- };
- obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
- if (extrasids == null) {
- const x = { type: type, domain: domain, meshid: { $in: meshes } };
- if (id) { x._id = id; }
- var f = obj.file.find(x, { type: 0 });
- f.count(function (err, count) { func(err, count); });
- } else {
- const x = { type: type, domain: domain, $or: [{ meshid: { $in: meshes } }, { _id: { $in: extrasids } }] };
- if (id) { x._id = id; }
- var f = obj.file.find(x, { type: 0 });
- f.count(function (err, count) { func(err, count); });
- }
- };
- obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
- var x = { type: type, domain: domain, nodeid: { $in: nodes } };
- if (id) { x._id = id; }
- obj.file.find(x, { type: 0 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
- };
- obj.GetAllType = function (type, func) { obj.file.find({ type: type }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.find({ type: type, domain: domain, _id: { $in: ids } }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetUserWithEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email, emailVerified: true }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetNodeByComputerName = function (domain, rname, func) { obj.file.find({ type: 'node', domain: domain, rname: rname }).sort({ lastbootuptime: -1 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
-
- // Bulk operations
- if (parent.config.settings.mongodbbulkoperations) {
- obj.Remove = function (id, func) { // Fast remove operation using a bulk find() to reduce round trips to the database.
- if (obj.filePendingRemoves == null) {
- // No pending removes, perform the operation now.
- obj.dbCounters.fileRemove++;
- obj.filePendingRemoves = {};
- obj.filePendingRemoves[id] = [func];
- obj.file.deleteOne({ _id: id }, fileBulkRemoveCompleted);
- } else {
- // Add remove to pending list.
- obj.dbCounters.fileRemovePending++;
- if (obj.filePendingRemove == null) { obj.filePendingRemove = {}; }
- if (obj.filePendingRemove[id] == null) { obj.filePendingRemove[id] = [func]; } else { obj.filePendingRemove[id].push(func); }
- }
- };
- } else {
- obj.Remove = function (id, func) { obj.dbCounters.fileRemove++; obj.file.deleteOne({ _id: id }, func); };
- }
-
- obj.RemoveAll = function (func) { obj.file.deleteMany({}, { multi: true }, func); };
- obj.RemoveAllOfType = function (type, func) { obj.file.deleteMany({ type: type }, { multi: true }, func); };
- obj.InsertMany = function (data, func) { obj.file.insertMany(data, func); }; // Insert records directly, no link escaping
- obj.RemoveMeshDocuments = function (id) { obj.file.deleteMany({ meshid: id }, { multi: true }); obj.file.deleteOne({ _id: 'nt' + id }); };
- obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
- obj.DeleteDomain = function (domain, func) { obj.file.deleteMany({ domain: domain }, { multi: true }, func); };
- obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
- obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
- obj.getLocalAmtNodes = function (func) { obj.file.find({ type: 'node', host: { $exists: true, $ne: null }, intelamt: { $exists: true } }).toArray(func); };
- obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { obj.file.find({ type: 'node', domain: domainid, mtype: mtype, 'intelamt.uuid': uuid }).toArray(func); };
-
- // TODO: Starting in MongoDB 4.0.3, you should use countDocuments() instead of count() that is deprecated. We should detect MongoDB version and switch.
- // https://docs.mongodb.com/manual/reference/method/db.collection.countDocuments/
- //obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { obj.file.countDocuments({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max)); }); } }
- obj.isMaxType = function (max, type, domainid, func) {
- if (obj.file.countDocuments) {
- if (max == null) { func(false); } else { obj.file.countDocuments({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); }
- } else {
- if (max == null) { func(false); } else { obj.file.count({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); }
- }
- }
-
- // Database actions on the events collection
- obj.GetAllEvents = function (func) { obj.eventsfile.find({}).toArray(func); };
-
- // Bulk operations
- if (parent.config.settings.mongodbbulkoperations) {
- obj.StoreEvent = function (event, func) { // Fast MongoDB event store using bulkWrite()
- if (obj.eventsFilePendingSet == false) {
- // Perform the operation now
- obj.dbCounters.eventsSet++;
- obj.eventsFilePendingSet = true; obj.eventsFilePendingSets = null;
- if (func != null) { obj.eventsFilePendingCbs = [func]; }
- obj.eventsfile.bulkWrite([{ insertOne: { document: event } }], eventsFileBulkWriteCompleted);
- } else {
- // Add this operation to the pending list
- obj.dbCounters.eventsSetPending++;
- if (obj.eventsFilePendingSets == null) { obj.eventsFilePendingSets = [] }
- obj.eventsFilePendingSets.push(event);
- if (func != null) { if (obj.eventsFilePendingCb == null) { obj.eventsFilePendingCb = [func]; } else { obj.eventsFilePendingCb.push(func); } }
- }
- };
- } else {
- obj.StoreEvent = function (event, func) { obj.dbCounters.eventsSet++; obj.eventsfile.insertOne(event, func); };
- }
-
- obj.GetEvents = function (ids, domain, filter, func) {
- var finddata = { domain: domain, ids: { $in: ids } };
- if (filter != null) finddata.action = filter;
- obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).toArray(func);
- };
- obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
- var finddata = { domain: domain, ids: { $in: ids } };
- if (filter != null) finddata.action = filter;
- obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).toArray(func);
- };
- obj.GetUserEvents = function (ids, domain, userid, filter, func) {
- var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
- if (filter != null) finddata.action = filter;
- obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).toArray(func);
- };
- obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
- var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
- if (filter != null) finddata.action = filter;
- obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).toArray(func);
- };
- obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) { obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }], msgid: { $in: msgids }, time: { $gte: start, $lte: end } }).project({ type: 0, _id: 0, domain: 0, node: 0 }).sort({ time: 1 }).toArray(func); };
- obj.GetUserLoginEvents = function (domain, userid, func) { obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }).project({ action: 1, time: 1, msgid: 1, msgArgs: 1, tokenName: 1 }).sort({ time: -1 }).toArray(func); };
- obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
- var finddata = { domain: domain, nodeid: nodeid };
- if (filter != null) finddata.action = filter;
- obj.eventsfile.find(finddata).project({ type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).toArray(func);
- };
- obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
- var finddata = { domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } };
- if (filter != null) finddata.action = filter;
- obj.eventsfile.find(finddata).project({ type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).toArray(func);
- };
- obj.RemoveAllEvents = function (domain) { obj.eventsfile.deleteMany({ domain: domain }, { multi: true }); };
- obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; obj.eventsfile.deleteMany({ domain: domain, nodeid: nodeid }, { multi: true }); };
- obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; obj.eventsfile.deleteMany({ domain: domain, userid: userid }, { multi: true }); };
- obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) {
- if (obj.eventsfile.countDocuments) {
- obj.eventsfile.countDocuments({ action: 'authfail', userid: userid, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null) ? count : 0); });
- } else {
- obj.eventsfile.count({ action: 'authfail', userid: userid, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null) ? count : 0); });
- }
- }
-
- // Database actions on the power collection
- obj.getAllPower = function (func) { obj.powerfile.find({}).toArray(func); };
-
- // Bulk operations
- if (parent.config.settings.mongodbbulkoperations) {
- obj.storePowerEvent = function (event, multiServer, func) { // Fast MongoDB event store using bulkWrite()
- if (multiServer != null) { event.server = multiServer.serverid; }
- if (obj.powerFilePendingSet == false) {
- // Perform the operation now
- obj.dbCounters.powerSet++;
- obj.powerFilePendingSet = true; obj.powerFilePendingSets = null;
- if (func != null) { obj.powerFilePendingCbs = [func]; }
- obj.powerfile.bulkWrite([{ insertOne: { document: event } }], powerFileBulkWriteCompleted);
- } else {
- // Add this operation to the pending list
- obj.dbCounters.powerSetPending++;
- if (obj.powerFilePendingSets == null) { obj.powerFilePendingSets = [] }
- obj.powerFilePendingSets.push(event);
- if (func != null) { if (obj.powerFilePendingCb == null) { obj.powerFilePendingCb = [func]; } else { obj.powerFilePendingCb.push(func); } }
- }
- };
- } else {
- obj.storePowerEvent = function (event, multiServer, func) { obj.dbCounters.powerSet++; if (multiServer != null) { event.server = multiServer.serverid; } obj.powerfile.insertOne(event, func); };
- }
-
- obj.getPowerTimeline = function (nodeid, func) { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }).project({ _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }).toArray(func); };
- obj.removeAllPowerEvents = function () { obj.powerfile.deleteMany({}, { multi: true }); };
- obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; obj.powerfile.deleteMany({ nodeid: nodeid }, { multi: true }); };
-
- // Database actions on the SMBIOS collection
- obj.GetAllSMBIOS = function (func) { obj.smbiosfile.find({}).toArray(func); };
- obj.SetSMBIOS = function (smbios, func) { obj.smbiosfile.updateOne({ _id: smbios._id }, { $set: smbios }, { upsert: true }, func); };
- obj.RemoveSMBIOS = function (id) { obj.smbiosfile.deleteOne({ _id: id }); };
- obj.GetSMBIOS = function (id, func) { obj.smbiosfile.find({ _id: id }).toArray(func); };
-
- // Database actions on the Server Stats collection
- obj.SetServerStats = function (data, func) { obj.serverstatsfile.insertOne(data, func); };
- obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); obj.serverstatsfile.find({ time: { $gt: t } }, { _id: 0, cpu: 0 }).toArray(func); };
-
- // Read a configuration file from the database
- obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
-
- // Write a configuration file to the database
- obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
-
- // List all configuration files
- obj.listConfigFiles = function (func) { obj.file.find({ type: 'cfile' }).sort({ _id: 1 }).toArray(func); }
-
- // Get database information
- obj.getDbStats = function (func) {
- obj.stats = { c: 6 };
- obj.getStats(function (r) { obj.stats.recordTypes = r; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } })
- obj.file.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
- obj.eventsfile.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
- obj.powerfile.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
- obj.smbiosfile.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
- obj.serverstatsfile.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
- }
-
- // Correct database information of obj.getDbStats before returning it
- function getDbStatsEx(data) {
- var r = {};
- if (data.recordTypes != null) { r = data.recordTypes; }
- try { r.smbios = data['meshcentral.smbios'].count; } catch (ex) { }
- try { r.power = data['meshcentral.power'].count; } catch (ex) { }
- try { r.events = data['meshcentral.events'].count; } catch (ex) { }
- try { r.serverstats = data['meshcentral.serverstats'].count; } catch (ex) { }
- return r;
- }
-
- // Plugin operations
- if (obj.pluginsActive) {
- obj.addPlugin = function (plugin, func) { plugin.type = 'plugin'; obj.pluginsfile.insertOne(plugin, func); }; // Add a plugin
- obj.getPlugins = function (func) { obj.pluginsfile.find({ type: 'plugin' }).project({ type: 0 }).sort({ name: 1 }).toArray(func); }; // Get all plugins
- obj.getPlugin = function (id, func) { id = require('mongodb').ObjectId(id); obj.pluginsfile.find({ _id: id }).sort({ name: 1 }).toArray(func); }; // Get plugin
- obj.deletePlugin = function (id, func) { id = require('mongodb').ObjectId(id); obj.pluginsfile.deleteOne({ _id: id }, func); }; // Delete plugin
- obj.setPluginStatus = function (id, status, func) { id = require('mongodb').ObjectId(id); obj.pluginsfile.updateOne({ _id: id }, { $set: { status: status } }, func); };
- obj.updatePlugin = function (id, args, func) { delete args._id; id = require('mongodb').ObjectId(id); obj.pluginsfile.updateOne({ _id: id }, { $set: args }, func); };
- }
-
- } else {
- // Database actions on the main collection (NeDB and MongoJS)
- obj.Set = function (data, func) {
- obj.dbCounters.fileSet++;
- data = common.escapeLinksFieldNameEx(data);
- var xdata = performTypedRecordEncrypt(data); obj.file.update({ _id: xdata._id }, xdata, { upsert: true }, func);
- };
- obj.Get = function (id, func) {
- if (arguments.length > 2) {
- var parms = [func];
- for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
- var func2 = function _func2(arg1, arg2) {
- var userCallback = _func2.userArgs.shift();
- _func2.userArgs.unshift(arg2);
- _func2.userArgs.unshift(arg1);
- userCallback.apply(obj, _func2.userArgs);
- };
- func2.userArgs = parms;
- obj.file.find({ _id: id }, function (err, docs) {
- if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
- func2(err, performTypedRecordDecrypt(docs));
- });
- } else {
- obj.file.find({ _id: id }, function (err, docs) {
- if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
- func(err, performTypedRecordDecrypt(docs));
- });
- }
- };
- obj.GetAll = function (func) { obj.file.find({}, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetHash = function (id, func) { obj.file.find({ _id: id }, { _id: 0, hash: 1 }, func); };
- obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- //obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, domain, type, id, skip, limit, func) {
- //var x = { type: type, domain: domain, meshid: { $in: meshes } };
- //if (id) { x._id = id; }
- //obj.file.find(x, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
- //};
- obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
- if (extrasids == null) {
- const x = { type: type, domain: domain, meshid: { $in: meshes } };
- if (id) { x._id = id; }
- obj.file.find(x, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
- } else {
- const x = { type: type, domain: domain, $or: [{ meshid: { $in: meshes } }, { _id: { $in: extrasids } }] };
- if (id) { x._id = id; }
- obj.file.find(x, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
- }
- };
- obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
- var x = { type: type, domain: domain, nodeid: { $in: nodes } };
- if (id) { x._id = id; }
- obj.file.find(x, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
- };
- obj.GetAllType = function (type, func) { obj.file.find({ type: type }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.find({ type: type, domain: domain, _id: { $in: ids } }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetUserWithEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email }, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email, emailVerified: true }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.GetNodeByComputerName = function (domain, rname, func) { obj.file.find({ type: 'node', domain: domain, rname: rname }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
- obj.Remove = function (id, func) { obj.file.remove({ _id: id }, func); };
- obj.RemoveAll = function (func) { obj.file.remove({}, { multi: true }, func); };
- obj.RemoveAllOfType = function (type, func) { obj.file.remove({ type: type }, { multi: true }, func); };
- obj.InsertMany = function (data, func) { obj.file.insert(data, func); }; // Insert records directly, no link escaping
- obj.RemoveMeshDocuments = function (id) { obj.file.remove({ meshid: id }, { multi: true }); obj.file.remove({ _id: 'nt' + id }); };
- obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
- obj.DeleteDomain = function (domain, func) { obj.file.remove({ domain: domain }, { multi: true }, func); };
- obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
- obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
- obj.getLocalAmtNodes = function (func) { obj.file.find({ type: 'node', host: { $exists: true, $ne: null }, intelamt: { $exists: true } }, func); };
- obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { obj.file.find({ type: 'node', domain: domainid, mtype: mtype, 'intelamt.uuid': uuid }, func); };
- obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { obj.file.count({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); } }
-
- // Database actions on the events collection
- obj.GetAllEvents = function (func) { obj.eventsfile.find({}, func); };
- obj.StoreEvent = function (event, func) { obj.eventsfile.insert(event, func); };
- obj.GetEvents = function (ids, domain, filter, func) {
- var finddata = { domain: domain, ids: { $in: ids } };
- if (filter != null) finddata.action = filter;
- if (obj.databaseType == DB_NEDB) {
- obj.eventsfile.find(finddata, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func);
- } else {
- obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func);
- }
- };
- obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
- var finddata = { domain: domain, ids: { $in: ids } };
- if (filter != null) finddata.action = filter;
- if (obj.databaseType == DB_NEDB) {
- obj.eventsfile.find(finddata, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func);
- } else {
- obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func);
- }
- };
- obj.GetUserEvents = function (ids, domain, userid, filter, func) {
- var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
- if (filter != null) finddata.action = filter;
- if (obj.databaseType == DB_NEDB) {
- obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func);
- } else {
- obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func);
- }
- };
- obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
- var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
- if (filter != null) finddata.action = filter;
- if (obj.databaseType == DB_NEDB) {
- obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func);
- } else {
- obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func);
- }
- };
- obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
- if (obj.databaseType == DB_NEDB) {
- obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }], msgid: { $in: msgids }, time: { $gte: start, $lte: end } }, { type: 0, _id: 0, domain: 0, node: 0 }).sort({ time: 1 }).exec(func);
- } else {
- obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }], msgid: { $in: msgids }, time: { $gte: start, $lte: end } }, { type: 0, _id: 0, domain: 0, node: 0 }).sort({ time: 1 }, func);
- }
- };
- obj.GetUserLoginEvents = function (domain, userid, func) {
- if (obj.databaseType == DB_NEDB) {
- obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }, { action: 1, time: 1, msgid: 1, msgArgs: 1, tokenName: 1 }).sort({ time: -1 }).exec(func);
- } else {
- obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }, { action: 1, time: 1, msgid: 1, msgArgs: 1, tokenName: 1 }).sort({ time: -1 }, func);
- }
- };
- obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
- var finddata = { domain: domain, nodeid: nodeid };
- if (filter != null) finddata.action = filter;
- if (obj.databaseType == DB_NEDB) {
- obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).exec(func);
- } else {
- obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit, func);
- }
- };
- obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
- var finddata = { domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } };
- if (filter != null) finddata.action = filter;
- if (obj.databaseType == DB_NEDB) {
- obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).exec(func);
- } else {
- obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit, func);
- }
- };
- obj.RemoveAllEvents = function (domain) { obj.eventsfile.remove({ domain: domain }, { multi: true }); };
- obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; obj.eventsfile.remove({ domain: domain, nodeid: nodeid }, { multi: true }); };
- obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; obj.eventsfile.remove({ domain: domain, userid: userid }, { multi: true }); };
- obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) { obj.eventsfile.count({ action: 'authfail', userid: userid, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null) ? count : 0); }); }
-
- // Database actions on the power collection
- obj.getAllPower = function (func) { obj.powerfile.find({}, func); };
- obj.storePowerEvent = function (event, multiServer, func) { if (multiServer != null) { event.server = multiServer.serverid; } obj.powerfile.insert(event, func); };
- obj.getPowerTimeline = function (nodeid, func) { if (obj.databaseType == DB_NEDB) { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }, { _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }).exec(func); } else { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }, { _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }, func); } };
- obj.removeAllPowerEvents = function () { obj.powerfile.remove({}, { multi: true }); };
- obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; obj.powerfile.remove({ nodeid: nodeid }, { multi: true }); };
-
- // Database actions on the SMBIOS collection
- if (obj.smbiosfile != null) {
- obj.GetAllSMBIOS = function (func) { obj.smbiosfile.find({}, func); };
- obj.SetSMBIOS = function (smbios, func) { obj.smbiosfile.update({ _id: smbios._id }, smbios, { upsert: true }, func); };
- obj.RemoveSMBIOS = function (id) { obj.smbiosfile.remove({ _id: id }); };
- obj.GetSMBIOS = function (id, func) { obj.smbiosfile.find({ _id: id }, func); };
- }
-
- // Database actions on the Server Stats collection
- obj.SetServerStats = function (data, func) { obj.serverstatsfile.insert(data, func); };
- obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); obj.serverstatsfile.find({ time: { $gt: t } }, { _id: 0, cpu: 0 }, func); };
-
- // Read a configuration file from the database
- obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
-
- // Write a configuration file to the database
- obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
-
- // List all configuration files
- obj.listConfigFiles = function (func) { obj.file.find({ type: 'cfile' }).sort({ _id: 1 }).exec(func); }
-
- // Get database information
- obj.getDbStats = function (func) {
- obj.stats = { c: 5 };
- obj.getStats(function (r) { obj.stats.recordTypes = r; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } })
- obj.file.count({}, function (err, count) { obj.stats.meshcentral = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
- obj.eventsfile.count({}, function (err, count) { obj.stats.events = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
- obj.powerfile.count({}, function (err, count) { obj.stats.power = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
- obj.serverstatsfile.count({}, function (err, count) { obj.stats.serverstats = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
- }
-
- // Correct database information of obj.getDbStats before returning it
- function getDbStatsEx(data) {
- var r = {};
- if (data.recordTypes != null) { r = data.recordTypes; }
- try { r.smbios = data['smbios'].count; } catch (ex) { }
- try { r.power = data['power'].count; } catch (ex) { }
- try { r.events = data['events'].count; } catch (ex) { }
- try { r.serverstats = data['serverstats'].count; } catch (ex) { }
- return r;
- }
-
- // Plugin operations
- if (obj.pluginsActive) {
- obj.addPlugin = function (plugin, func) { plugin.type = 'plugin'; obj.pluginsfile.insert(plugin, func); }; // Add a plugin
- obj.getPlugins = function (func) { obj.pluginsfile.find({ 'type': 'plugin' }, { 'type': 0 }).sort({ name: 1 }).exec(func); }; // Get all plugins
- obj.getPlugin = function (id, func) { obj.pluginsfile.find({ _id: id }).sort({ name: 1 }).exec(func); }; // Get plugin
- obj.deletePlugin = function (id, func) { obj.pluginsfile.remove({ _id: id }, func); }; // Delete plugin
- obj.setPluginStatus = function (id, status, func) { obj.pluginsfile.update({ _id: id }, { $set: { status: status } }, func); };
- obj.updatePlugin = function (id, args, func) { delete args._id; obj.pluginsfile.update({ _id: id }, { $set: args }, func); };
- }
-
- }
-
- // Get all configuration files
- obj.getAllConfigFiles = function (password, func) {
- obj.GetAllType('cfile', function (err, docs) {
- if (err != null) { func(null); return; }
- var r = null;
- for (var i = 0; i < docs.length; i++) {
- var name = docs[i]._id.split('/')[1];
- var data = obj.decryptData(password, docs[i].data);
- if (data != null) { if (r == null) { r = {}; } r[name] = data; }
- }
- func(r);
- });
- }
-
- func(obj); // Completed function setup
- }
-
- // Return a human readable string with current backup configuration
- obj.getBackupConfig = function () {
- var r = '', backupPath = parent.backuppath;
-
- let dbname = 'meshcentral';
- if (parent.args.mongodbname) { dbname = parent.args.mongodbname; }
- else if ((typeof parent.args.mariadb == 'object') && (typeof parent.args.mariadb.database == 'string')) { dbname = parent.args.mariadb.database; }
- else if ((typeof parent.args.mysql == 'object') && (typeof parent.args.mysql.database == 'string')) { dbname = parent.args.mysql.database; }
- else if ((typeof parent.args.postgres == 'object') && (typeof parent.args.postgres.database == 'string')) { dbname = parent.args.postgres.database; }
- else if (typeof parent.config.settings.sqlite3 == 'string') {dbname = parent.config.settings.sqlite3 + '.sqlite'};
-
- const currentDate = new Date();
- const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2);
- obj.newAutoBackupFile = parent.config.settings.autobackup.backupname + fileSuffix;
-
- r += 'DB Name: ' + dbname + '\r\n';
- r += 'DB Type: ' + DB_LIST[obj.databaseType] + '\r\n';
-
- if (parent.config.settings.autobackup.backupintervalhours == -1) {
- r += 'Backup disabled\r\n';
- } else {
- r += 'BackupPath: ' + backupPath + '\r\n';
- r += 'BackupFile: ' + obj.newAutoBackupFile + '.zip\r\n';
-
- if (parent.config.settings.autobackup.backuphour != null && parent.config.settings.autobackup.backuphour != -1) {
- r += 'Backup between: ' + parent.config.settings.autobackup.backuphour + 'H-' + (parent.config.settings.autobackup.backuphour + 1) + 'H\r\n';
- }
- r += 'Backup Interval (Hours): ' + parent.config.settings.autobackup.backupintervalhours + '\r\n';
- if (parent.config.settings.autobackup.keeplastdaysbackup != null) {
- r += 'Keep Last Backups (Days): ' + parent.config.settings.autobackup.keeplastdaysbackup + '\r\n';
- }
- if (parent.config.settings.autobackup.zippassword != null) {
- r += 'ZIP Password: ';
- if (typeof parent.config.settings.autobackup.zippassword != 'string') { r += 'Bad zippassword type, Backups will not be encrypted\r\n'; }
- else if (parent.config.settings.autobackup.zippassword == "") { r += 'Blank zippassword, Backups will fail\r\n'; }
- else { r += 'Set\r\n'; }
- }
- if (parent.config.settings.autobackup.mongodumppath != null) {
- r += 'MongoDump Path: ';
- if (typeof parent.config.settings.autobackup.mongodumppath != 'string') { r += 'Bad mongodumppath type\r\n'; }
- else { r += parent.config.settings.autobackup.mongodumppath + '\r\n'; }
- }
- if (parent.config.settings.autobackup.mysqldumppath != null) {
- r += 'MySqlDump Path: ';
- if (typeof parent.config.settings.autobackup.mysqldumppath != 'string') { r += 'Bad mysqldump type\r\n'; }
- else { r += parent.config.settings.autobackup.mysqldumppath + '\r\n'; }
- }
- if (parent.config.settings.autobackup.pgdumppath != null) {
- r += 'pgDump Path: ';
- if (typeof parent.config.settings.autobackup.pgdumppath != 'string') { r += 'Bad pgdump type\r\n'; }
- else { r += parent.config.settings.autobackup.pgdumppath + '\r\n'; }
- }
- if (parent.config.settings.autobackup.backupotherfolders) {
- r += 'Backup other folders: ';
- r += parent.filespath + ', ' + parent.recordpath + '\r\n';
- }
- if (parent.config.settings.autobackup.backupwebfolders) {
- r += 'Backup webfolders: ';
- if (parent.webViewsOverridePath) {r += parent.webViewsOverridePath };
- if (parent.webPublicOverridePath) {r += ', '+ parent.webPublicOverridePath};
- if (parent.webEmailsOverridePath) {r += ',' + parent.webEmailsOverridePath};
- r+= '\r\n';
- }
- if (parent.config.settings.autobackup.backupignorefilesglob != []) {
- r += 'Backup IgnoreFilesGlob: ';
- { r += parent.config.settings.autobackup.backupignorefilesglob + '\r\n'; }
- }
- if (parent.config.settings.autobackup.backupskipfoldersglob != []) {
- r += 'Backup SkipFoldersGlob: ';
- { r += parent.config.settings.autobackup.backupskipfoldersglob + '\r\n'; }
- }
-
- if (typeof parent.config.settings.autobackup.s3 == 'object') {
- r += 'S3 Backups: Enabled\r\n';
- }
- if (typeof parent.config.settings.autobackup.webdav == 'object') {
- r += 'WebDAV Backups: Enabled\r\n';
- r += 'WebDAV backup path: ' + ((typeof parent.config.settings.autobackup.webdav.foldername == 'string') ? parent.config.settings.autobackup.webdav.foldername : 'MeshCentral-Backups') + '\r\n';
- r += 'WebDAV maximum files: '+ ((typeof parent.config.settings.autobackup.webdav.maxfiles == 'number') ? parent.config.settings.autobackup.webdav.maxfiles : 'no limit') + '\r\n';
- }
- if (typeof parent.config.settings.autobackup.googledrive == 'object') {
- r += 'Google Drive Backups: Enabled\r\n';
- }
-
-
- }
-
- return r;
- }
-
- function buildSqlDumpCommand() {
- var props = (obj.databaseType == DB_MARIADB) ? parent.args.mariadb : parent.args.mysql;
-
- var mysqldumpPath = 'mysqldump';
- if (parent.config.settings.autobackup && parent.config.settings.autobackup.mysqldumppath) {
- mysqldumpPath = path.normalize(parent.config.settings.autobackup.mysqldumppath);
- }
-
- var cmd = '\"' + mysqldumpPath + '\" --user=\'' + props.user + '\'';
- // Windows will treat ' as part of the pw. Linux/Unix requires it to escape.
- cmd += (parent.platform == 'win32') ? ' --password=\"' + props.password + '\"' : ' --password=\'' + props.password + '\'';
- if (props.host) { cmd += ' -h ' + props.host; }
- if (props.port) { cmd += ' -P ' + props.port; }
-
- if (props.awsrds) { cmd += ' --single-transaction'; }
-
- // SSL options different on mariadb/mysql
- var sslOptions = '';
- if (obj.databaseType == DB_MARIADB) {
- if (props.ssl) {
- sslOptions = ' --ssl';
- if (props.ssl.cacertpath) sslOptions = ' --ssl-ca=' + props.ssl.cacertpath;
- if (props.ssl.dontcheckserveridentity != true) {sslOptions += ' --ssl-verify-server-cert'} else {sslOptions += ' --ssl-verify-server-cert=false'};
- if (props.ssl.clientcertpath) sslOptions += ' --ssl-cert=' + props.ssl.clientcertpath;
- if (props.ssl.clientkeypath) sslOptions += ' --ssl-key=' + props.ssl.clientkeypath;
- }
- } else {
- if (props.ssl) {
- sslOptions = ' --ssl-mode=required';
- if (props.ssl.cacertpath) sslOptions = ' --ssl-ca=' + props.ssl.cacertpath;
- if (props.ssl.dontcheckserveridentity != true) sslOptions += ' --ssl-mode=verify_identity';
- else sslOptions += ' --ssl-mode=required';
- if (props.ssl.clientcertpath) sslOptions += ' --ssl-cert=' + props.ssl.clientcertpath;
- if (props.ssl.clientkeypath) sslOptions += ' --ssl-key=' + props.ssl.clientkeypath;
- }
- }
- cmd += sslOptions;
-
- var dbname = (props.database) ? props.database : 'meshcentral';
- cmd += ' ' + dbname
-
- return cmd;
- }
-
- function buildMongoDumpCommand() {
- const dburl = parent.args.mongodb;
-
- var mongoDumpPath = 'mongodump';
- if (parent.config.settings.autobackup && parent.config.settings.autobackup.mongodumppath) {
- mongoDumpPath = path.normalize(parent.config.settings.autobackup.mongodumppath);
- }
-
- var cmd = '"' + mongoDumpPath + '"';
- if (dburl) { cmd = '\"' + mongoDumpPath + '\" --uri=\"' + dburl + '\"'; }
- if (parent.config.settings.autobackup?.mongodumpargs) {
- cmd = '\"' + mongoDumpPath + '\" ' + parent.config.settings.autobackup.mongodumpargs;
- if (!parent.config.settings.autobackup.mongodumpargs.includes("--db=")) {cmd += ' --db=' + (parent.config.settings.mongodbname ? parent.config.settings.mongodbname : 'meshcentral')};
- }
- return cmd;
- }
-
- // Check that the server is capable of performing a backup
- // Tries configured custom location with fallback to default location
- // Now runs after autobackup config init in meshcentral.js so config options are checked
- obj.checkBackupCapability = function (func) {
- if (parent.config.settings.autobackup.backupintervalhours == -1) { return; };
- //block backup until validated. Gets put back if all checks are ok.
- let backupInterval = parent.config.settings.autobackup.backupintervalhours;
- parent.config.settings.autobackup.backupintervalhours = -1;
- let backupPath = parent.backuppath;
-
- if (backupPath.startsWith(parent.datapath)) {
- func(1, "Backup path can't be set within meshcentral-data folder. No backups will be made.");
- return;
- }
- // Check create/write backupdir
- try { fs.mkdirSync(backupPath); }
- catch (e) {
- // EEXIST error = dir already exists
- if (e.code != 'EEXIST' ) {
- //Unable to create backuppath
- console.error(e.message);
- func(1, 'Unable to create ' + backupPath + '. No backups will be made. Error: ' + e.message);
- return;
- }
- }
- const currentDate = new Date();
- const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2);
- const testFile = path.join(backupPath, parent.config.settings.autobackup.backupname + fileSuffix + '.zip');
- try { fs.writeFileSync( testFile, "DeleteMe"); }
- catch (e) {
- //Unable to create file
- console.error (e.message);
- func(1, "Backuppath (" + backupPath + ") can't be written to. No backups will be made. Error: " + e.message);
- return;
- }
- try { fs.unlinkSync(testFile); parent.debug('backup', 'Backuppath ' + backupPath + ' accesscheck successful');}
- catch (e) {
- console.error (e.message);
- func(1, "Backuppathtestfile (" + testFile + ") can't be deleted, check filerights. Error: " + e.message);
- // Assume write rights, no delete rights. Continue with warning.
- //return;
- }
-
- // Check database dumptools
- if ((obj.databaseType == DB_MONGOJS) || (obj.databaseType == DB_MONGODB)) {
- // Check that we have access to MongoDump
- var cmd = buildMongoDumpCommand();
- cmd += (parent.platform == 'win32') ? ' --archive=\"nul\"' : ' --archive=\"/dev/null\"';
- const child_process = require('child_process');
- child_process.exec(cmd, { cwd: backupPath }, function (error, stdout, stderr) {
- if ((error != null) && (error != '')) {
- func(1, "Mongodump error, backup will not be performed. Check path or use mongodumppath & mongodumpargs");
- return;
- } else {parent.config.settings.autobackup.backupintervalhours = backupInterval;}
- });
- } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
- // Check that we have access to mysqldump
- var cmd = buildSqlDumpCommand();
- cmd += ' > ' + ((parent.platform == 'win32') ? '\"nul\"' : '\"/dev/null\"');
- const child_process = require('child_process');
- child_process.exec(cmd, { cwd: backupPath, timeout: 1000*30 }, function(error, stdout, stdin) {
- if ((error != null) && (error != '')) {
- func(1, "mysqldump error, backup will not be performed. Check path or use mysqldumppath");
- return;
- } else {parent.config.settings.autobackup.backupintervalhours = backupInterval;}
-
- });
- } else if (obj.databaseType == DB_POSTGRESQL) {
- // Check that we have access to pg_dump
- parent.config.settings.autobackup.pgdumppath = path.normalize(parent.config.settings.autobackup.pgdumppath ? parent.config.settings.autobackup.pgdumppath : 'pg_dump');
- let cmd = '"' + parent.config.settings.autobackup.pgdumppath + '"'
- + ' --dbname=postgresql://' + parent.config.settings.postgres.user + ":" +parent.config.settings.postgres.password
- + "@" + parent.config.settings.postgres.host + ":" + parent.config.settings.postgres.port + "/" + databaseName
- + ' > ' + ((parent.platform == 'win32') ? '\"nul\"' : '\"/dev/null\"');
- const child_process = require('child_process');
- child_process.exec(cmd, { cwd: backupPath }, function(error, stdout, stdin) {
- if ((error != null) && (error != '')) {
- func(1, "pg_dump error, backup will not be performed. Check path or use pgdumppath.");
- return;
- } else {parent.config.settings.autobackup.backupintervalhours = backupInterval;}
- });
- } else {
- //all ok, enable backup
- parent.config.settings.autobackup.backupintervalhours = backupInterval;}
- }
-
- // MongoDB pending bulk read operation, perform fast bulk document reads.
- function fileBulkReadCompleted(err, docs) {
- // Send out callbacks with results
- if (docs != null) {
- for (var i in docs) {
- if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); }
- const id = docs[i]._id;
- if (obj.filePendingGets[id] != null) {
- for (var j in obj.filePendingGets[id]) {
- if (typeof obj.filePendingGets[id][j] == 'function') { obj.filePendingGets[id][j](err, performTypedRecordDecrypt([docs[i]])); }
- }
- delete obj.filePendingGets[id];
- }
- }
- }
-
- // If there are not results, send out a null callback
- for (var i in obj.filePendingGets) { for (var j in obj.filePendingGets[i]) { obj.filePendingGets[i][j](err, []); } }
-
- // Move on to process any more pending get operations
- obj.filePendingGets = obj.filePendingGet;
- obj.filePendingGet = null;
- if (obj.filePendingGets != null) {
- var findlist = [];
- for (var i in obj.filePendingGets) { findlist.push(i); }
- obj.file.find({ _id: { $in: findlist } }).toArray(fileBulkReadCompleted);
- }
- }
-
- // MongoDB pending bulk remove operation, perform fast bulk document removes.
- function fileBulkRemoveCompleted(err) {
- // Send out callbacks
- for (var i in obj.filePendingRemoves) {
- for (var j in obj.filePendingRemoves[i]) {
- if (typeof obj.filePendingRemoves[i][j] == 'function') { obj.filePendingRemoves[i][j](err); }
- }
- }
-
- // Move on to process any more pending get operations
- obj.filePendingRemoves = obj.filePendingRemove;
- obj.filePendingRemove = null;
- if (obj.filePendingRemoves != null) {
- obj.dbCounters.fileRemoveBulk++;
- var findlist = [], count = 0;
- for (var i in obj.filePendingRemoves) { findlist.push(i); count++; }
- obj.file.deleteMany({ _id: { $in: findlist } }, { multi: true }, fileBulkRemoveCompleted);
- }
- }
-
- // MongoDB pending bulk write operation, perform fast bulk document replacement.
- function fileBulkWriteCompleted() {
- // Callbacks
- if (obj.filePendingCbs != null) {
- for (var i in obj.filePendingCbs) { if (typeof obj.filePendingCbs[i] == 'function') { obj.filePendingCbs[i](); } }
- obj.filePendingCbs = null;
- }
- if (obj.filePendingSets != null) {
- // Perform pending operations
- obj.dbCounters.fileSetBulk++;
- var ops = [];
- obj.filePendingCbs = obj.filePendingCb;
- obj.filePendingCb = null;
- for (var i in obj.filePendingSets) { ops.push({ replaceOne: { filter: { _id: i }, replacement: performTypedRecordEncrypt(common.escapeLinksFieldNameEx(obj.filePendingSets[i])), upsert: true } }); }
- obj.file.bulkWrite(ops, fileBulkWriteCompleted);
- obj.filePendingSets = null;
- } else {
- // All done, no pending operations.
- obj.filePendingSet = false;
- }
- }
-
- // MongoDB pending bulk write operation, perform fast bulk document replacement.
- function eventsFileBulkWriteCompleted() {
- // Callbacks
- if (obj.eventsFilePendingCbs != null) { for (var i in obj.eventsFilePendingCbs) { obj.eventsFilePendingCbs[i](); } obj.eventsFilePendingCbs = null; }
- if (obj.eventsFilePendingSets != null) {
- // Perform pending operations
- obj.dbCounters.eventsSetBulk++;
- var ops = [];
- for (var i in obj.eventsFilePendingSets) { ops.push({ insertOne: { document: obj.eventsFilePendingSets[i] } }); }
- obj.eventsFilePendingCbs = obj.eventsFilePendingCb;
- obj.eventsFilePendingCb = null;
- obj.eventsFilePendingSets = null;
- obj.eventsfile.bulkWrite(ops, eventsFileBulkWriteCompleted);
- } else {
- // All done, no pending operations.
- obj.eventsFilePendingSet = false;
- }
- }
-
- // MongoDB pending bulk write operation, perform fast bulk document replacement.
- function powerFileBulkWriteCompleted() {
- // Callbacks
- if (obj.powerFilePendingCbs != null) { for (var i in obj.powerFilePendingCbs) { obj.powerFilePendingCbs[i](); } obj.powerFilePendingCbs = null; }
- if (obj.powerFilePendingSets != null) {
- // Perform pending operations
- obj.dbCounters.powerSetBulk++;
- var ops = [];
- for (var i in obj.powerFilePendingSets) { ops.push({ insertOne: { document: obj.powerFilePendingSets[i] } }); }
- obj.powerFilePendingCbs = obj.powerFilePendingCb;
- obj.powerFilePendingCb = null;
- obj.powerFilePendingSets = null;
- obj.powerfile.bulkWrite(ops, powerFileBulkWriteCompleted);
- } else {
- // All done, no pending operations.
- obj.powerFilePendingSet = false;
- }
- }
-
- // Perform a server backup
- obj.performBackup = function (func) {
- parent.debug('backup','Entering performBackup');
- try {
- if (obj.performingBackup) return 'Backup alreay in progress.';
- if (parent.config.settings.autobackup.backupintervalhours == -1) { if (func) { func('Backup disabled.'); return 'Backup disabled.' }};
- obj.performingBackup = true;
- let backupPath = parent.backuppath;
- let dataPath = parent.datapath;
-
- const currentDate = new Date();
- const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2);
- obj.newAutoBackupFile = path.join(backupPath, parent.config.settings.autobackup.backupname + fileSuffix + '.zip');
- parent.debug('backup','newAutoBackupFile=' + obj.newAutoBackupFile);
-
- if ((obj.databaseType == DB_MONGOJS) || (obj.databaseType == DB_MONGODB)) {
- // Perform a MongoDump
- const dbname = (parent.args.mongodbname) ? (parent.args.mongodbname) : 'meshcentral';
- const dburl = parent.args.mongodb;
-
- obj.newDBDumpFile = path.join(backupPath, (dbname + '-mongodump-' + fileSuffix + '.archive'));
-
- var cmd = buildMongoDumpCommand();
- cmd += (dburl) ? ' --archive=\"' + obj.newDBDumpFile + '\"' :
- ' --db=\"' + dbname + '\" --archive=\"' + obj.newDBDumpFile + '\"';
- parent.debug('backup','Mongodump cmd: ' + cmd);
- const child_process = require('child_process');
- const dumpProcess = child_process.exec(
- cmd,
- { cwd: parent.parentpath },
- (error)=> {if (error) {obj.backupStatus |= BACKUPFAIL_DBDUMP; console.error('ERROR: Unable to perform MongoDB backup: ' + error + '\r\n'); obj.createBackupfile(func);}}
- );
-
- dumpProcess.on('exit', (code) => {
- if (code != 0) {console.log(`Mongodump child process exited with code ${code}`); obj.backupStatus |= BACKUPFAIL_DBDUMP;}
- obj.createBackupfile(func);
- });
-
- } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
- // Perform a MySqlDump backup
- const newBackupFile = 'mysqldump-' + fileSuffix;
- obj.newDBDumpFile = path.join(backupPath, newBackupFile + '.sql');
-
- var cmd = buildSqlDumpCommand();
- cmd += ' --result-file=\"' + obj.newDBDumpFile + '\"';
- parent.debug('backup','Maria/MySQLdump cmd: ' + cmd);
-
- const child_process = require('child_process');
- const dumpProcess = child_process.exec(
- cmd,
- { cwd: parent.parentpath },
- (error)=> {if (error) {obj.backupStatus |= BACKUPFAIL_DBDUMP; console.error('ERROR: Unable to perform MySQL backup: ' + error + '\r\n'); obj.createBackupfile(func);}}
- );
- dumpProcess.on('exit', (code) => {
- if (code != 0) {console.error(`MySQLdump child process exited with code ${code}`); obj.backupStatus |= BACKUPFAIL_DBDUMP;}
- obj.createBackupfile(func);
- });
-
- } else if (obj.databaseType == DB_SQLITE) {
- //.db3 suffix to escape escape backupfile glob to exclude the sqlite db files
- obj.newDBDumpFile = path.join(backupPath, databaseName + '-sqlitedump-' + fileSuffix + '.db3');
- // do a VACUUM INTO in favor of the backup API to compress the export, see https://www.sqlite.org/backup.html
- parent.debug('backup','SQLitedump: VACUUM INTO ' + obj.newDBDumpFile);
- obj.file.exec('VACUUM INTO \'' + obj.newDBDumpFile + '\'', function (err) {
- if (err) { console.error('SQLite backup error: ' + err); obj.backupStatus |=BACKUPFAIL_DBDUMP;};
- //always finish/clean up
- obj.createBackupfile(func);
- });
- } else if (obj.databaseType == DB_POSTGRESQL) {
- // Perform a PostgresDump backup
- const newBackupFile = databaseName + '-pgdump-' + fileSuffix + '.sql';
- obj.newDBDumpFile = path.join(backupPath, newBackupFile);
- let cmd = '"' + parent.config.settings.autobackup.pgdumppath + '"'
- + ' --dbname=postgresql://' + parent.config.settings.postgres.user + ":" +parent.config.settings.postgres.password
- + "@" + parent.config.settings.postgres.host + ":" + parent.config.settings.postgres.port + "/" + databaseName
- + " --file=" + obj.newDBDumpFile;
- parent.debug('backup','Postgresqldump cmd: ' + cmd);
- const child_process = require('child_process');
- const dumpProcess = child_process.exec(
- cmd,
- { cwd: dataPath },
- (error)=> {if (error) {obj.backupStatus |= BACKUPFAIL_DBDUMP; console.log('ERROR: Unable to perform PostgreSQL dump: ' + error.message + '\r\n'); obj.createBackupfile(func);}}
- );
- dumpProcess.on('exit', (code) => {
- if (code != 0) {console.log(`PostgreSQLdump child process exited with code: ` + code); obj.backupStatus |= BACKUPFAIL_DBDUMP;}
- obj.createBackupfile(func);
- });
- } else {
- // NeDB/Acebase backup, no db dump needed, just make a file backup
- obj.createBackupfile(func);
- }
- } catch (ex) { console.error(ex); parent.addServerWarning( 'Something went wrong during performBackup, check errorlog: ' +ex.message, true); };
- return 'Starting auto-backup...';
- };
-
- obj.createBackupfile = function(func) {
- parent.debug('backup', 'Entering createBackupfile');
- let archiver = require('archiver');
- let archive = null;
- let zipLevel = Math.min(Math.max(Number(parent.config.settings.autobackup.zipcompression ? parent.config.settings.autobackup.zipcompression : 5),1),9);
-
- //if password defined, create encrypted zip
- if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.zippassword == 'string')) {
- try {
- //Only register format once, otherwise it triggers an error
- if (archiver.isRegisteredFormat('zip-encrypted') == false) { archiver.registerFormat('zip-encrypted', require('archiver-zip-encrypted')); }
- archive = archiver.create('zip-encrypted', { zlib: { level: zipLevel }, encryptionMethod: 'aes256', password: parent.config.settings.autobackup.zippassword });
- if (func) { func('Creating encrypted ZIP'); }
- } catch (ex) { // registering encryption failed, do not fall back to non-encrypted, fail backup and skip old backup removal as a precaution to not lose any backups
- obj.backupStatus |= BACKUPFAIL_ZIPMODULE;
- if (func) { func('Zipencryptionmodule failed, aborting');}
- console.error('Zipencryptionmodule failed, aborting');
- }
- } else {
- if (func) { func('Creating a NON-ENCRYPTED ZIP'); }
- archive = archiver('zip', { zlib: { level: zipLevel } });
- }
-
- //original behavior, just a filebackup if dbdump fails : (obj.backupStatus == 0 || obj.backupStatus == BACKUPFAIL_DBDUMP)
- if (obj.backupStatus == 0) {
- // Zip the data directory with the dbdump|NeDB files
- let output = fs.createWriteStream(obj.newAutoBackupFile);
-
- // Archive finalized and closed
- output.on('close', function () {
- if (obj.backupStatus == 0) {
- let mesg = 'Auto-backup completed: ' + obj.newAutoBackupFile + ', backup-size: ' + ((archive.pointer() / 1048576).toFixed(2)) + "Mb";
- console.log(mesg);
- if (func) { func(mesg); };
- obj.performCloudBackup(obj.newAutoBackupFile, func);
- obj.removeExpiredBackupfiles(func);
-
- } else {
- let mesg = 'Zipbackup failed (' + obj.backupStatus.toString(2).slice(-8) + '), deleting incomplete backup: ' + obj.newAutoBackupFile;
- if (func) { func(mesg) }
- else { parent.addServerWarning(mesg, true ) };
- if (fs.existsSync(obj.newAutoBackupFile)) { fs.unlink(obj.newAutoBackupFile, function (err) { if (err) {console.error('Failed to clean up backupfile: ' + err.message)} }) };
- };
- if (obj.databaseType != DB_NEDB) {
- //remove dump archive file, because zipped and otherwise fills up
- if (fs.existsSync(obj.newDBDumpFile)) { fs.unlink(obj.newDBDumpFile, function (err) { if (err) {console.error('Failed to clean up dbdump file: ' + err.message) } }) };
- };
- obj.performingBackup = false;
- obj.backupStatus = 0x0;
- }
- );
- output.on('end', function () { });
- output.on('error', function (err) {
- if ((obj.backupStatus & BACKUPFAIL_ZIPCREATE) == 0) {
- console.error('Output error: ' + err.message);
- if (func) { func('Output error: ' + err.message); };
- obj.backupStatus |= BACKUPFAIL_ZIPCREATE;
- archive.abort();
- };
- });
- archive.on('warning', function (err) {
- //if files added to the archiver object aren't reachable anymore (e.g. sqlite-journal files)
- //an ENOENT warning is given, but the archiver module has no option to/does not skip/resume
- //so the backup needs te be aborted as it otherwise leaves an incomplete zip and never 'ends'
- if ((obj.backupStatus & BACKUPFAIL_ZIPCREATE) == 0) {
- console.log('Zip warning: ' + err.message);
- if (func) { func('Zip warning: ' + err.message); };
- obj.backupStatus |= BACKUPFAIL_ZIPCREATE;
- archive.abort();
- };
- });
- archive.on('error', function (err) {
- if ((obj.backupStatus & BACKUPFAIL_ZIPCREATE) == 0) {
- console.error('Zip error: ' + err.message);
- if (func) { func('Zip error: ' + err.message); };
- obj.backupStatus |= BACKUPFAIL_ZIPCREATE;
- archive.abort();
- }
- });
- archive.pipe(output);
-
- let globIgnoreFiles;
- //slice in case exclusion gets pushed
- globIgnoreFiles = parent.config.settings.autobackup.backupignorefilesglob ? parent.config.settings.autobackup.backupignorefilesglob.slice() : [];
- if (parent.config.settings.sqlite3) { globIgnoreFiles.push (datapathFoldername + '/' + databaseName + '.sqlite*'); }; //skip sqlite database file, and temp files with ext -journal, -wal & -shm
- //archiver.glob doesn't seem to use the third param, archivesubdir. Bug?
- //workaround: go up a dir and add data dir explicitly to keep the zip tidy
- archive.glob((datapathFoldername + '/**'), {
- cwd: datapathParentPath,
- ignore: globIgnoreFiles,
- skip: (parent.config.settings.autobackup.backupskipfoldersglob ? parent.config.settings.autobackup.backupskipfoldersglob : [])
- });
-
- if (parent.config.settings.autobackup.backupwebfolders) {
- if (parent.webViewsOverridePath) { archive.directory(parent.webViewsOverridePath, 'meshcentral-views'); }
- if (parent.webPublicOverridePath) { archive.directory(parent.webPublicOverridePath, 'meshcentral-public'); }
- if (parent.webEmailsOverridePath) { archive.directory(parent.webEmailsOverridePath, 'meshcentral-emails'); }
- };
- if (parent.config.settings.autobackup.backupotherfolders) {
- archive.directory(parent.filespath, 'meshcentral-files');
- archive.directory(parent.recordpath, 'meshcentral-recordings');
- };
- //add dbdump to the root of the zip
- if (obj.newDBDumpFile != null) archive.file(obj.newDBDumpFile, { name: path.basename(obj.newDBDumpFile) });
- archive.finalize();
- } else {
- //failed somewhere before zipping
- console.error('Backup failed ('+ obj.backupStatus.toString(2).slice(-8) + ')');
- if (func) { func('Backup failed ('+ obj.backupStatus.toString(2).slice(-8) + ')') }
- else {
- parent.addServerWarning('Backup failed ('+ obj.backupStatus.toString(2).slice(-8) + ')', true);
- }
- //Just in case something's there
- if (fs.existsSync(obj.newDBDumpFile)) { fs.unlink(obj.newDBDumpFile, function (err) { if (err) {console.error('Failed to clean up dbdump file: ' + err.message) } }); };
- obj.backupStatus = 0x0;
- obj.performingBackup = false;
- };
- };
-
- // Remove expired backupfiles by filenamedate
- obj.removeExpiredBackupfiles = function (func) {
- if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.keeplastdaysbackup == 'number')) {
- let cutoffDate = new Date();
- cutoffDate.setDate(cutoffDate.getDate() - parent.config.settings.autobackup.keeplastdaysbackup);
- fs.readdir(parent.backuppath, function (err, dir) {
- try {
- if (err == null) {
- if (dir.length > 0) {
- let fileName = parent.config.settings.autobackup.backupname;
- let checked = 0;
- let removed = 0;
- for (var i in dir) {
- var name = dir[i];
- parent.debug('backup', "checking file: ", path.join(parent.backuppath, name));
- if (name.startsWith(fileName) && name.endsWith('.zip')) {
- var timex = name.substring(fileName.length, name.length - 4).split('-');
- if (timex.length == 5) {
- checked++;
- var fileDate = new Date(parseInt(timex[0]), parseInt(timex[1]) - 1, parseInt(timex[2]), parseInt(timex[3]), parseInt(timex[4]));
- if (fileDate && (cutoffDate > fileDate)) {
- console.log("Removing expired backup file: ", path.join(parent.backuppath, name));
- fs.unlink(path.join(parent.backuppath, name), function (err) { if (err) { console.error(err.message); if (func) {func('Error removing: ' + err.message); } } });
- removed++;
- }
- }
- else { parent.debug('backup', "file: " + name + " timestamp failure: ", timex); }
- }
- }
- let mesg= 'Checked ' + checked + ' candidates in ' + parent.backuppath + '. Removed ' + removed + ' expired backupfiles using cutoffDate: '+ cutoffDate.toLocaleString('default', { dateStyle: 'short', timeStyle: 'short' });
- parent.debug (mesg);
- if (func) { func(mesg); }
- } else { console.error('No files found in ' + parent.backuppath + '. There should be at least one.')}
- }
- else
- { console.error(err); parent.addServerWarning( 'Reading files in backup directory ' + parent.backuppath + ' failed, check errorlog: ' + err.message, true); }
- } catch (ex) { console.error(ex); parent.addServerWarning( 'Something went wrong during removeExpiredBackupfiles, check errorlog: ' +ex.message, true); }
- });
- }
- }
-
- async function webDAVBackup(filename, func) {
- try {
- const webDAV = await import ('webdav');
- const wdConfig = parent.config.settings.autobackup.webdav;
- const client = webDAV.createClient(wdConfig.url, {
- username: wdConfig.username,
- password: wdConfig.password,
- maxContentLength: Infinity,
- maxBodyLength: Infinity
- });
- if (await client.exists(wdConfig.foldername) === false) {
- await client.createDirectory(wdConfig.foldername, { recursive: true});
- } else {
- // Clean up our WebDAV folder
- if ((typeof wdConfig.maxfiles == 'number') && (wdConfig.maxfiles > 1)) {
- const fileName = parent.config.settings.autobackup.backupname;
- //only files matching our backupfilename
- let files = await client.getDirectoryContents(wdConfig.foldername, { deep: false, glob: "/**/" + fileName + "*.zip" });
- const xdateTimeSort = function (a, b) { if (a.xdate > b.xdate) return 1; if (a.xdate < b.xdate) return -1; return 0; }
- for (const i in files) { files[i].xdate = new Date(files[i].lastmod); }
- files.sort(xdateTimeSort);
- while (files.length >= wdConfig.maxfiles) {
- let delFile = files.shift().filename;
- await client.deleteFile(delFile);
- console.log('WebDAV file deleted: ' + delFile); if (func) { func('WebDAV file deleted: ' + delFile); }
- }
- }
- }
- // Upload to the WebDAV folder
- const { pipeline } = require('stream/promises');
- await pipeline(fs.createReadStream(filename), client.createWriteStream( wdConfig.foldername + path.basename(filename)));
- console.log('WebDAV upload completed: ' + wdConfig.foldername + path.basename(filename)); if (func) { func('WebDAV upload completed: ' + wdConfig.foldername + path.basename(filename)); }
- }
- catch(err) {
- console.error('WebDAV error: ' + err.message); if (func) { func('WebDAV error: ' + err.message);}
- }
- }
-
- // Perform cloud backup
- obj.performCloudBackup = function (filename, func) {
- // WebDAV Backup
- if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.webdav == 'object')) {
- parent.debug( 'backup', 'Entering WebDAV backup'); if (func) { func('Entering WebDAV backup.'); }
- webDAVBackup(filename, func);
- }
-
- // Google Drive Backup
- if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.googledrive == 'object')) {
- parent.debug( 'backup', 'Entering Google Drive backup');
- obj.Get('GoogleDriveBackup', function (err, docs) {
- if ((err != null) || (docs.length != 1) || (docs[0].state != 3)) return;
- if (func) { func('Attempting Google Drive upload...'); }
- const {google} = require('googleapis');
- const oAuth2Client = new google.auth.OAuth2(docs[0].clientid, docs[0].clientsecret, "urn:ietf:wg:oauth:2.0:oob");
- oAuth2Client.on('tokens', function (tokens) { if (tokens.refresh_token) { docs[0].token = tokens.refresh_token; parent.db.Set(docs[0]); } }); // Update the token in the database
- oAuth2Client.setCredentials(docs[0].token);
- const drive = google.drive({ version: 'v3', auth: oAuth2Client });
- const createdTimeSort = function (a, b) { if (a.createdTime > b.createdTime) return 1; if (a.createdTime < b.createdTime) return -1; return 0; }
-
- // Called once we know our folder id, clean up and upload a backup.
- var useGoogleDrive = function (folderid) {
- // List files to see if we need to delete older ones
- if (typeof parent.config.settings.autobackup.googledrive.maxfiles == 'number') {
- drive.files.list({
- q: 'trashed = false and \'' + folderid + '\' in parents',
- fields: 'nextPageToken, files(id, name, size, createdTime)',
- }, function (err, res) {
- if (err) {
- console.log('GoogleDrive (files.list) error: ' + err);
- if (func) { func('GoogleDrive (files.list) error: ' + err); }
- return;
- }
- // Delete any old files if more than 10 files are present in the backup folder.
- res.data.files.sort(createdTimeSort);
- while (res.data.files.length >= parent.config.settings.autobackup.googledrive.maxfiles) { drive.files.delete({ fileId: res.data.files.shift().id }, function (err, res) { }); }
- });
- }
-
- //console.log('Uploading...');
- if (func) { func('Uploading to Google Drive...'); }
-
- // Upload the backup
- drive.files.create({
- requestBody: { name: require('path').basename(filename), mimeType: 'text/plain', parents: [folderid] },
- media: { mimeType: 'application/zip', body: require('fs').createReadStream(filename) },
- }, function (err, res) {
- if (err) {
- console.log('GoogleDrive (files.create) error: ' + err);
- if (func) { func('GoogleDrive (files.create) error: ' + err); }
- return;
- }
- //console.log('Upload done.');
- if (func) { func('Google Drive upload completed.'); }
- });
- }
-
- // Fetch the folder name
- var folderName = 'MeshCentral-Backups';
- if (typeof parent.config.settings.autobackup.googledrive.foldername == 'string') { folderName = parent.config.settings.autobackup.googledrive.foldername; }
-
- // Find our backup folder, create one if needed.
- drive.files.list({
- q: 'mimeType = \'application/vnd.google-apps.folder\' and name=\'' + folderName + '\' and trashed = false',
- fields: 'nextPageToken, files(id, name)',
- }, function (err, res) {
- if (err) {
- console.log('GoogleDrive error: ' + err);
- if (func) { func('GoogleDrive error: ' + err); }
- return;
- }
- if (res.data.files.length == 0) {
- // Create a folder
- drive.files.create({ resource: { 'name': folderName, 'mimeType': 'application/vnd.google-apps.folder' }, fields: 'id' }, function (err, file) {
- if (err) {
- console.log('GoogleDrive (folder.create) error: ' + err);
- if (func) { func('GoogleDrive (folder.create) error: ' + err); }
- return;
- }
- useGoogleDrive(file.data.id);
- });
- } else { useGoogleDrive(res.data.files[0].id); }
- });
- });
- }
-
- // S3 Backup
- if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.s3 == 'object')) {
- parent.debug( 'backup', 'Entering S3 backup');
- var s3folderName = 'MeshCentral-Backups';
- if (typeof parent.config.settings.autobackup.s3.foldername == 'string') { s3folderName = parent.config.settings.autobackup.s3.foldername; }
- // Construct the config object
- var accessKey = parent.config.settings.autobackup.s3.accesskey,
- secretKey = parent.config.settings.autobackup.s3.secretkey,
- endpoint = parent.config.settings.autobackup.s3.endpoint ? parent.config.settings.autobackup.s3.endpoint : 's3.amazonaws.com',
- port = parent.config.settings.autobackup.s3.port ? parent.config.settings.autobackup.s3.port : 443,
- useSsl = parent.config.settings.autobackup.s3.ssl ? parent.config.settings.autobackup.s3.ssl : true,
- bucketName = parent.config.settings.autobackup.s3.bucketname,
- pathPrefix = s3folderName,
- threshold = parent.config.settings.autobackup.s3.maxfiles ? parent.config.settings.autobackup.s3.maxfiles : 0,
- fileToUpload = filename;
- // Create a MinIO client
- const Minio = require('minio');
- var minioClient = new Minio.Client({
- endPoint: endpoint,
- port: port,
- useSSL: useSsl,
- accessKey: accessKey,
- secretKey: secretKey
- });
- // List objects in the specified bucket and path prefix
- var listObjectsPromise = new Promise(function(resolve, reject) {
- var items = [];
- var stream = minioClient.listObjects(bucketName, pathPrefix, true);
- stream.on('data', function(item) {
- if (!item.name.endsWith('/')) { // Exclude directories
- items.push(item);
- }
- });
- stream.on('end', function() {
- resolve(items);
- });
- stream.on('error', function(err) {
- reject(err);
- });
- });
- listObjectsPromise.then(function(objects) {
- // Count the number of files
- var fileCount = objects.length;
- // Return if no files to carry on uploading
- if (fileCount === 0) { return Promise.resolve(); }
- // Sort the files by LastModified date (oldest first)
- objects.sort(function(a, b) { return new Date(a.lastModified) - new Date(b.lastModified); });
- // Check if the threshold is zero and return if
- if (threshold === 0) { return Promise.resolve(); }
- // Check if the number of files exceeds the threshold (maxfiles) is 0
- if (fileCount >= threshold) {
- // Calculate how many files need to be deleted to make space for the new file
- var filesToDelete = fileCount - threshold + 1; // +1 to make space for the new file
- if (func) { func('Deleting ' + filesToDelete + ' older ' + (filesToDelete == 1 ? 'file' : 'files') + ' from S3 ...'); }
- // Create an array of promises for deleting files
- var deletePromises = objects.slice(0, filesToDelete).map(function(fileToDelete) {
- return new Promise(function(resolve, reject) {
- minioClient.removeObject(bucketName, fileToDelete.name, function(err) {
- if (err) {
- reject(err);
- } else {
- if (func) { func('Deleted file: ' + fileToDelete.name + ' from S3'); }
- resolve();
- }
- });
- });
- });
- // Wait for all deletions to complete
- return Promise.all(deletePromises);
- } else {
- return Promise.resolve(); // No deletion needed
- }
- }).then(function() {
- // Determine the upload path by combining the pathPrefix with the filename
- var fileName = require('path').basename(fileToUpload);
- var uploadPath = require('path').join(pathPrefix, fileName);
- // Upload a new file
- var uploadPromise = new Promise(function(resolve, reject) {
- if (func) { func('Uploading file ' + uploadPath + ' to S3'); }
- minioClient.fPutObject(bucketName, uploadPath, fileToUpload, function(err, etag) {
- if (err) {
- reject(err);
- } else {
- if (func) { func('Uploaded file: ' + uploadPath + ' to S3'); }
- resolve(etag);
- }
- });
- });
- return uploadPromise;
- }).catch(function(error) {
- if (func) { func('Error managing files in S3: ' + error); }
- });
- }
- }
-
- // Transfer NeDB data into the current database
- obj.nedbtodb = function (func) {
- var nedbDatastore = null;
- try { nedbDatastore = require('@seald-io/nedb'); } catch (ex) { } // This is the NeDB with Node 23 support.
- if (nedbDatastore == null) {
- try { nedbDatastore = require('@yetzt/nedb'); } catch (ex) { } // This is the NeDB with fixed security dependencies.
- if (nedbDatastore == null) { nedbDatastore = require('nedb'); } // So not to break any existing installations, if the old NeDB is present, use it.
- }
-
- var datastoreOptions = { filename: parent.getConfigFilePath('meshcentral.db'), autoload: true };
-
- // If a DB encryption key is provided, perform database encryption
- if ((typeof parent.args.dbencryptkey == 'string') && (parent.args.dbencryptkey.length != 0)) {
- // Hash the database password into a AES256 key and setup encryption and decryption.
- var nedbKey = parent.crypto.createHash('sha384').update(parent.args.dbencryptkey).digest('raw').slice(0, 32);
- datastoreOptions.afterSerialization = function (plaintext) {
- const iv = parent.crypto.randomBytes(16);
- const aes = parent.crypto.createCipheriv('aes-256-cbc', nedbKey, iv);
- var ciphertext = aes.update(plaintext);
- ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
- return ciphertext.toString('base64');
- }
- datastoreOptions.beforeDeserialization = function (ciphertext) {
- const ciphertextBytes = Buffer.from(ciphertext, 'base64');
- const iv = ciphertextBytes.slice(0, 16);
- const data = ciphertextBytes.slice(16);
- const aes = parent.crypto.createDecipheriv('aes-256-cbc', nedbKey, iv);
- var plaintextBytes = Buffer.from(aes.update(data));
- plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
- return plaintextBytes.toString();
- }
- }
-
- // Setup all NeDB collections
- var nedbfile = new nedbDatastore(datastoreOptions);
- var nedbeventsfile = new nedbDatastore({ filename: parent.getConfigFilePath('meshcentral-events.db'), autoload: true, corruptAlertThreshold: 1 });
- var nedbpowerfile = new nedbDatastore({ filename: parent.getConfigFilePath('meshcentral-power.db'), autoload: true, corruptAlertThreshold: 1 });
- var nedbserverstatsfile = new nedbDatastore({ filename: parent.getConfigFilePath('meshcentral-stats.db'), autoload: true, corruptAlertThreshold: 1 });
-
- // Transfered record counts
- var normalRecordsTransferCount = 0;
- var eventRecordsTransferCount = 0;
- var powerRecordsTransferCount = 0;
- var statsRecordsTransferCount = 0;
- obj.pendingTransfer = 0;
-
- // Transfer the data from main database
- nedbfile.find({}, function (err, docs) {
- if ((err == null) && (docs.length > 0)) {
- performTypedRecordDecrypt(docs)
- for (var i in docs) {
- obj.pendingTransfer++;
- normalRecordsTransferCount++;
- obj.Set(common.unEscapeLinksFieldName(docs[i]), function () { obj.pendingTransfer--; });
- }
- }
-
- // Transfer events
- nedbeventsfile.find({}, function (err, docs) {
- if ((err == null) && (docs.length > 0)) {
- for (var i in docs) {
- obj.pendingTransfer++;
- eventRecordsTransferCount++;
- obj.StoreEvent(docs[i], function () { obj.pendingTransfer--; });
- }
- }
-
- // Transfer power events
- nedbpowerfile.find({}, function (err, docs) {
- if ((err == null) && (docs.length > 0)) {
- for (var i in docs) {
- obj.pendingTransfer++;
- powerRecordsTransferCount++;
- obj.storePowerEvent(docs[i], null, function () { obj.pendingTransfer--; });
- }
- }
-
- // Transfer server stats
- nedbserverstatsfile.find({}, function (err, docs) {
- if ((err == null) && (docs.length > 0)) {
- for (var i in docs) {
- obj.pendingTransfer++;
- statsRecordsTransferCount++;
- obj.SetServerStats(docs[i], function () { obj.pendingTransfer--; });
- }
- }
-
- // Only exit when all the records are stored.
- setInterval(function () {
- if (obj.pendingTransfer == 0) { func("Done. " + normalRecordsTransferCount + " record(s), " + eventRecordsTransferCount + " event(s), " + powerRecordsTransferCount + " power change(s), " + statsRecordsTransferCount + " stat(s)."); }
- }, 200)
- });
- });
- });
- });
- }
-
- function padNumber(number, digits) { return Array(Math.max(digits - String(number).length + 1, 0)).join(0) + number; }
-
- // Called when a node has changed
- function dbNodeChange(nodeChange, added) {
- if (parent.webserver == null) return;
- common.unEscapeLinksFieldName(nodeChange.fullDocument);
- const node = performTypedRecordDecrypt([nodeChange.fullDocument])[0];
- parent.DispatchEvent(['*', node.meshid], obj, { etype: 'node', action: (added ? 'addnode' : 'changenode'), node: parent.webserver.CloneSafeNode(node), nodeid: node._id, domain: node.domain, nolog: 1 });
- }
-
- // Called when a device group has changed
- function dbMeshChange(meshChange, added) {
- if (parent.webserver == null) return;
- common.unEscapeLinksFieldName(meshChange.fullDocument);
- const mesh = performTypedRecordDecrypt([meshChange.fullDocument])[0];
-
- // Update the mesh object in memory
- const mmesh = parent.webserver.meshes[mesh._id];
- if (mmesh != null) {
- // Update an existing device group
- for (var i in mesh) { mmesh[i] = mesh[i]; }
- for (var i in mmesh) { if (mesh[i] == null) { delete mmesh[i]; } }
- } else {
- // Device group not present, create it.
- parent.webserver.meshes[mesh._id] = mesh;
- }
-
- // Send the mesh update
- var mesh2 = Object.assign({}, mesh); // Shallow clone
- if (mesh2.deleted) { mesh2.action = 'deletemesh'; } else { mesh2.action = (added ? 'createmesh' : 'meshchange'); }
- mesh2.meshid = mesh2._id;
- mesh2.nolog = 1;
- delete mesh2.type;
- delete mesh2._id;
- parent.DispatchEvent(['*', mesh2.meshid], obj, parent.webserver.CloneSafeMesh(mesh2));
- }
-
- // Called when a user account has changed
- function dbUserChange(userChange, added) {
- if (parent.webserver == null) return;
- common.unEscapeLinksFieldName(userChange.fullDocument);
- const user = performTypedRecordDecrypt([userChange.fullDocument])[0];
-
- // Update the user object in memory
- const muser = parent.webserver.users[user._id];
- if (muser != null) {
- // Update an existing user
- for (var i in user) { muser[i] = user[i]; }
- for (var i in muser) { if (user[i] == null) { delete muser[i]; } }
- } else {
- // User not present, create it.
- parent.webserver.users[user._id] = user;
- }
-
- // Send the user update
- var targets = ['*', 'server-users', user._id];
- if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
- parent.DispatchEvent(targets, obj, { etype: 'user', userid: user._id, username: user.name, account: parent.webserver.CloneSafeUser(user), action: (added ? 'accountcreate' : 'accountchange'), domain: user.domain, nolog: 1 });
- }
-
- // Called when a user group has changed
- function dbUGrpChange(ugrpChange, added) {
- if (parent.webserver == null) return;
- common.unEscapeLinksFieldName(ugrpChange.fullDocument);
- const usergroup = ugrpChange.fullDocument;
-
- // Update the user group object in memory
- const uusergroup = parent.webserver.userGroups[usergroup._id];
- if (uusergroup != null) {
- // Update an existing user group
- for (var i in usergroup) { uusergroup[i] = usergroup[i]; }
- for (var i in uusergroup) { if (usergroup[i] == null) { delete uusergroup[i]; } }
- } else {
- // Usergroup not present, create it.
- parent.webserver.userGroups[usergroup._id] = usergroup;
- }
-
- // Send the user group update
- var usergroup2 = Object.assign({}, usergroup); // Shallow clone
- usergroup2.action = (added ? 'createusergroup' : 'usergroupchange');
- usergroup2.ugrpid = usergroup2._id;
- usergroup2.nolog = 1;
- delete usergroup2.type;
- delete usergroup2._id;
- parent.DispatchEvent(['*', usergroup2.ugrpid], obj, usergroup2);
- }
-
- function dbMergeSqlArray(arr) {
- var x = '';
- for (var i in arr) { if (x != '') { x += ','; } x += '\'' + arr[i] + '\''; }
- return x;
- }
-
- return obj;
-};
+/**
+* @description MeshCentral database module
+* @author Ylian Saint-Hilaire
+* @copyright Intel Corporation 2018-2022
+* @license Apache-2.0
+* @version v0.0.2
+*/
+
+/*xjslint node: true */
+/*xjslint plusplus: true */
+/*xjslint maxlen: 256 */
+/*jshint node: true */
+/*jshint strict: false */
+/*jshint esversion: 6 */
+"use strict";
+
+//
+// Construct Meshcentral database object
+//
+// The default database is NeDB
+// https://github.com/louischatriot/nedb
+//
+// Alternativety, MongoDB can be used
+// https://www.mongodb.com/
+// Just run with --mongodb [connectionstring], where the connection string is documented here: https://docs.mongodb.com/manual/reference/connection-string/
+// The default collection is "meshcentral", but you can override it using --mongodbcol [collection]
+//
+module.exports.CreateDB = function (parent, func) {
+ var obj = {};
+ var Datastore = null;
+ var expireEventsSeconds = (60 * 60 * 24 * 20); // By default, expire events after 20 days (1728000). (Seconds * Minutes * Hours * Days)
+ var expirePowerEventsSeconds = (60 * 60 * 24 * 10); // By default, expire power events after 10 days (864000). (Seconds * Minutes * Hours * Days)
+ var expireServerStatsSeconds = (60 * 60 * 24 * 30); // By default, expire server stats after 30 days (2592000). (Seconds * Minutes * Hours * Days)
+ const common = require('./common.js');
+ const path = require('path');
+ const fs = require('fs');
+ const DB_NEDB = 1, DB_MONGOJS = 2, DB_MONGODB = 3,DB_MARIADB = 4, DB_MYSQL = 5, DB_POSTGRESQL = 6, DB_ACEBASE = 7, DB_SQLITE = 8;
+ const DB_LIST = ['None', 'NeDB', 'MongoJS', 'MongoDB', 'MariaDB', 'MySQL', 'PostgreSQL', 'AceBase', 'SQLite']; //for the info command
+ let databaseName = 'meshcentral';
+ let datapathParentPath = path.dirname(parent.datapath);
+ let datapathFoldername = path.basename(parent.datapath);
+ const SQLITE_AUTOVACUUM = ['none', 'full', 'incremental'];
+ const SQLITE_SYNCHRONOUS = ['off', 'normal', 'full', 'extra'];
+ obj.sqliteConfig = {
+ maintenance: '',
+ startupVacuum: false,
+ autoVacuum: 'full',
+ incrementalVacuum: 100,
+ journalMode: 'delete',
+ journalSize: 4096000,
+ synchronous: 'full',
+ };
+ obj.performingBackup = false;
+ const BACKUPFAIL_ZIPCREATE = 0x0001;
+ const BACKUPFAIL_ZIPMODULE = 0x0010;
+ const BACKUPFAIL_DBDUMP = 0x0100;
+ obj.backupStatus = 0x0;
+ obj.newAutoBackupFile = null;
+ obj.newDBDumpFile = null;
+ obj.identifier = null;
+ obj.dbKey = null;
+ obj.dbRecordsEncryptKey = null;
+ obj.dbRecordsDecryptKey = null;
+ obj.changeStream = false;
+ obj.pluginsActive = ((parent.config) && (parent.config.settings) && (parent.config.settings.plugins != null) && (parent.config.settings.plugins != false) && ((typeof parent.config.settings.plugins != 'object') || (parent.config.settings.plugins.enabled != false)));
+ obj.dbCounters = {
+ fileSet: 0,
+ fileRemove: 0,
+ powerSet: 0,
+ eventsSet: 0
+ }
+
+ // MongoDB bulk operations state
+ if (parent.config.settings.mongodbbulkoperations) {
+ // Added counters
+ obj.dbCounters.fileSetPending = 0;
+ obj.dbCounters.fileSetBulk = 0;
+ obj.dbCounters.fileRemovePending = 0;
+ obj.dbCounters.fileRemoveBulk = 0;
+ obj.dbCounters.powerSetPending = 0;
+ obj.dbCounters.powerSetBulk = 0;
+ obj.dbCounters.eventsSetPending = 0;
+ obj.dbCounters.eventsSetBulk = 0;
+
+ /// Added bulk accumulators
+ obj.filePendingGet = null;
+ obj.filePendingGets = null;
+ obj.filePendingRemove = null;
+ obj.filePendingRemoves = null;
+ obj.filePendingSet = false;
+ obj.filePendingSets = null;
+ obj.filePendingCb = null;
+ obj.filePendingCbs = null;
+ obj.powerFilePendingSet = false;
+ obj.powerFilePendingSets = null;
+ obj.powerFilePendingCb = null;
+ obj.powerFilePendingCbs = null;
+ obj.eventsFilePendingSet = false;
+ obj.eventsFilePendingSets = null;
+ obj.eventsFilePendingCb = null;
+ obj.eventsFilePendingCbs = null;
+ }
+
+ obj.SetupDatabase = function (func) {
+ // Check if the database unique identifier is present
+ // This is used to check that in server peering mode, everyone is using the same database.
+ obj.Get('DatabaseIdentifier', function (err, docs) {
+ if (err != null) { parent.debug('db', 'ERROR (Get DatabaseIdentifier): ' + err); }
+ if ((err == null) && (docs.length == 1) && (docs[0].value != null)) {
+ obj.identifier = docs[0].value;
+ } else {
+ obj.identifier = Buffer.from(require('crypto').randomBytes(48), 'binary').toString('hex');
+ obj.Set({ _id: 'DatabaseIdentifier', value: obj.identifier });
+ }
+ });
+
+ // Load database schema version and check if we need to update
+ obj.Get('SchemaVersion', function (err, docs) {
+ if (err != null) { parent.debug('db', 'ERROR (Get SchemaVersion): ' + err); }
+ var ver = 0;
+ if ((err == null) && (docs.length == 1)) { ver = docs[0].value; }
+ if (ver == 1) { console.log('This is an unsupported beta 1 database, delete it to create a new one.'); process.exit(0); }
+
+ // TODO: Any schema upgrades here...
+ obj.Set({ _id: 'SchemaVersion', value: 2 });
+
+ func(ver);
+ });
+ };
+
+ // Perform database maintenance
+ obj.maintenance = function () {
+ parent.debug('db', 'Entering database maintenance');
+ if (obj.databaseType == DB_NEDB) { // NeDB will not remove expired records unless we try to access them. This will force the removal.
+ obj.eventsfile.remove({ time: { '$lt': new Date(Date.now() - (expireEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
+ obj.powerfile.remove({ time: { '$lt': new Date(Date.now() - (expirePowerEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
+ obj.serverstatsfile.remove({ time: { '$lt': new Date(Date.now() - (expireServerStatsSeconds * 1000)) } }, { multi: true }); // Force delete older events
+ } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) { // MariaDB or MySQL
+ sqlDbQuery('DELETE FROM events WHERE time < ?', [new Date(Date.now() - (expireEventsSeconds * 1000))], function (doc, err) { }); // Delete events older than expireEventsSeconds
+ sqlDbQuery('DELETE FROM power WHERE time < ?', [new Date(Date.now() - (expirePowerEventsSeconds * 1000))], function (doc, err) { }); // Delete events older than expirePowerSeconds
+ sqlDbQuery('DELETE FROM serverstats WHERE expire < ?', [new Date()], function (doc, err) { }); // Delete events where expiration date is in the past
+ sqlDbQuery('DELETE FROM smbios WHERE expire < ?', [new Date()], function (doc, err) { }); // Delete events where expiration date is in the past
+ } else if (obj.databaseType == DB_ACEBASE) { // AceBase
+ //console.log('Performing AceBase maintenance');
+ obj.file.query('events').filter('time', '<', new Date(Date.now() - (expireEventsSeconds * 1000))).remove().then(function () {
+ obj.file.query('stats').filter('time', '<', new Date(Date.now() - (expireServerStatsSeconds * 1000))).remove().then(function () {
+ obj.file.query('power').filter('time', '<', new Date(Date.now() - (expirePowerEventsSeconds * 1000))).remove().then(function () {
+ //console.log('AceBase maintenance done');
+ });
+ });
+ });
+ } else if (obj.databaseType == DB_SQLITE) { // SQLite3
+ //sqlite does not return rows affected for INSERT, UPDATE or DELETE statements, see https://www.sqlite.org/pragma.html#pragma_count_changes
+ obj.file.serialize(function () {
+ obj.file.run('DELETE FROM events WHERE time < ?', [new Date(Date.now() - (expireEventsSeconds * 1000))]);
+ obj.file.run('DELETE FROM power WHERE time < ?', [new Date(Date.now() - (expirePowerEventsSeconds * 1000))]);
+ obj.file.run('DELETE FROM serverstats WHERE expire < ?', [new Date()]);
+ obj.file.run('DELETE FROM smbios WHERE expire < ?', [new Date()]);
+ obj.file.exec(obj.sqliteConfig.maintenance, function (err) {
+ if (err) {console.log('Maintenance error: ' + err.message)};
+ if (parent.config.settings.debug) {
+ sqliteGetPragmas(['freelist_count', 'page_size', 'page_count', 'cache_size' ], function (pragma, pragmaValue) {
+ parent.debug('db', 'SQLite Maintenance: ' + pragma + '=' + pragmaValue);
+ });
+ };
+ });
+ });
+ }
+ obj.removeInactiveDevices();
+ }
+
+ // Remove inactive devices
+ obj.removeInactiveDevices = function (showall, cb) {
+ // Get a list of domains and what their inactive device removal setting is
+ var removeInactiveDevicesPerDomain = {}, minRemoveInactiveDevicesPerDomain = {}, minRemoveInactiveDevice = 9999;
+ for (var i in parent.config.domains) {
+ if (typeof parent.config.domains[i].autoremoveinactivedevices == 'number') {
+ var v = parent.config.domains[i].autoremoveinactivedevices;
+ if ((v >= 1) && (v <= 2000)) {
+ if (v < minRemoveInactiveDevice) { minRemoveInactiveDevice = v; }
+ removeInactiveDevicesPerDomain[i] = v;
+ minRemoveInactiveDevicesPerDomain[i] = v;
+ }
+ }
+ }
+
+ // Check if any device groups have a inactive device removal setting
+ for (var i in parent.webserver.meshes) {
+ if (typeof parent.webserver.meshes[i].expireDevs == 'number') {
+ var v = parent.webserver.meshes[i].expireDevs;
+ if ((v >= 1) && (v <= 2000)) {
+ if (v < minRemoveInactiveDevice) { minRemoveInactiveDevice = v; }
+ if ((minRemoveInactiveDevicesPerDomain[parent.webserver.meshes[i].domain] == null) || (minRemoveInactiveDevicesPerDomain[parent.webserver.meshes[i].domain] > v)) {
+ minRemoveInactiveDevicesPerDomain[parent.webserver.meshes[i].domain] = v;
+ }
+ } else {
+ delete parent.webserver.meshes[i].expireDevs;
+ }
+ }
+ }
+
+ // If there are no such settings for any domain, we can exit now.
+ if (minRemoveInactiveDevice == 9999) { if (cb) { cb("No device removal policy set, nothing to do."); } return; }
+ const now = Date.now();
+
+ // For each domain with a inactive device removal setting, get a list of last device connections
+ for (var domainid in minRemoveInactiveDevicesPerDomain) {
+ obj.GetAllTypeNoTypeField('lastconnect', domainid, function (err, docs) {
+ if ((err != null) || (docs == null)) return;
+ for (var j in docs) {
+ const days = Math.floor((now - docs[j].time) / 86400000); // Calculate the number of inactive days
+ var expireDays = -1;
+ if (removeInactiveDevicesPerDomain[docs[j].domain]) { expireDays = removeInactiveDevicesPerDomain[docs[j].domain]; }
+ const mesh = parent.webserver.meshes[docs[j].meshid];
+ if (mesh && (typeof mesh.expireDevs == 'number')) { expireDays = mesh.expireDevs; }
+ var remove = false;
+ if (expireDays > 0) {
+ if (expireDays < days) { remove = true; }
+ if (cb) { if (showall || remove) { cb(docs[j]._id.substring(2) + ', ' + days + ' days, expire ' + expireDays + ' days' + (remove ? ', removing' : '')); } }
+ if (remove) {
+ // Check if this device is connected right now
+ const nodeid = docs[j]._id.substring(2);
+ const conn = parent.GetConnectivityState(nodeid);
+ if (conn == null) {
+ // Remove the device
+ obj.Get(nodeid, function (err, docs) {
+ if (err != null) return;
+ if ((docs == null) || (docs.length != 1)) { obj.Remove('lc' + nodeid); return; } // Remove last connect time
+ const node = docs[0];
+
+ // Delete this node including network interface information, events and timeline
+ obj.Remove(node._id); // Remove node with that id
+ obj.Remove('if' + node._id); // Remove interface information
+ obj.Remove('nt' + node._id); // Remove notes
+ obj.Remove('lc' + node._id); // Remove last connect time
+ obj.Remove('si' + node._id); // Remove system information
+ obj.Remove('al' + node._id); // Remove error log last time
+ if (obj.RemoveSMBIOS) { obj.RemoveSMBIOS(node._id); } // Remove SMBios data
+ obj.RemoveAllNodeEvents(node._id); // Remove all events for this node
+ obj.removeAllPowerEventsForNode(node._id); // Remove all power events for this node
+ if (typeof node.pmt == 'string') { obj.Remove('pmt_' + node.pmt); } // Remove Push Messaging Token
+ obj.Get('ra' + node._id, function (err, nodes) {
+ if ((nodes != null) && (nodes.length == 1)) { obj.Remove('da' + nodes[0].daid); } // Remove diagnostic agent to real agent link
+ obj.Remove('ra' + node._id); // Remove real agent to diagnostic agent link
+ });
+
+ // Remove any user node links
+ if (node.links != null) {
+ for (var i in node.links) {
+ if (i.startsWith('user/')) {
+ var cuser = parent.webserver.users[i];
+ if ((cuser != null) && (cuser.links != null) && (cuser.links[node._id] != null)) {
+ // Remove the user link & save the user
+ delete cuser.links[node._id];
+ if (Object.keys(cuser.links).length == 0) { delete cuser.links; }
+ obj.SetUser(cuser);
+
+ // Notify user change
+ var targets = ['*', 'server-users', cuser._id];
+ var event = { etype: 'user', userid: cuser._id, username: cuser.name, action: 'accountchange', msgid: 86, msgArgs: [cuser.name], msg: 'Removed user device rights for ' + cuser.name, domain: node.domain, account: parent.webserver.CloneSafeUser(cuser) };
+ if (obj.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
+ parent.DispatchEvent(targets, obj, event);
+ }
+ } else if (i.startsWith('ugrp/')) {
+ var cusergroup = parent.userGroups[i];
+ if ((cusergroup != null) && (cusergroup.links != null) && (cusergroup.links[node._id] != null)) {
+ // Remove the user link & save the user
+ delete cusergroup.links[node._id];
+ if (Object.keys(cusergroup.links).length == 0) { delete cusergroup.links; }
+ obj.Set(cusergroup);
+
+ // Notify user change
+ var targets = ['*', 'server-users', cusergroup._id];
+ var event = { etype: 'ugrp', ugrpid: cusergroup._id, name: cusergroup.name, desc: cusergroup.desc, action: 'usergroupchange', links: cusergroup.links, msgid: 163, msgArgs: [node.name, cusergroup.name], msg: 'Removed device ' + node.name + ' from user group ' + cusergroup.name };
+ if (obj.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
+ parent.DispatchEvent(targets, obj, event);
+ }
+ }
+ }
+ }
+
+ // Event node deletion
+ var meshname = '(unknown)';
+ if ((parent.webserver.meshes[node.meshid] != null) && (parent.webserver.meshes[node.meshid].name != null)) { meshname = parent.webserver.meshes[node.meshid].name; }
+ var event = { etype: 'node', action: 'removenode', nodeid: node._id, msgid: 87, msgArgs: [node.name, meshname], msg: 'Removed device ' + node.name + ' from device group ' + meshname, domain: node.domain };
+ // TODO: We can't use the changeStream for node delete because we will not know the meshid the device was in.
+ //if (obj.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to remove the node. Another event will come.
+ parent.DispatchEvent(parent.webserver.CreateNodeDispatchTargets(node.meshid, node._id), obj, event);
+ });
+ }
+ }
+ }
+ }
+ });
+ }
+ }
+
+ // Remove all reference to a domain from the database
+ obj.removeDomain = function (domainName, func) {
+ var pendingCalls;
+ // Remove all events, power events and SMBIOS data from the main collection. They are all in seperate collections now.
+ if (obj.databaseType == DB_ACEBASE) {
+ // AceBase
+ pendingCalls = 3;
+ obj.file.query('meshcentral').filter('domain', '==', domainName).remove().then(function () { if (--pendingCalls == 0) { func(); } });
+ obj.file.query('events').filter('domain', '==', domainName).remove().then(function () { if (--pendingCalls == 0) { func(); } });
+ obj.file.query('power').filter('domain', '==', domainName).remove().then(function () { if (--pendingCalls == 0) { func(); } });
+ } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL) || (obj.databaseType == DB_POSTGRESQL)) {
+ // MariaDB, MySQL or PostgreSQL
+ pendingCalls = 2;
+ sqlDbQuery('DELETE FROM main WHERE domain = $1', [domainName], function () { if (--pendingCalls == 0) { func(); } });
+ sqlDbQuery('DELETE FROM events WHERE domain = $1', [domainName], function () { if (--pendingCalls == 0) { func(); } });
+ } else if (obj.databaseType == DB_MONGODB) {
+ // MongoDB
+ pendingCalls = 3;
+ obj.file.deleteMany({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
+ obj.eventsfile.deleteMany({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
+ obj.powerfile.deleteMany({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
+ } else {
+ // NeDB or MongoJS
+ pendingCalls = 3;
+ obj.file.remove({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
+ obj.eventsfile.remove({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
+ obj.powerfile.remove({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
+ }
+ }
+
+ obj.cleanup = function (func) {
+ // TODO: Remove all mesh links to invalid users
+ // TODO: Remove all meshes that dont have any links
+
+ // Remove all events, power events and SMBIOS data from the main collection. They are all in seperate collections now.
+ if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL) || (obj.databaseType == DB_POSTGRESQL)) {
+ // MariaDB, MySQL or PostgreSQL
+ obj.RemoveAllOfType('event', function () { });
+ obj.RemoveAllOfType('power', function () { });
+ obj.RemoveAllOfType('smbios', function () { });
+ } else if (obj.databaseType == DB_MONGODB) {
+ // MongoDB
+ obj.file.deleteMany({ type: 'event' }, { multi: true });
+ obj.file.deleteMany({ type: 'power' }, { multi: true });
+ obj.file.deleteMany({ type: 'smbios' }, { multi: true });
+ } else if ((obj.databaseType == DB_NEDB) || (obj.databaseType == DB_MONGOJS)) {
+ // NeDB or MongoJS
+ obj.file.remove({ type: 'event' }, { multi: true });
+ obj.file.remove({ type: 'power' }, { multi: true });
+ obj.file.remove({ type: 'smbios' }, { multi: true });
+ }
+
+ // List of valid identifiers
+ var validIdentifiers = {}
+
+ // Load all user groups
+ obj.GetAllType('ugrp', function (err, docs) {
+ if (err != null) { parent.debug('db', 'ERROR (GetAll user): ' + err); }
+ if ((err == null) && (docs.length > 0)) {
+ for (var i in docs) {
+ // Add this as a valid user identifier
+ validIdentifiers[docs[i]._id] = 1;
+ }
+ }
+
+ // Fix all of the creating & login to ticks by seconds, not milliseconds.
+ obj.GetAllType('user', function (err, docs) {
+ if (err != null) { parent.debug('db', 'ERROR (GetAll user): ' + err); }
+ if ((err == null) && (docs.length > 0)) {
+ for (var i in docs) {
+ var fixed = false;
+
+ // Add this as a valid user identifier
+ validIdentifiers[docs[i]._id] = 1;
+
+ // Fix email address capitalization
+ if (docs[i].email && (docs[i].email != docs[i].email.toLowerCase())) {
+ docs[i].email = docs[i].email.toLowerCase(); fixed = true;
+ }
+
+ // Fix account creation
+ if (docs[i].creation) {
+ if (docs[i].creation > 1300000000000) { docs[i].creation = Math.floor(docs[i].creation / 1000); fixed = true; }
+ if ((docs[i].creation % 1) != 0) { docs[i].creation = Math.floor(docs[i].creation); fixed = true; }
+ }
+
+ // Fix last account login
+ if (docs[i].login) {
+ if (docs[i].login > 1300000000000) { docs[i].login = Math.floor(docs[i].login / 1000); fixed = true; }
+ if ((docs[i].login % 1) != 0) { docs[i].login = Math.floor(docs[i].login); fixed = true; }
+ }
+
+ // Fix last password change
+ if (docs[i].passchange) {
+ if (docs[i].passchange > 1300000000000) { docs[i].passchange = Math.floor(docs[i].passchange / 1000); fixed = true; }
+ if ((docs[i].passchange % 1) != 0) { docs[i].passchange = Math.floor(docs[i].passchange); fixed = true; }
+ }
+
+ // Fix subscriptions
+ if (docs[i].subscriptions != null) { delete docs[i].subscriptions; fixed = true; }
+
+ // Save the user if needed
+ if (fixed) { obj.Set(docs[i]); }
+ }
+
+ // Remove all objects that have a "meshid" that no longer points to a valid mesh.
+ // Fix any incorrectly escaped user identifiers
+ obj.GetAllType('mesh', function (err, docs) {
+ if (err != null) { parent.debug('db', 'ERROR (GetAll mesh): ' + err); }
+ var meshlist = [];
+ if ((err == null) && (docs.length > 0)) {
+ for (var i in docs) {
+ var meshChange = false;
+ docs[i] = common.unEscapeLinksFieldName(docs[i]);
+ meshlist.push(docs[i]._id);
+
+ // Make sure all mesh types are number type, if not, fix it.
+ if (typeof docs[i].mtype == 'string') { docs[i].mtype = parseInt(docs[i].mtype); meshChange = true; }
+
+ // If the device group is deleted, remove any invite codes
+ if (docs[i].deleted && docs[i].invite) { delete docs[i].invite; meshChange = true; }
+
+ // Take a look at the links
+ if (docs[i].links != null) {
+ for (var j in docs[i].links) {
+ if (validIdentifiers[j] == null) {
+ // This identifier is not known, let see if we can fix it.
+ var xid = j, xid2 = common.unEscapeFieldName(xid);
+ while ((xid != xid2) && (validIdentifiers[xid2] == null)) { xid = xid2; xid2 = common.unEscapeFieldName(xid2); }
+ if (validIdentifiers[xid2] == 1) {
+ //console.log('Fixing id: ' + j + ' to ' + xid2);
+ docs[i].links[xid2] = docs[i].links[j];
+ delete docs[i].links[j];
+ meshChange = true;
+ } else {
+ // TODO: here, we may want to clean up links to users and user groups that do not exist anymore.
+ //console.log('Unknown id: ' + j);
+ }
+ }
+ }
+ }
+
+ // Save the updated device group if needed
+ if (meshChange) { obj.Set(docs[i]); }
+ }
+ }
+ if (obj.databaseType == DB_SQLITE) {
+ // SQLite
+
+ } else if (obj.databaseType == DB_ACEBASE) {
+ // AceBase
+
+ } else if (obj.databaseType == DB_POSTGRESQL) {
+ // Postgres
+ sqlDbQuery('DELETE FROM Main WHERE ((extra != NULL) AND (extra LIKE (\'mesh/%\')) AND (extra != ANY ($1)))', [meshlist], function (err, response) { });
+ } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
+ // MariaDB
+ sqlDbQuery('DELETE FROM Main WHERE (extra LIKE ("mesh/%") AND (extra NOT IN ?)', [meshlist], function (err, response) { });
+ } else if (obj.databaseType == DB_MONGODB) {
+ // MongoDB
+ obj.file.deleteMany({ meshid: { $exists: true, $nin: meshlist } }, { multi: true });
+ } else {
+ // NeDB or MongoJS
+ obj.file.remove({ meshid: { $exists: true, $nin: meshlist } }, { multi: true });
+ }
+
+ // We are done
+ validIdentifiers = null;
+ if (func) { func(); }
+ });
+ }
+ });
+ });
+ };
+
+ // Get encryption key
+ obj.getEncryptDataKey = function (password, salt, iterations) {
+ if (typeof password != 'string') return null;
+ let key;
+ try {
+ key = parent.crypto.pbkdf2Sync(password, salt, iterations, 32, 'sha384');
+ } catch (ex) {
+ // If this previous call fails, it's probably because older pbkdf2 did not specify the hashing function, just use the default.
+ key = parent.crypto.pbkdf2Sync(password, salt, iterations, 32);
+ }
+ return key
+ }
+
+ // Encrypt data
+ obj.encryptData = function (password, plaintext) {
+ let encryptionVersion = 0x01;
+ let iterations = 100000
+ const iv = parent.crypto.randomBytes(16);
+ var key = obj.getEncryptDataKey(password, iv, iterations);
+ if (key == null) return null;
+ const aes = parent.crypto.createCipheriv('aes-256-gcm', key, iv);
+ var ciphertext = aes.update(plaintext);
+ let versionbuf = Buffer.allocUnsafe(2);
+ versionbuf.writeUInt16BE(encryptionVersion);
+ let iterbuf = Buffer.allocUnsafe(4);
+ iterbuf.writeUInt32BE(iterations);
+ let encryptedBuf = aes.final();
+ ciphertext = Buffer.concat([versionbuf, iterbuf, aes.getAuthTag(), iv, ciphertext, encryptedBuf]);
+ return ciphertext.toString('base64');
+ }
+
+ // Decrypt data
+ obj.decryptData = function (password, ciphertext) {
+ // Adding an encryption version lets us avoid try catching in the future
+ let ciphertextBytes = Buffer.from(ciphertext, 'base64');
+ let encryptionVersion = ciphertextBytes.readUInt16BE(0);
+ try {
+ switch (encryptionVersion) {
+ case 0x01:
+ let iterations = ciphertextBytes.readUInt32BE(2);
+ let authTag = ciphertextBytes.slice(6, 22);
+ const iv = ciphertextBytes.slice(22, 38);
+ const data = ciphertextBytes.slice(38);
+ let key = obj.getEncryptDataKey(password, iv, iterations);
+ if (key == null) return null;
+ const aes = parent.crypto.createDecipheriv('aes-256-gcm', key, iv);
+ aes.setAuthTag(authTag);
+ let plaintextBytes = Buffer.from(aes.update(data));
+ plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
+ return plaintextBytes;
+ default:
+ return obj.oldDecryptData(password, ciphertextBytes);
+ }
+ } catch (ex) { return obj.oldDecryptData(password, ciphertextBytes); }
+ }
+
+ // Encrypt data
+ // The older encryption system uses CBC without integraty checking.
+ // This method is kept only for testing
+ obj.oldEncryptData = function (password, plaintext) {
+ let key = parent.crypto.createHash('sha384').update(password).digest('raw').slice(0, 32);
+ if (key == null) return null;
+ const iv = parent.crypto.randomBytes(16);
+ const aes = parent.crypto.createCipheriv('aes-256-cbc', key, iv);
+ var ciphertext = aes.update(plaintext);
+ ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
+ return ciphertext.toString('base64');
+ }
+
+ // Decrypt data
+ // The older encryption system uses CBC without integraty checking.
+ // This method is kept only to convert the old encryption to the new one.
+ obj.oldDecryptData = function (password, ciphertextBytes) {
+ if (typeof password != 'string') return null;
+ try {
+ const iv = ciphertextBytes.slice(0, 16);
+ const data = ciphertextBytes.slice(16);
+ let key = parent.crypto.createHash('sha384').update(password).digest('raw').slice(0, 32);
+ const aes = parent.crypto.createDecipheriv('aes-256-cbc', key, iv);
+ let plaintextBytes = Buffer.from(aes.update(data));
+ plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
+ return plaintextBytes;
+ } catch (ex) { return null; }
+ }
+
+ // Get the number of records in the database for various types, this is the slow NeDB way.
+ // WARNING: This is a terrible query for database performance. Only do this when needed. This query will look at almost every document in the database.
+ obj.getStats = function (func) {
+ if (obj.databaseType == DB_ACEBASE) {
+ // AceBase
+ // TODO
+ } else if (obj.databaseType == DB_POSTGRESQL) {
+ // PostgreSQL
+ // TODO
+ } else if (obj.databaseType == DB_MYSQL) {
+ // MySQL
+ // TODO
+ } else if (obj.databaseType == DB_MARIADB) {
+ // MariaDB
+ // TODO
+ } else if (obj.databaseType == DB_MONGODB) {
+ // MongoDB
+ obj.file.aggregate([{ "$group": { _id: "$type", count: { $sum: 1 } } }]).toArray(function (err, docs) {
+ var counters = {}, totalCount = 0;
+ if (err == null) { for (var i in docs) { if (docs[i]._id != null) { counters[docs[i]._id] = docs[i].count; totalCount += docs[i].count; } } }
+ func(counters);
+ });
+ } else if (obj.databaseType == DB_MONGOJS) {
+ // MongoJS
+ obj.file.aggregate([{ "$group": { _id: "$type", count: { $sum: 1 } } }], function (err, docs) {
+ var counters = {}, totalCount = 0;
+ if (err == null) { for (var i in docs) { if (docs[i]._id != null) { counters[docs[i]._id] = docs[i].count; totalCount += docs[i].count; } } }
+ func(counters);
+ });
+ } else if (obj.databaseType == DB_NEDB) {
+ // NeDB version
+ obj.file.count({ type: 'node' }, function (err, nodeCount) {
+ obj.file.count({ type: 'mesh' }, function (err, meshCount) {
+ obj.file.count({ type: 'user' }, function (err, userCount) {
+ obj.file.count({ type: 'sysinfo' }, function (err, sysinfoCount) {
+ obj.file.count({ type: 'note' }, function (err, noteCount) {
+ obj.file.count({ type: 'iploc' }, function (err, iplocCount) {
+ obj.file.count({ type: 'ifinfo' }, function (err, ifinfoCount) {
+ obj.file.count({ type: 'cfile' }, function (err, cfileCount) {
+ obj.file.count({ type: 'lastconnect' }, function (err, lastconnectCount) {
+ obj.file.count({}, function (err, totalCount) {
+ func({ node: nodeCount, mesh: meshCount, user: userCount, sysinfo: sysinfoCount, iploc: iplocCount, note: noteCount, ifinfo: ifinfoCount, cfile: cfileCount, lastconnect: lastconnectCount, total: totalCount });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ }
+ }
+
+ // This is used to rate limit a number of operation per day. Returns a startValue each new days, but you can substract it and save the value in the db.
+ obj.getValueOfTheDay = function (id, startValue, func) { obj.Get(id, function (err, docs) { var date = new Date(), t = date.toLocaleDateString(); if ((err == null) && (docs.length == 1)) { var r = docs[0]; if (r.day == t) { func({ _id: id, value: r.value, day: t }); return; } } func({ _id: id, value: startValue, day: t }); }); };
+ obj.escapeBase64 = function escapeBase64(val) { return (val.replace(/\+/g, '@').replace(/\//g, '$')); }
+
+ // Encrypt an database object
+ obj.performRecordEncryptionRecode = function (func) {
+ var count = 0;
+ obj.GetAllType('user', function (err, docs) {
+ if (err != null) { parent.debug('db', 'ERROR (performRecordEncryptionRecode): ' + err); }
+ if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } }
+ obj.GetAllType('node', function (err, docs) {
+ if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } }
+ obj.GetAllType('mesh', function (err, docs) {
+ if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } }
+ if (obj.databaseType == DB_NEDB) { // If we are using NeDB, compact the database.
+ obj.file.compactDatafile();
+ obj.file.on('compaction.done', function () { func(count); }); // It's important to wait for compaction to finish before exit, otherwise NeDB may corrupt.
+ } else {
+ func(count); // For all other databases, normal exit.
+ }
+ });
+ });
+ });
+ }
+
+ // Encrypt an database object
+ function performTypedRecordDecrypt(data) {
+ if ((data == null) || (obj.dbRecordsDecryptKey == null) || (typeof data != 'object')) return data;
+ for (var i in data) {
+ if ((data[i] == null) || (typeof data[i] != 'object')) continue;
+ data[i] = performPartialRecordDecrypt(data[i]);
+ if ((data[i].intelamt != null) && (typeof data[i].intelamt == 'object') && (data[i].intelamt._CRYPT)) { data[i].intelamt = performPartialRecordDecrypt(data[i].intelamt); }
+ if ((data[i].amt != null) && (typeof data[i].amt == 'object') && (data[i].amt._CRYPT)) { data[i].amt = performPartialRecordDecrypt(data[i].amt); }
+ if ((data[i].kvm != null) && (typeof data[i].kvm == 'object') && (data[i].kvm._CRYPT)) { data[i].kvm = performPartialRecordDecrypt(data[i].kvm); }
+ }
+ return data;
+ }
+
+ // Encrypt an database object
+ function performTypedRecordEncrypt(data) {
+ if (obj.dbRecordsEncryptKey == null) return data;
+ if (data.type == 'user') { return performPartialRecordEncrypt(Clone(data), ['otpkeys', 'otphkeys', 'otpsecret', 'salt', 'hash', 'oldpasswords']); }
+ else if ((data.type == 'node') && (data.ssh || data.rdp || data.intelamt)) {
+ var xdata = Clone(data);
+ if (data.ssh || data.rdp) { xdata = performPartialRecordEncrypt(xdata, ['ssh', 'rdp']); }
+ if (data.intelamt) { xdata.intelamt = performPartialRecordEncrypt(xdata.intelamt, ['pass', 'mpspass']); }
+ return xdata;
+ }
+ else if ((data.type == 'mesh') && (data.amt || data.kvm)) {
+ var xdata = Clone(data);
+ if (data.amt) { xdata.amt = performPartialRecordEncrypt(xdata.amt, ['password']); }
+ if (data.kvm) { xdata.kvm = performPartialRecordEncrypt(xdata.kvm, ['pass']); }
+ return xdata;
+ }
+ return data;
+ }
+
+ // Encrypt an object and return a buffer.
+ function performPartialRecordEncrypt(plainobj, encryptNames) {
+ if (typeof plainobj != 'object') return plainobj;
+ var enc = {}, enclen = 0;
+ for (var i in encryptNames) { if (plainobj[encryptNames[i]] != null) { enclen++; enc[encryptNames[i]] = plainobj[encryptNames[i]]; delete plainobj[encryptNames[i]]; } }
+ if (enclen > 0) { plainobj._CRYPT = performRecordEncrypt(enc); } else { delete plainobj._CRYPT; }
+ return plainobj;
+ }
+
+ // Encrypt an object and return a buffer.
+ function performPartialRecordDecrypt(plainobj) {
+ if ((typeof plainobj != 'object') || (plainobj._CRYPT == null)) return plainobj;
+ var enc = performRecordDecrypt(plainobj._CRYPT);
+ if (enc != null) { for (var i in enc) { plainobj[i] = enc[i]; } }
+ delete plainobj._CRYPT;
+ return plainobj;
+ }
+
+ // Encrypt an object and return a base64.
+ function performRecordEncrypt(plainobj) {
+ if (obj.dbRecordsEncryptKey == null) return null;
+ const iv = parent.crypto.randomBytes(12);
+ const aes = parent.crypto.createCipheriv('aes-256-gcm', obj.dbRecordsEncryptKey, iv);
+ var ciphertext = aes.update(JSON.stringify(plainobj));
+ var cipherfinal = aes.final();
+ ciphertext = Buffer.concat([iv, aes.getAuthTag(), ciphertext, cipherfinal]);
+ return ciphertext.toString('base64');
+ }
+
+ // Takes a base64 and return an object.
+ function performRecordDecrypt(ciphertext) {
+ if (obj.dbRecordsDecryptKey == null) return null;
+ const ciphertextBytes = Buffer.from(ciphertext, 'base64');
+ const iv = ciphertextBytes.slice(0, 12);
+ const data = ciphertextBytes.slice(28);
+ const aes = parent.crypto.createDecipheriv('aes-256-gcm', obj.dbRecordsDecryptKey, iv);
+ aes.setAuthTag(ciphertextBytes.slice(12, 28));
+ var plaintextBytes, r;
+ try {
+ plaintextBytes = Buffer.from(aes.update(data));
+ plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
+ r = JSON.parse(plaintextBytes.toString());
+ } catch (e) { throw "Incorrect DbRecordsDecryptKey/DbRecordsEncryptKey or invalid database _CRYPT data: " + e; }
+ return r;
+ }
+
+ // Clone an object (TODO: Make this more efficient)
+ function Clone(v) { return JSON.parse(JSON.stringify(v)); }
+
+ // Read expiration time from configuration file
+ if (typeof parent.args.dbexpire == 'object') {
+ if (typeof parent.args.dbexpire.events == 'number') { expireEventsSeconds = parent.args.dbexpire.events; }
+ if (typeof parent.args.dbexpire.powerevents == 'number') { expirePowerEventsSeconds = parent.args.dbexpire.powerevents; }
+ if (typeof parent.args.dbexpire.statsevents == 'number') { expireServerStatsSeconds = parent.args.dbexpire.statsevents; }
+ }
+
+ // If a DB record encryption key is provided, perform database record encryption
+ if ((typeof parent.args.dbrecordsencryptkey == 'string') && (parent.args.dbrecordsencryptkey.length != 0)) {
+ // Hash the database password into a AES256 key and setup encryption and decryption.
+ obj.dbRecordsEncryptKey = obj.dbRecordsDecryptKey = parent.crypto.createHash('sha384').update(parent.args.dbrecordsencryptkey).digest('raw').slice(0, 32);
+ }
+
+ // If a DB record decryption key is provided, perform database record decryption
+ if ((typeof parent.args.dbrecordsdecryptkey == 'string') && (parent.args.dbrecordsdecryptkey.length != 0)) {
+ // Hash the database password into a AES256 key and setup encryption and decryption.
+ obj.dbRecordsDecryptKey = parent.crypto.createHash('sha384').update(parent.args.dbrecordsdecryptkey).digest('raw').slice(0, 32);
+ }
+
+
+ function createTablesIfNotExist(dbname) {
+ var useDatabase = 'USE ' + dbname;
+ sqlDbQuery(useDatabase, null, function (err, docs) {
+ if (err != null) {
+ console.log("Unable to connect to database: " + err);
+ process.exit();
+ }
+ if (err == null) {
+ parent.debug('db', 'Checking tables...');
+ sqlDbBatchExec([
+ 'CREATE TABLE IF NOT EXISTS main (id VARCHAR(256) NOT NULL, type CHAR(32), domain CHAR(64), extra CHAR(255), extraex CHAR(255), doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))',
+ 'CREATE TABLE IF NOT EXISTS events (id INT NOT NULL AUTO_INCREMENT, time DATETIME, domain CHAR(64), action CHAR(255), nodeid CHAR(255), userid CHAR(255), doc JSON, PRIMARY KEY(id), CHECK(json_valid(doc)))',
+ 'CREATE TABLE IF NOT EXISTS eventids (fkid INT NOT NULL, target CHAR(255), CONSTRAINT fk_eventid FOREIGN KEY (fkid) REFERENCES events (id) ON DELETE CASCADE ON UPDATE RESTRICT)',
+ 'CREATE TABLE IF NOT EXISTS serverstats (time DATETIME, expire DATETIME, doc JSON, PRIMARY KEY(time), CHECK (json_valid(doc)))',
+ 'CREATE TABLE IF NOT EXISTS power (id INT NOT NULL AUTO_INCREMENT, time DATETIME, nodeid CHAR(255), doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))',
+ 'CREATE TABLE IF NOT EXISTS smbios (id CHAR(255), time DATETIME, expire DATETIME, doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))',
+ 'CREATE TABLE IF NOT EXISTS plugin (id INT NOT NULL AUTO_INCREMENT, doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))'
+ ], function (err) {
+ parent.debug('db', 'Checking indexes...');
+ sqlDbExec('CREATE INDEX ndxtypedomainextra ON main (type, domain, extra)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxextra ON main (extra)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxextraex ON main (extraex)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxeventstime ON events(time)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxeventsusername ON events(domain, userid, time)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxeventsdomainnodeidtime ON events(domain, nodeid, time)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxeventids ON eventids(target)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxserverstattime ON serverstats (time)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxserverstatexpire ON serverstats (expire)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxpowernodeidtime ON power (nodeid, time)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxsmbiostime ON smbios (time)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxsmbiosexpire ON smbios (expire)', null, function (err, response) { });
+ setupFunctions(func);
+ });
+ }
+ });
+ }
+
+ if (parent.args.sqlite3) {
+ // SQLite3 database setup
+ obj.databaseType = DB_SQLITE;
+ const sqlite3 = require('sqlite3');
+ let configParams = parent.config.settings.sqlite3;
+ if (typeof configParams == 'string') {databaseName = configParams} else {databaseName = configParams.name ? configParams.name : 'meshcentral';};
+ obj.sqliteConfig.startupVacuum = configParams.startupvacuum ? configParams.startupvacuum : false;
+ obj.sqliteConfig.autoVacuum = configParams.autovacuum ? configParams.autovacuum.toLowerCase() : 'incremental';
+ obj.sqliteConfig.incrementalVacuum = configParams.incrementalvacuum ? configParams.incrementalvacuum : 100;
+ obj.sqliteConfig.journalMode = configParams.journalmode ? configParams.journalmode.toLowerCase() : 'delete';
+ //allowed modes, 'none' excluded because not usefull for this app, maybe also remove 'memory'?
+ if (!(['delete', 'truncate', 'persist', 'memory', 'wal'].includes(obj.sqliteConfig.journalMode))) { obj.sqliteConfig.journalMode = 'delete'};
+ obj.sqliteConfig.journalSize = configParams.journalsize ? configParams.journalsize : 409600;
+ //wal can use the more performant 'normal' mode, see https://www.sqlite.org/pragma.html#pragma_synchronous
+ obj.sqliteConfig.synchronous = (obj.sqliteConfig.journalMode == 'wal') ? 'normal' : 'full';
+ if (obj.sqliteConfig.journalMode == 'wal') {obj.sqliteConfig.maintenance += 'PRAGMA wal_checkpoint(PASSIVE);'};
+ if (obj.sqliteConfig.autoVacuum == 'incremental') {obj.sqliteConfig.maintenance += 'PRAGMA incremental_vacuum(' + obj.sqliteConfig.incrementalVacuum + ');'};
+ obj.sqliteConfig.maintenance += 'PRAGMA optimize;';
+
+ parent.debug('db', 'SQlite config options: ' + JSON.stringify(obj.sqliteConfig, null, 4));
+ if (obj.sqliteConfig.journalMode == 'memory') { console.log('[WARNING] journal_mode=memory: this can lead to database corruption if there is a crash during a transaction. See https://www.sqlite.org/pragma.html#pragma_journal_mode') };
+ //.cached not usefull
+ obj.file = new sqlite3.Database(path.join(parent.datapath, databaseName + '.sqlite'), sqlite3.OPEN_READWRITE, function (err) {
+ if (err && (err.code == 'SQLITE_CANTOPEN')) {
+ // Database needs to be created
+ obj.file = new sqlite3.Database(path.join(parent.datapath, databaseName + '.sqlite'), function (err) {
+ if (err) { console.log("SQLite Error: " + err); process.exit(1); }
+ obj.file.exec(`
+ CREATE TABLE main (id VARCHAR(256) PRIMARY KEY NOT NULL, type CHAR(32), domain CHAR(64), extra CHAR(255), extraex CHAR(255), doc JSON);
+ CREATE TABLE events(id INTEGER PRIMARY KEY, time TIMESTAMP, domain CHAR(64), action CHAR(255), nodeid CHAR(255), userid CHAR(255), doc JSON);
+ CREATE TABLE eventids(fkid INT NOT NULL, target CHAR(255), CONSTRAINT fk_eventid FOREIGN KEY (fkid) REFERENCES events (id) ON DELETE CASCADE ON UPDATE RESTRICT);
+ CREATE TABLE serverstats (time TIMESTAMP PRIMARY KEY, expire TIMESTAMP, doc JSON);
+ CREATE TABLE power (id INTEGER PRIMARY KEY, time TIMESTAMP, nodeid CHAR(255), doc JSON);
+ CREATE TABLE smbios (id CHAR(255) PRIMARY KEY, time TIMESTAMP, expire TIMESTAMP, doc JSON);
+ CREATE TABLE plugin (id INTEGER PRIMARY KEY, doc JSON);
+ CREATE INDEX ndxtypedomainextra ON main (type, domain, extra);
+ CREATE INDEX ndxextra ON main (extra);
+ CREATE INDEX ndxextraex ON main (extraex);
+ CREATE INDEX ndxeventstime ON events(time);
+ CREATE INDEX ndxeventsusername ON events(domain, userid, time);
+ CREATE INDEX ndxeventsdomainnodeidtime ON events(domain, nodeid, time);
+ CREATE INDEX ndxeventids ON eventids(target);
+ CREATE INDEX ndxserverstattime ON serverstats (time);
+ CREATE INDEX ndxserverstatexpire ON serverstats (expire);
+ CREATE INDEX ndxpowernodeidtime ON power (nodeid, time);
+ CREATE INDEX ndxsmbiostime ON smbios (time);
+ CREATE INDEX ndxsmbiosexpire ON smbios (expire);
+ `, function (err) {
+ // Completed DB creation of SQLite3
+ sqliteSetOptions(func);
+ //setupFunctions could be put in the sqliteSetupOptions, but left after it for clarity
+ setupFunctions(func);
+ }
+ );
+ });
+ return;
+ } else if (err) { console.log("SQLite Error: " + err); process.exit(0); }
+
+ //for existing db's
+ sqliteSetOptions();
+ //setupFunctions could be put in the sqliteSetupOptions, but left after it for clarity
+ setupFunctions(func);
+ });
+ } else if (parent.args.acebase) {
+ // AceBase database setup
+ obj.databaseType = DB_ACEBASE;
+ const { AceBase } = require('acebase');
+ // For information on AceBase sponsor: https://github.com/appy-one/acebase/discussions/100
+ obj.file = new AceBase('meshcentral', { sponsor: ((typeof parent.args.acebase == 'object') && (parent.args.acebase.sponsor)), logLevel: 'error', storage: { path: parent.datapath } });
+ // Get all the databases ready
+ obj.file.ready(function () {
+ // Create AceBase indexes
+ obj.file.indexes.create('meshcentral', 'type', { include: ['domain', 'meshid'] });
+ obj.file.indexes.create('meshcentral', 'email');
+ obj.file.indexes.create('meshcentral', 'meshid');
+ obj.file.indexes.create('meshcentral', 'intelamt.uuid');
+ obj.file.indexes.create('events', 'userid', { include: ['action'] });
+ obj.file.indexes.create('events', 'domain', { include: ['nodeid', 'time'] });
+ obj.file.indexes.create('events', 'ids', { include: ['time'] });
+ obj.file.indexes.create('events', 'time');
+ obj.file.indexes.create('power', 'nodeid', { include: ['time'] });
+ obj.file.indexes.create('power', 'time');
+ obj.file.indexes.create('stats', 'time');
+ obj.file.indexes.create('stats', 'expire');
+ // Completed setup of AceBase
+ setupFunctions(func);
+ });
+ } else if (parent.args.mariadb || parent.args.mysql) {
+ var connectinArgs = (parent.args.mariadb) ? parent.args.mariadb : parent.args.mysql;
+ if (typeof connectinArgs == 'string') {
+ const parts = connectinArgs.split(/[:@/]+/);
+ var connectionObject = {
+ "user": parts[1],
+ "password": parts[2],
+ "host": parts[3],
+ "port": parts[4],
+ "database": parts[5]
+ };
+ var dbname = (connectionObject.database != null) ? connectionObject.database : 'meshcentral';
+ } else {
+ var dbname = (connectinArgs.database != null) ? connectinArgs.database : 'meshcentral';
+
+ // Including the db name in the connection obj will cause a connection faliure if it does not exist
+ var connectionObject = Clone(connectinArgs);
+ delete connectionObject.database;
+
+ try {
+ if (connectinArgs.ssl) {
+ if (connectinArgs.ssl.dontcheckserveridentity == true) { connectionObject.ssl.checkServerIdentity = function (name, cert) { return undefined; } };
+ if (connectinArgs.ssl.cacertpath) { connectionObject.ssl.ca = [require('fs').readFileSync(connectinArgs.ssl.cacertpath, 'utf8')]; }
+ if (connectinArgs.ssl.clientcertpath) { connectionObject.ssl.cert = [require('fs').readFileSync(connectinArgs.ssl.clientcertpath, 'utf8')]; }
+ if (connectinArgs.ssl.clientkeypath) { connectionObject.ssl.key = [require('fs').readFileSync(connectinArgs.ssl.clientkeypath, 'utf8')]; }
+ }
+ } catch (ex) {
+ console.log('Error loading SQL Connector certificate: ' + ex);
+ process.exit();
+ }
+ }
+
+ if (parent.args.mariadb) {
+ // Use MariaDB
+ obj.databaseType = DB_MARIADB;
+ var tempDatastore = require('mariadb').createPool(connectionObject);
+ tempDatastore.getConnection().then(function (conn) {
+ conn.query('CREATE DATABASE IF NOT EXISTS ' + dbname).then(function (result) {
+ conn.release();
+ }).catch(function (ex) { console.log('Auto-create database failed: ' + ex); });
+ }).catch(function (ex) { console.log('Auto-create database failed: ' + ex); });
+ setTimeout(function () { tempDatastore.end(); }, 2000);
+
+ connectionObject.database = dbname;
+ Datastore = require('mariadb').createPool(connectionObject);
+ createTablesIfNotExist(dbname);
+ } else if (parent.args.mysql) {
+ // Use MySQL
+ obj.databaseType = DB_MYSQL;
+ var tempDatastore = require('mysql2').createPool(connectionObject);
+ tempDatastore.query('CREATE DATABASE IF NOT EXISTS ' + dbname, function (error) {
+ if (error != null) {
+ console.log('Auto-create database failed: ' + error);
+ }
+ connectionObject.database = dbname;
+ Datastore = require('mysql2').createPool(connectionObject);
+ createTablesIfNotExist(dbname);
+ });
+ setTimeout(function () { tempDatastore.end(); }, 2000);
+ }
+ } else if (parent.args.postgres) {
+ // Postgres SQL
+ let connectinArgs = parent.args.postgres;
+ connectinArgs.database = (databaseName = (connectinArgs.database != null) ? connectinArgs.database : 'meshcentral');
+
+ let DatastoreTest;
+ obj.databaseType = DB_POSTGRESQL;
+ const { Client } = require('pg');
+ Datastore = new Client(connectinArgs);
+ //Connect to and check pg db first to check if own db exists. Otherwise errors out on 'database does not exist'
+ connectinArgs.database = 'postgres';
+ DatastoreTest = new Client(connectinArgs);
+ DatastoreTest.connect();
+ connectinArgs.database = databaseName; //put the name back for backupconfig info
+ DatastoreTest.query('SELECT 1 FROM pg_catalog.pg_database WHERE datname = $1', [databaseName], function (err, res) { // check database exists first before creating
+ if (res.rowCount != 0) { // database exists now check tables exists
+ DatastoreTest.end();
+ Datastore.connect();
+ Datastore.query('SELECT doc FROM main WHERE id = $1', ['DatabaseIdentifier'], function (err, res) {
+ if (err == null) {
+ (res.rowCount ==0) ? postgreSqlCreateTables(func) : setupFunctions(func)
+ } else
+ if (err.code == '42P01') { //42P01 = undefined table, https://www.postgresql.org/docs/current/errcodes-appendix.html
+ postgreSqlCreateTables(func);
+ } else {
+ console.log('Postgresql database exists, other error: ', err.message); process.exit(0);
+ };
+ });
+ } else { // If not present, create the tables and indexes
+ //not needed, just use a create db statement: const pgtools = require('pgtools');
+ DatastoreTest.query('CREATE DATABASE "'+ databaseName + '";', [], function (err, res) {
+ if (err == null) {
+ // Create the tables and indexes
+ DatastoreTest.end();
+ Datastore.connect();
+ postgreSqlCreateTables(func);
+ } else {
+ console.log('Postgresql database create error: ', err.message);
+ process.exit(0);
+ }
+ });
+ }
+ });
+ } else if (parent.args.mongodb) {
+ // Use MongoDB
+ obj.databaseType = DB_MONGODB;
+
+ // If running an older NodeJS version, TextEncoder/TextDecoder is required
+ if (global.TextEncoder == null) { global.TextEncoder = require('util').TextEncoder; }
+ if (global.TextDecoder == null) { global.TextDecoder = require('util').TextDecoder; }
+
+ require('mongodb').MongoClient.connect(parent.args.mongodb, { useNewUrlParser: true, useUnifiedTopology: true, enableUtf8Validation: false }, function (err, client) {
+ if (err != null) { console.log("Unable to connect to database: " + err); process.exit(); return; }
+ Datastore = client;
+ parent.debug('db', 'Connected to MongoDB database...');
+
+ // Get the database name and setup the database client
+ var dbname = 'meshcentral';
+ if (parent.args.mongodbname) { dbname = parent.args.mongodbname; }
+ const dbcollectionname = (parent.args.mongodbcol) ? (parent.args.mongodbcol) : 'meshcentral';
+ const db = client.db(dbname);
+
+ // Check the database version
+ db.admin().serverInfo(function (err, info) {
+ if ((err != null) || (info == null) || (info.versionArray == null) || (Array.isArray(info.versionArray) == false) || (info.versionArray.length < 2) || (typeof info.versionArray[0] != 'number') || (typeof info.versionArray[1] != 'number')) {
+ console.log('WARNING: Unable to check MongoDB version.');
+ } else {
+ if ((info.versionArray[0] < 3) || ((info.versionArray[0] == 3) && (info.versionArray[1] < 6))) {
+ // We are running with mongoDB older than 3.6, this is not good.
+ parent.addServerWarning("Current version of MongoDB (" + info.version + ") is too old, please upgrade to MongoDB 3.6 or better.", true);
+ }
+ }
+ });
+
+ // Setup MongoDB main collection and indexes
+ obj.file = db.collection(dbcollectionname);
+ obj.file.indexes(function (err, indexes) {
+ // Check if we need to reset indexes
+ var indexesByName = {}, indexCount = 0;
+ for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
+ if ((indexCount != 5) || (indexesByName['TypeDomainMesh1'] == null) || (indexesByName['Email1'] == null) || (indexesByName['Mesh1'] == null) || (indexesByName['AmtUuid1'] == null)) {
+ console.log('Resetting main indexes...');
+ obj.file.dropIndexes(function (err) {
+ obj.file.createIndex({ type: 1, domain: 1, meshid: 1 }, { sparse: 1, name: 'TypeDomainMesh1' }); // Speeds up GetAllTypeNoTypeField() and GetAllTypeNoTypeFieldMeshFiltered()
+ obj.file.createIndex({ email: 1 }, { sparse: 1, name: 'Email1' }); // Speeds up GetUserWithEmail() and GetUserWithVerifiedEmail()
+ obj.file.createIndex({ meshid: 1 }, { sparse: 1, name: 'Mesh1' }); // Speeds up RemoveMesh()
+ obj.file.createIndex({ 'intelamt.uuid': 1 }, { sparse: 1, name: 'AmtUuid1' }); // Speeds up getAmtUuidMeshNode()
+ });
+ }
+ });
+
+ // Setup the changeStream on the MongoDB main collection if possible
+ if (parent.args.mongodbchangestream == true) {
+ obj.dbCounters.changeStream = { change: 0, update: 0, insert: 0, delete: 0 };
+ if (typeof obj.file.watch != 'function') {
+ console.log('WARNING: watch() is not a function, MongoDB ChangeStream not supported.');
+ } else {
+ obj.fileChangeStream = obj.file.watch([{ $match: { $or: [{ 'fullDocument.type': { $in: ['node', 'mesh', 'user', 'ugrp'] } }, { 'operationType': 'delete' }] } }], { fullDocument: 'updateLookup' });
+ obj.fileChangeStream.on('change', function (change) {
+ obj.dbCounters.changeStream.change++;
+ if ((change.operationType == 'update') || (change.operationType == 'replace')) {
+ obj.dbCounters.changeStream.update++;
+ switch (change.fullDocument.type) {
+ case 'node': { dbNodeChange(change, false); break; } // A node has changed
+ case 'mesh': { dbMeshChange(change, false); break; } // A device group has changed
+ case 'user': { dbUserChange(change, false); break; } // A user account has changed
+ case 'ugrp': { dbUGrpChange(change, false); break; } // A user account has changed
+ }
+ } else if (change.operationType == 'insert') {
+ obj.dbCounters.changeStream.insert++;
+ switch (change.fullDocument.type) {
+ case 'node': { dbNodeChange(change, true); break; } // A node has added
+ case 'mesh': { dbMeshChange(change, true); break; } // A device group has created
+ case 'user': { dbUserChange(change, true); break; } // A user account has created
+ case 'ugrp': { dbUGrpChange(change, true); break; } // A user account has created
+ }
+ } else if (change.operationType == 'delete') {
+ obj.dbCounters.changeStream.delete++;
+ if ((change.documentKey == null) || (change.documentKey._id == null)) return;
+ var splitId = change.documentKey._id.split('/');
+ switch (splitId[0]) {
+ case 'node': {
+ //Not Good: Problem here is that we don't know what meshid the node belonged to before the delete.
+ //parent.DispatchEvent(['*', node.meshid], obj, { etype: 'node', action: 'removenode', nodeid: change.documentKey._id, domain: splitId[1] });
+ break;
+ }
+ case 'mesh': {
+ parent.DispatchEvent(['*', change.documentKey._id], obj, { etype: 'mesh', action: 'deletemesh', meshid: change.documentKey._id, domain: splitId[1] });
+ break;
+ }
+ case 'user': {
+ //Not Good: This is not a perfect user removal because we don't know what groups the user was in.
+ //parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', action: 'accountremove', userid: change.documentKey._id, domain: splitId[1], username: splitId[2] });
+ break;
+ }
+ case 'ugrp': {
+ parent.DispatchEvent(['*', change.documentKey._id], obj, { etype: 'ugrp', action: 'deleteusergroup', ugrpid: change.documentKey._id, domain: splitId[1] });
+ break;
+ }
+ }
+ }
+ });
+ obj.changeStream = true;
+ }
+ }
+
+ // Setup MongoDB events collection and indexes
+ obj.eventsfile = db.collection('events'); // Collection containing all events
+ obj.eventsfile.indexes(function (err, indexes) {
+ // Check if we need to reset indexes
+ var indexesByName = {}, indexCount = 0;
+ for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
+ if ((indexCount != 5) || (indexesByName['UseridAction1'] == null) || (indexesByName['DomainNodeTime1'] == null) || (indexesByName['IdsAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
+ // Reset all indexes
+ console.log("Resetting events indexes...");
+ obj.eventsfile.dropIndexes(function (err) {
+ obj.eventsfile.createIndex({ userid: 1, action: 1 }, { sparse: 1, name: 'UseridAction1' });
+ obj.eventsfile.createIndex({ domain: 1, nodeid: 1, time: -1 }, { sparse: 1, name: 'DomainNodeTime1' });
+ obj.eventsfile.createIndex({ ids: 1, time: -1 }, { sparse: 1, name: 'IdsAndTime1' });
+ obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
+ });
+ } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireEventsSeconds) {
+ // Reset the timeout index
+ console.log("Resetting events expire index...");
+ obj.eventsfile.dropIndex('ExpireTime1', function (err) {
+ obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
+ });
+ }
+ });
+
+ // Setup MongoDB power events collection and indexes
+ obj.powerfile = db.collection('power'); // Collection containing all power events
+ obj.powerfile.indexes(function (err, indexes) {
+ // Check if we need to reset indexes
+ var indexesByName = {}, indexCount = 0;
+ for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
+ if ((indexCount != 3) || (indexesByName['NodeIdAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
+ // Reset all indexes
+ console.log("Resetting power events indexes...");
+ obj.powerfile.dropIndexes(function (err) {
+ // Create all indexes
+ obj.powerfile.createIndex({ nodeid: 1, time: 1 }, { sparse: 1, name: 'NodeIdAndTime1' });
+ obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
+ });
+ } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expirePowerEventsSeconds) {
+ // Reset the timeout index
+ console.log("Resetting power events expire index...");
+ obj.powerfile.dropIndex('ExpireTime1', function (err) {
+ // Reset the expire power events index
+ obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
+ });
+ }
+ });
+
+ // Setup MongoDB smbios collection, no indexes needed
+ obj.smbiosfile = db.collection('smbios'); // Collection containing all smbios information
+
+ // Setup MongoDB server stats collection
+ obj.serverstatsfile = db.collection('serverstats'); // Collection of server stats
+ obj.serverstatsfile.indexes(function (err, indexes) {
+ // Check if we need to reset indexes
+ var indexesByName = {}, indexCount = 0;
+ for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
+ if ((indexCount != 3) || (indexesByName['ExpireTime1'] == null)) {
+ // Reset all indexes
+ console.log("Resetting server stats indexes...");
+ obj.serverstatsfile.dropIndexes(function (err) {
+ // Create all indexes
+ obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
+ obj.serverstatsfile.createIndex({ 'expire': 1 }, { expireAfterSeconds: 0, name: 'ExpireTime2' }); // Auto-expire events
+ });
+ } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireServerStatsSeconds) {
+ // Reset the timeout index
+ console.log("Resetting server stats expire index...");
+ obj.serverstatsfile.dropIndex('ExpireTime1', function (err) {
+ // Reset the expire server stats index
+ obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
+ });
+ }
+ });
+
+ // Setup plugin info collection
+ if (obj.pluginsActive) { obj.pluginsfile = db.collection('plugins'); }
+
+ setupFunctions(func); // Completed setup of MongoDB
+ });
+ } else if (parent.args.xmongodb) {
+ // Use MongoJS, this is the old system.
+ obj.databaseType = DB_MONGOJS;
+ Datastore = require('mongojs');
+ var db = Datastore(parent.args.xmongodb);
+ var dbcollection = 'meshcentral';
+ if (parent.args.mongodbcol) { dbcollection = parent.args.mongodbcol; }
+
+ // Setup MongoDB main collection and indexes
+ obj.file = db.collection(dbcollection);
+ obj.file.getIndexes(function (err, indexes) {
+ // Check if we need to reset indexes
+ var indexesByName = {}, indexCount = 0;
+ for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
+ if ((indexCount != 5) || (indexesByName['TypeDomainMesh1'] == null) || (indexesByName['Email1'] == null) || (indexesByName['Mesh1'] == null) || (indexesByName['AmtUuid1'] == null)) {
+ console.log("Resetting main indexes...");
+ obj.file.dropIndexes(function (err) {
+ obj.file.createIndex({ type: 1, domain: 1, meshid: 1 }, { sparse: 1, name: 'TypeDomainMesh1' }); // Speeds up GetAllTypeNoTypeField() and GetAllTypeNoTypeFieldMeshFiltered()
+ obj.file.createIndex({ email: 1 }, { sparse: 1, name: 'Email1' }); // Speeds up GetUserWithEmail() and GetUserWithVerifiedEmail()
+ obj.file.createIndex({ meshid: 1 }, { sparse: 1, name: 'Mesh1' }); // Speeds up RemoveMesh()
+ obj.file.createIndex({ 'intelamt.uuid': 1 }, { sparse: 1, name: 'AmtUuid1' }); // Speeds up getAmtUuidMeshNode()
+ });
+ }
+ });
+
+ // Setup MongoDB events collection and indexes
+ obj.eventsfile = db.collection('events'); // Collection containing all events
+ obj.eventsfile.getIndexes(function (err, indexes) {
+ // Check if we need to reset indexes
+ var indexesByName = {}, indexCount = 0;
+ for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
+ if ((indexCount != 5) || (indexesByName['UseridAction1'] == null) || (indexesByName['DomainNodeTime1'] == null) || (indexesByName['IdsAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
+ // Reset all indexes
+ console.log("Resetting events indexes...");
+ obj.eventsfile.dropIndexes(function (err) {
+ obj.eventsfile.createIndex({ userid: 1, action: 1 }, { sparse: 1, name: 'UseridAction1' });
+ obj.eventsfile.createIndex({ domain: 1, nodeid: 1, time: -1 }, { sparse: 1, name: 'DomainNodeTime1' });
+ obj.eventsfile.createIndex({ ids: 1, time: -1 }, { sparse: 1, name: 'IdsAndTime1' });
+ obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
+ });
+ } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireEventsSeconds) {
+ // Reset the timeout index
+ console.log("Resetting events expire index...");
+ obj.eventsfile.dropIndex('ExpireTime1', function (err) {
+ obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
+ });
+ }
+ });
+
+ // Setup MongoDB power events collection and indexes
+ obj.powerfile = db.collection('power'); // Collection containing all power events
+ obj.powerfile.getIndexes(function (err, indexes) {
+ // Check if we need to reset indexes
+ var indexesByName = {}, indexCount = 0;
+ for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
+ if ((indexCount != 3) || (indexesByName['NodeIdAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
+ // Reset all indexes
+ console.log("Resetting power events indexes...");
+ obj.powerfile.dropIndexes(function (err) {
+ // Create all indexes
+ obj.powerfile.createIndex({ nodeid: 1, time: 1 }, { sparse: 1, name: 'NodeIdAndTime1' });
+ obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
+ });
+ } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expirePowerEventsSeconds) {
+ // Reset the timeout index
+ console.log("Resetting power events expire index...");
+ obj.powerfile.dropIndex('ExpireTime1', function (err) {
+ // Reset the expire power events index
+ obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
+ });
+ }
+ });
+
+ // Setup MongoDB smbios collection, no indexes needed
+ obj.smbiosfile = db.collection('smbios'); // Collection containing all smbios information
+
+ // Setup MongoDB server stats collection
+ obj.serverstatsfile = db.collection('serverstats'); // Collection of server stats
+ obj.serverstatsfile.getIndexes(function (err, indexes) {
+ // Check if we need to reset indexes
+ var indexesByName = {}, indexCount = 0;
+ for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
+ if ((indexCount != 3) || (indexesByName['ExpireTime1'] == null)) {
+ // Reset all indexes
+ console.log("Resetting server stats indexes...");
+ obj.serverstatsfile.dropIndexes(function (err) {
+ // Create all indexes
+ obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
+ obj.serverstatsfile.createIndex({ 'expire': 1 }, { expireAfterSeconds: 0, name: 'ExpireTime2' }); // Auto-expire events
+ });
+ } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireServerStatsSeconds) {
+ // Reset the timeout index
+ console.log("Resetting server stats expire index...");
+ obj.serverstatsfile.dropIndex('ExpireTime1', function (err) {
+ // Reset the expire server stats index
+ obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
+ });
+ }
+ });
+
+ // Setup plugin info collection
+ if (obj.pluginsActive) { obj.pluginsfile = db.collection('plugins'); }
+
+ setupFunctions(func); // Completed setup of MongoJS
+ } else {
+ // Use NeDB (The default)
+ obj.databaseType = DB_NEDB;
+ try { Datastore = require('@seald-io/nedb'); } catch (ex) { } // This is the NeDB with Node 23 support.
+ if (Datastore == null) {
+ try { Datastore = require('@yetzt/nedb'); } catch (ex) { } // This is the NeDB with fixed security dependencies.
+ if (Datastore == null) { Datastore = require('nedb'); } // So not to break any existing installations, if the old NeDB is present, use it.
+ }
+ var datastoreOptions = { filename: parent.getConfigFilePath('meshcentral.db'), autoload: true };
+
+ // If a DB encryption key is provided, perform database encryption
+ if ((typeof parent.args.dbencryptkey == 'string') && (parent.args.dbencryptkey.length != 0)) {
+ // Hash the database password into a AES256 key and setup encryption and decryption.
+ obj.dbKey = parent.crypto.createHash('sha384').update(parent.args.dbencryptkey).digest('raw').slice(0, 32);
+ datastoreOptions.afterSerialization = function (plaintext) {
+ const iv = parent.crypto.randomBytes(16);
+ const aes = parent.crypto.createCipheriv('aes-256-cbc', obj.dbKey, iv);
+ var ciphertext = aes.update(plaintext);
+ ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
+ return ciphertext.toString('base64');
+ }
+ datastoreOptions.beforeDeserialization = function (ciphertext) {
+ const ciphertextBytes = Buffer.from(ciphertext, 'base64');
+ const iv = ciphertextBytes.slice(0, 16);
+ const data = ciphertextBytes.slice(16);
+ const aes = parent.crypto.createDecipheriv('aes-256-cbc', obj.dbKey, iv);
+ var plaintextBytes = Buffer.from(aes.update(data));
+ plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
+ return plaintextBytes.toString();
+ }
+ }
+
+ // Start NeDB main collection and setup indexes
+ obj.file = new Datastore(datastoreOptions);
+ obj.file.setAutocompactionInterval(86400000); // Compact once a day
+ obj.file.ensureIndex({ fieldName: 'type' });
+ obj.file.ensureIndex({ fieldName: 'domain' });
+ obj.file.ensureIndex({ fieldName: 'meshid', sparse: true });
+ obj.file.ensureIndex({ fieldName: 'nodeid', sparse: true });
+ obj.file.ensureIndex({ fieldName: 'email', sparse: true });
+
+ // Setup the events collection and setup indexes
+ obj.eventsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-events.db'), autoload: true, corruptAlertThreshold: 1 });
+ obj.eventsfile.setAutocompactionInterval(86400000); // Compact once a day
+ obj.eventsfile.ensureIndex({ fieldName: 'ids' }); // TODO: Not sure if this is a good index, this is a array field.
+ obj.eventsfile.ensureIndex({ fieldName: 'nodeid', sparse: true });
+ obj.eventsfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: expireEventsSeconds });
+ obj.eventsfile.remove({ time: { '$lt': new Date(Date.now() - (expireEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
+
+ // Setup the power collection and setup indexes
+ obj.powerfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-power.db'), autoload: true, corruptAlertThreshold: 1 });
+ obj.powerfile.setAutocompactionInterval(86400000); // Compact once a day
+ obj.powerfile.ensureIndex({ fieldName: 'nodeid' });
+ obj.powerfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: expirePowerEventsSeconds });
+ obj.powerfile.remove({ time: { '$lt': new Date(Date.now() - (expirePowerEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
+
+ // Setup the SMBIOS collection, for NeDB we don't setup SMBIOS since NeDB will corrupt the database. Remove any existing ones.
+ //obj.smbiosfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-smbios.db'), autoload: true, corruptAlertThreshold: 1 });
+ fs.unlink(parent.getConfigFilePath('meshcentral-smbios.db'), function () { });
+
+ // Setup the server stats collection and setup indexes
+ obj.serverstatsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-stats.db'), autoload: true, corruptAlertThreshold: 1 });
+ obj.serverstatsfile.setAutocompactionInterval(86400000); // Compact once a day
+ obj.serverstatsfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: expireServerStatsSeconds });
+ obj.serverstatsfile.ensureIndex({ fieldName: 'expire', expireAfterSeconds: 0 }); // Auto-expire events
+ obj.serverstatsfile.remove({ time: { '$lt': new Date(Date.now() - (expireServerStatsSeconds * 1000)) } }, { multi: true }); // Force delete older events
+
+ // Setup plugin info collection
+ if (obj.pluginsActive) {
+ obj.pluginsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-plugins.db'), autoload: true });
+ obj.pluginsfile.setAutocompactionInterval(86400000); // Compact once a day
+ }
+
+ setupFunctions(func); // Completed setup of NeDB
+ }
+
+ function sqliteSetOptions(func) {
+ //get current auto_vacuum mode for comparison
+ obj.file.get('PRAGMA auto_vacuum;', function(err, current){
+ let pragma = 'PRAGMA journal_mode=' + obj.sqliteConfig.journalMode + ';' +
+ 'PRAGMA synchronous='+ obj.sqliteConfig.synchronous + ';' +
+ 'PRAGMA journal_size_limit=' + obj.sqliteConfig.journalSize + ';' +
+ 'PRAGMA auto_vacuum=' + obj.sqliteConfig.autoVacuum + ';' +
+ 'PRAGMA incremental_vacuum=' + obj.sqliteConfig.incrementalVacuum + ';' +
+ 'PRAGMA optimize=0x10002;';
+ //check new autovacuum mode, if changing from or to 'none', a VACUUM needs to be done to activate it. See https://www.sqlite.org/pragma.html#pragma_auto_vacuum
+ if ( obj.sqliteConfig.startupVacuum
+ || (current.auto_vacuum == 0 && obj.sqliteConfig.autoVacuum !='none')
+ || (current.auto_vacuum != 0 && obj.sqliteConfig.autoVacuum =='none'))
+ {
+ pragma += 'VACUUM;';
+ };
+ parent.debug ('db', 'Config statement: ' + pragma);
+
+ obj.file.exec( pragma,
+ function (err) {
+ if (err) { parent.debug('db', 'Config pragma error: ' + (err.message)) };
+ sqliteGetPragmas(['journal_mode', 'journal_size_limit', 'freelist_count', 'auto_vacuum', 'page_size', 'wal_autocheckpoint', 'synchronous'], function (pragma, pragmaValue) {
+ parent.debug('db', 'PRAGMA: ' + pragma + '=' + pragmaValue);
+ });
+ });
+ });
+ //setupFunctions(func);
+ }
+
+ function sqliteGetPragmas (pragmas, func){
+ //pragmas can only be gotting one by one
+ pragmas.forEach (function (pragma) {
+ obj.file.get('PRAGMA ' + pragma + ';', function(err, res){
+ if (pragma == 'auto_vacuum') { res[pragma] = SQLITE_AUTOVACUUM[res[pragma]] };
+ if (pragma == 'synchronous') { res[pragma] = SQLITE_SYNCHRONOUS[res[pragma]] };
+ if (func) { func (pragma, res[pragma]); }
+ });
+ });
+ }
+ // Create the PostgreSQL tables
+ function postgreSqlCreateTables(func) {
+ // Database was created, create the tables
+ parent.debug('db', 'Creating tables...');
+ sqlDbBatchExec([
+ 'CREATE TABLE IF NOT EXISTS main (id VARCHAR(256) PRIMARY KEY NOT NULL, type CHAR(32), domain CHAR(64), extra CHAR(255), extraex CHAR(255), doc JSON)',
+ 'CREATE TABLE IF NOT EXISTS events(id SERIAL PRIMARY KEY, time TIMESTAMP, domain CHAR(64), action CHAR(255), nodeid CHAR(255), userid CHAR(255), doc JSON)',
+ 'CREATE TABLE IF NOT EXISTS eventids(fkid INT NOT NULL, target CHAR(255), CONSTRAINT fk_eventid FOREIGN KEY (fkid) REFERENCES events (id) ON DELETE CASCADE ON UPDATE RESTRICT)',
+ 'CREATE TABLE IF NOT EXISTS serverstats (time TIMESTAMP PRIMARY KEY, expire TIMESTAMP, doc JSON)',
+ 'CREATE TABLE IF NOT EXISTS power (id SERIAL PRIMARY KEY, time TIMESTAMP, nodeid CHAR(255), doc JSON)',
+ 'CREATE TABLE IF NOT EXISTS smbios (id CHAR(255) PRIMARY KEY, time TIMESTAMP, expire TIMESTAMP, doc JSON)',
+ 'CREATE TABLE IF NOT EXISTS plugin (id SERIAL PRIMARY KEY, doc JSON)'
+ ], function (results) {
+ parent.debug('db', 'Creating indexes...');
+ sqlDbExec('CREATE INDEX ndxtypedomainextra ON main (type, domain, extra)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxextra ON main (extra)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxextraex ON main (extraex)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxeventstime ON events(time)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxeventsusername ON events(domain, userid, time)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxeventsdomainnodeidtime ON events(domain, nodeid, time)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxeventids ON eventids(target)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxserverstattime ON serverstats (time)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxserverstatexpire ON serverstats (expire)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxpowernodeidtime ON power (nodeid, time)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxsmbiostime ON smbios (time)', null, function (err, response) { });
+ sqlDbExec('CREATE INDEX ndxsmbiosexpire ON smbios (expire)', null, function (err, response) { });
+ setupFunctions(func);
+ });
+ }
+
+ // Check the object names for a "."
+ function checkObjectNames(r, tag) {
+ if (typeof r != 'object') return;
+ for (var i in r) {
+ if (i.indexOf('.') >= 0) { throw ('BadDbName (' + tag + '): ' + JSON.stringify(r)); }
+ checkObjectNames(r[i], tag);
+ }
+ }
+
+ // Query the database
+ function sqlDbQuery(query, args, func, debug) {
+ if (obj.databaseType == DB_SQLITE) { // SQLite
+ if (args == null) { args = []; }
+ obj.file.all(query, args, function (err, docs) {
+ if (err != null) { console.log(query, args, err, docs); }
+ if (docs != null) {
+ for (var i in docs) {
+ if (typeof docs[i].doc == 'string') {
+ try { docs[i] = JSON.parse(docs[i].doc); } catch (ex) {
+ console.log(query, args, docs[i]);
+ }
+ }
+ }
+ }
+ if (func) { func(err, docs); }
+ });
+ } else if (obj.databaseType == DB_MARIADB) { // MariaDB
+ Datastore.getConnection()
+ .then(function (conn) {
+ conn.query(query, args)
+ .then(function (rows) {
+ conn.release();
+ var docs = [];
+ for (var i in rows) {
+ if (rows[i].doc) {
+ docs.push(performTypedRecordDecrypt((typeof rows[i].doc == 'object') ? rows[i].doc : JSON.parse(rows[i].doc)));
+ } else if ((rows.length == 1) && (rows[i]['COUNT(doc)'] != null)) {
+ // This is a SELECT COUNT() operation
+ docs = parseInt(rows[i]['COUNT(doc)']);
+ }
+ }
+ if (func) try { func(null, docs); } catch (ex) { console.log('SQLERR1', ex); }
+ })
+ .catch(function (err) { conn.release(); if (func) try { func(err); } catch (ex) { console.log('SQLERR2', ex); } });
+ }).catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log('SQLERR3', ex); } } });
+ } else if (obj.databaseType == DB_MYSQL) { // MySQL
+ Datastore.query(query, args, function (error, results, fields) {
+ if (error != null) {
+ if (func) try { func(error); } catch (ex) { console.log('SQLERR4', ex); }
+ } else {
+ var docs = [];
+ for (var i in results) {
+ if (results[i].doc) {
+ if (typeof results[i].doc == 'string') {
+ docs.push(JSON.parse(results[i].doc));
+ } else {
+ docs.push(results[i].doc);
+ }
+ } else if ((results.length == 1) && (results[i]['COUNT(doc)'] != null)) {
+ // This is a SELECT COUNT() operation
+ docs = results[i]['COUNT(doc)'];
+ }
+ }
+ if (func) { try { func(null, docs); } catch (ex) { console.log('SQLERR5', ex); } }
+ }
+ });
+ } else if (obj.databaseType == DB_POSTGRESQL) { // Postgres SQL
+ Datastore.query(query, args, function (error, results) {
+ if (error != null) {
+ if (func) try { func(error); } catch (ex) { console.log('SQLERR4', ex); }
+ } else {
+ var docs = [];
+ if ((results.command == 'INSERT') && (results.rows != null) && (results.rows.length == 1)) { docs = results.rows[0]; }
+ else if (results.command == 'SELECT') {
+ for (var i in results.rows) {
+ if (results.rows[i].doc) {
+ if (typeof results.rows[i].doc == 'string') {
+ docs.push(JSON.parse(results.rows[i].doc));
+ } else {
+ docs.push(results.rows[i].doc);
+ }
+ } else if (results.rows[i].count && (results.rows.length == 1)) {
+ // This is a SELECT COUNT() operation
+ docs = parseInt(results.rows[i].count);
+ }
+ }
+ }
+ if (func) { try { func(null, docs, results); } catch (ex) { console.log('SQLERR5', ex); } }
+ }
+ });
+ }
+ }
+
+ // Exec on the database
+ function sqlDbExec(query, args, func) {
+ if (obj.databaseType == DB_MARIADB) { // MariaDB
+ Datastore.getConnection()
+ .then(function (conn) {
+ conn.query(query, args)
+ .then(function (rows) {
+ conn.release();
+ if (func) try { func(null, rows[0]); } catch (ex) { console.log(ex); }
+ })
+ .catch(function (err) { conn.release(); if (func) try { func(err); } catch (ex) { console.log(ex); } });
+ }).catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } });
+ } else if ((obj.databaseType == DB_MYSQL) || (obj.databaseType == DB_POSTGRESQL)) { // MySQL or Postgres SQL
+ Datastore.query(query, args, function (error, results, fields) {
+ if (func) try { func(error, results ? results[0] : null); } catch (ex) { console.log(ex); }
+ });
+ }
+ }
+
+ // Execute a batch of commands on the database
+ function sqlDbBatchExec(queries, func) {
+ if (obj.databaseType == DB_MARIADB) { // MariaDB
+ Datastore.getConnection()
+ .then(function (conn) {
+ var Promises = [];
+ for (var i in queries) { if (typeof queries[i] == 'string') { Promises.push(conn.query(queries[i])); } else { Promises.push(conn.query(queries[i][0], queries[i][1])); } }
+ Promise.all(Promises)
+ .then(function (rows) { conn.release(); if (func) { try { func(null); } catch (ex) { console.log(ex); } } })
+ .catch(function (err) { conn.release(); if (func) { try { func(err); } catch (ex) { console.log(ex); } } });
+ })
+ .catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } });
+ } else if (obj.databaseType == DB_MYSQL) { // MySQL
+ Datastore.getConnection(function(err, connection) {
+ if (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } return; }
+ var Promises = [];
+ for (var i in queries) { if (typeof queries[i] == 'string') { Promises.push(connection.promise().query(queries[i])); } else { Promises.push(connection.promise().query(queries[i][0], queries[i][1])); } }
+ Promise.all(Promises)
+ .then(function (error, results, fields) { connection.release(); if (func) { try { func(error, results); } catch (ex) { console.log(ex); } } })
+ .catch(function (error, results, fields) { connection.release(); if (func) { try { func(error); } catch (ex) { console.log(ex); } } });
+ });
+ } else if (obj.databaseType == DB_POSTGRESQL) { // Postgres
+ var Promises = [];
+ for (var i in queries) { if (typeof queries[i] == 'string') { Promises.push(Datastore.query(queries[i])); } else { Promises.push(Datastore.query(queries[i][0], queries[i][1])); } }
+ Promise.all(Promises)
+ .then(function (error, results, fields) { if (func) { try { func(error, results); } catch (ex) { console.log(ex); } } })
+ .catch(function (error, results, fields) { if (func) { try { func(error); } catch (ex) { console.log(ex); } } });
+ }
+ }
+
+ function setupFunctions(func) {
+ if (obj.databaseType == DB_SQLITE) {
+ // Database actions on the main collection. SQLite3: https://www.linode.com/docs/guides/getting-started-with-nodejs-sqlite/
+ obj.Set = function (value, func) {
+ obj.dbCounters.fileSet++;
+ var extra = null, extraex = null;
+ value = common.escapeLinksFieldNameEx(value);
+ if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
+ if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
+ if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
+ sqlDbQuery('INSERT INTO main VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET type = $2, domain = $3, extra = $4, extraex = $5, doc = $6;', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, JSON.stringify(performTypedRecordEncrypt(value))], func);
+ }
+ obj.SetRaw = function (value, func) {
+ obj.dbCounters.fileSet++;
+ var extra = null, extraex = null;
+ if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
+ if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
+ if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
+ sqlDbQuery('INSERT INTO main VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET type = $2, domain = $3, extra = $4, extraex = $5, doc = $6;', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, JSON.stringify(performTypedRecordEncrypt(value))], func);
+ }
+ obj.Get = function (_id, func) {
+ sqlDbQuery('SELECT doc FROM main WHERE id = $1', [_id], function (err, docs) {
+ if ((docs != null) && (docs.length > 0)) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ }
+ obj.GetAll = function (func) {
+ sqlDbQuery('SELECT domain, doc FROM main', null, function (err, docs) {
+ if ((docs != null) && (docs.length > 0)) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ }
+ obj.GetHash = function (id, func) {
+ sqlDbQuery('SELECT doc FROM main WHERE id = $1', [id], function (err, docs) {
+ if ((docs != null) && (docs.length > 0)) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ }
+ obj.GetAllTypeNoTypeField = function (type, domain, func) {
+ sqlDbQuery('SELECT doc FROM main WHERE type = $1 AND domain = $2', [type, domain], function (err, docs) {
+ if ((docs != null) && (docs.length > 0)) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ };
+ obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
+ if (limit == 0) { limit = -1; } // In SQLite, no limit is -1
+ if (id && (id != '')) {
+ sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra IN (' + dbMergeSqlArray(meshes) + ')) LIMIT $4 OFFSET $5', [id, type, domain, limit, skip], function (err, docs) {
+ if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ } else {
+ if (extrasids == null) {
+ sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra IN (' + dbMergeSqlArray(meshes) + ')) LIMIT $3 OFFSET $4', [type, domain, limit, skip], function (err, docs) {
+ if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ } else {
+ sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra IN (' + dbMergeSqlArray(meshes) + ')) OR (id IN (' + dbMergeSqlArray(extrasids) + '))) LIMIT $3 OFFSET $4', [type, domain, limit, skip], function (err, docs) {
+ if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ }
+ }
+ };
+ obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
+ if (id && (id != '')) {
+ sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra IN (' + dbMergeSqlArray(meshes) + '))', [id, type, domain], function (err, docs) {
+ func(err, (err == null) ? docs[0]['COUNT(doc)'] : null);
+ });
+ } else {
+ if (extrasids == null) {
+ sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (type = $1) AND (domain = $2) AND (extra IN (' + dbMergeSqlArray(meshes) + '))', [type, domain], function (err, docs) {
+ func(err, (err == null) ? docs[0]['COUNT(doc)'] : null);
+ });
+ } else {
+ sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (type = $1) AND (domain = $2) AND ((extra IN (' + dbMergeSqlArray(meshes) + ')) OR (id IN (' + dbMergeSqlArray(extrasids) + ')))', [type, domain], function (err, docs) {
+ func(err, (err == null) ? docs[0]['COUNT(doc)'] : null);
+ });
+ }
+ }
+ };
+ obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
+ if (id && (id != '')) {
+ sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra IN (' + dbMergeSqlArray(nodes) + '))', [id, type, domain], function (err, docs) {
+ if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ } else {
+ sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra IN (' + dbMergeSqlArray(nodes) + '))', [type, domain], function (err, docs) {
+ if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ }
+ };
+ obj.GetAllType = function (type, func) {
+ sqlDbQuery('SELECT doc FROM main WHERE type = $1', [type], function (err, docs) {
+ if (docs != null) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ }
+ obj.GetAllIdsOfType = function (ids, domain, type, func) {
+ sqlDbQuery('SELECT doc FROM main WHERE (id IN (' + dbMergeSqlArray(ids) + ')) AND domain = $1 AND type = $2', [domain, type], function (err, docs) {
+ if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ }
+ obj.GetUserWithEmail = function (domain, email, func) {
+ sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extra = $2', [domain, 'email/' + email], function (err, docs) {
+ if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ }
+ obj.GetUserWithVerifiedEmail = function (domain, email, func) {
+ sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extra = $2', [domain, 'email/' + email], function (err, docs) {
+ if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ }
+ obj.GetNodeByComputerName = function (domain, rname, func) {
+ sqlDbQuery('SELECT doc FROM main WHERE type = $1 AND domain = $2 AND JSON_EXTRACT(doc, "$.rname") = $3 ORDER BY lastbootuptime',
+ ['node', domain, rname], function (err, docs) {
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ };
+ obj.Remove = function (id, func) { sqlDbQuery('DELETE FROM main WHERE id = $1', [id], func); };
+ obj.RemoveAll = function (func) { sqlDbQuery('DELETE FROM main', null, func); };
+ obj.RemoveAllOfType = function (type, func) { sqlDbQuery('DELETE FROM main WHERE type = $1', [type], func); };
+ obj.InsertMany = function (data, func) { var pendingOps = 0; for (var i in data) { pendingOps++; obj.SetRaw(data[i], function () { if (--pendingOps == 0) { func(); } }); } }; // Insert records directly, no link escaping
+ obj.RemoveMeshDocuments = function (id, func) { sqlDbQuery('DELETE FROM main WHERE extra = $1', [id], function () { sqlDbQuery('DELETE FROM main WHERE id = $1', ['nt' + id], func); }); };
+ obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
+ obj.DeleteDomain = function (domain, func) { sqlDbQuery('DELETE FROM main WHERE domain = $1', [domain], func); };
+ obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
+ obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
+ obj.getLocalAmtNodes = function (func) {
+ sqlDbQuery('SELECT doc FROM main WHERE (type = \'node\') AND (extraex IS NULL)', null, function (err, docs) {
+ if (docs != null) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ var r = []; if (err == null) { for (var i in docs) { if (docs[i].host != null && docs[i].intelamt != null) { r.push(docs[i]); } } } func(err, r);
+ });
+ };
+ obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) {
+ sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extraex = $2', [domainid, 'uuid/' + uuid], function (err, docs) {
+ if (docs != null) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
+ func(err, docs);
+ });
+ };
+ obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { sqlDbExec('SELECT COUNT(id) FROM main WHERE domain = $1 AND type = $2', [domainid, type], function (err, response) { func((response['COUNT(id)'] == null) || (response['COUNT(id)'] > max), response['COUNT(id)']) }); } }
+
+ // Database actions on the events collection
+ obj.GetAllEvents = function (func) {
+ sqlDbQuery('SELECT doc FROM events', null, func);
+ };
+ obj.StoreEvent = function (event, func) {
+ obj.dbCounters.eventsSet++;
+ sqlDbQuery('INSERT INTO events VALUES (NULL, $1, $2, $3, $4, $5, $6) RETURNING id', [event.time, ((typeof event.domain == 'string') ? event.domain : null), event.action, event.nodeid ? event.nodeid : null, event.userid ? event.userid : null, JSON.stringify(event)], function (err, docs) {
+ if(func){ func(); }
+ if ((err == null) && (docs[0].id)) {
+ for (var i in event.ids) {
+ if (event.ids[i] != '*') {
+ obj.pendingTransfer++;
+ sqlDbQuery('INSERT INTO eventids VALUES ($1, $2)', [docs[0].id, event.ids[i]], function(){ if(func){ func(); } });
+ }
+ }
+ }
+ });
+ };
+ obj.GetEvents = function (ids, domain, filter, func) {
+ var query = "SELECT doc FROM events ";
+ var dataarray = [domain];
+ if (ids.indexOf('*') >= 0) {
+ query = query + "WHERE (domain = $1";
+ if (filter != null) {
+ query = query + " AND action = $2";
+ dataarray.push(filter);
+ }
+ query = query + ") ORDER BY time DESC";
+ } else {
+ query = query + 'JOIN eventids ON id = fkid WHERE (domain = $1 AND (target IN (' + dbMergeSqlArray(ids) + '))';
+ if (filter != null) {
+ query = query + " AND action = $2";
+ dataarray.push(filter);
+ }
+ query = query + ") GROUP BY id ORDER BY time DESC ";
+ }
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
+ var query = "SELECT doc FROM events ";
+ var dataarray = [domain];
+ if (ids.indexOf('*') >= 0) {
+ query = query + "WHERE (domain = $1";
+ if (filter != null) {
+ query = query + " AND action = $2) ORDER BY time DESC LIMIT $3";
+ dataarray.push(filter);
+ } else {
+ query = query + ") ORDER BY time DESC LIMIT $2";
+ }
+ } else {
+ query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND (target IN (" + dbMergeSqlArray(ids) + "))";
+ if (filter != null) {
+ query = query + " AND action = $2) GROUP BY id ORDER BY time DESC LIMIT $3";
+ dataarray.push(filter);
+ } else {
+ query = query + ") GROUP BY id ORDER BY time DESC LIMIT $2";
+ }
+ }
+ dataarray.push(limit);
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetUserEvents = function (ids, domain, userid, filter, func) {
+ var query = "SELECT doc FROM events ";
+ var dataarray = [domain, userid];
+ if (ids.indexOf('*') >= 0) {
+ query = query + "WHERE (domain = $1 AND userid = $2";
+ if (filter != null) {
+ query = query + " AND action = $3";
+ dataarray.push(filter);
+ }
+ query = query + ") ORDER BY time DESC";
+ } else {
+ query = query + 'JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target IN (' + dbMergeSqlArray(ids) + '))';
+ if (filter != null) {
+ query = query + " AND action = $3";
+ dataarray.push(filter);
+ }
+ query = query + ") GROUP BY id ORDER BY time DESC";
+ }
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
+ var query = "SELECT doc FROM events ";
+ var dataarray = [domain, userid];
+ if (ids.indexOf('*') >= 0) {
+ query = query + "WHERE (domain = $1 AND userid = $2";
+ if (filter != null) {
+ query = query + " AND action = $3) ORDER BY time DESC LIMIT $4";
+ dataarray.push(filter);
+ } else {
+ query = query + ") ORDER BY time DESC LIMIT $3";
+ }
+ } else {
+ query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target IN (" + dbMergeSqlArray(ids) + "))";
+ if (filter != null) {
+ query = query + " AND action = $3) GROUP BY id ORDER BY time DESC LIMIT $4";
+ dataarray.push(filter);
+ } else {
+ query = query + ") GROUP BY id ORDER BY time DESC LIMIT $3";
+ }
+ }
+ dataarray.push(limit);
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
+ if (ids.indexOf('*') >= 0) {
+ sqlDbQuery('SELECT doc FROM events WHERE ((domain = $1) AND (time BETWEEN $2 AND $3)) ORDER BY time', [domain, start, end], func);
+ } else {
+ sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE ((domain = $1) AND (target IN (' + dbMergeSqlArray(ids) + ')) AND (time BETWEEN $2 AND $3)) GROUP BY id ORDER BY time', [domain, start, end], func);
+ }
+ };
+ //obj.GetUserLoginEvents = function (domain, userid, func) { } // TODO
+ obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
+ var query = "SELECT doc FROM events WHERE (nodeid = $1 AND domain = $2";
+ var dataarray = [nodeid, domain];
+ if (filter != null) {
+ query = query + " AND action = $3) ORDER BY time DESC LIMIT $4";
+ dataarray.push(filter);
+ } else {
+ query = query + ") ORDER BY time DESC LIMIT $3";
+ }
+ dataarray.push(limit);
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
+ var query = "SELECT doc FROM events WHERE (nodeid = $1) AND (domain = $2) AND ((userid = $3) OR (userid IS NULL)) ";
+ var dataarray = [nodeid, domain, userid];
+ if (filter != null) {
+ query = query + "AND (action = $4) ORDER BY time DESC LIMIT $5";
+ dataarray.push(filter);
+ } else {
+ query = query + "ORDER BY time DESC LIMIT $4";
+ }
+ dataarray.push(limit);
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.RemoveAllEvents = function (domain) { sqlDbQuery('DELETE FROM events', null, function (err, docs) { }); };
+ obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = $1 AND nodeid = $2', [domain, nodeid], function (err, docs) { }); };
+ obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = $1 AND userid = $2', [domain, userid], function (err, docs) { }); };
+ obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) { sqlDbQuery('SELECT COUNT(*) FROM events WHERE action = \'authfail\' AND domain = $1 AND userid = $2 AND time > $3', [domainid, userid, lastlogin], function (err, response) { func(err == null ? response[0]['COUNT(*)'] : 0); }); }
+
+ // Database actions on the power collection
+ obj.getAllPower = function (func) { sqlDbQuery('SELECT doc FROM power', null, func); };
+ obj.storePowerEvent = function (event, multiServer, func) { obj.dbCounters.powerSet++; if (multiServer != null) { event.server = multiServer.serverid; } sqlDbQuery('INSERT INTO power VALUES (NULL, $1, $2, $3)', [event.time, event.nodeid ? event.nodeid : null, JSON.stringify(event)], func); };
+ obj.getPowerTimeline = function (nodeid, func) { sqlDbQuery('SELECT doc FROM power WHERE ((nodeid = $1) OR (nodeid = \'*\')) ORDER BY time ASC', [nodeid], func); };
+ obj.removeAllPowerEvents = function () { sqlDbQuery('DELETE FROM power', null, function (err, docs) { }); };
+ obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; sqlDbQuery('DELETE FROM power WHERE nodeid = $1', [nodeid], function (err, docs) { }); };
+
+ // Database actions on the SMBIOS collection
+ obj.GetAllSMBIOS = function (func) { sqlDbQuery('SELECT doc FROM smbios', null, func); };
+ obj.SetSMBIOS = function (smbios, func) { var expire = new Date(smbios.time); expire.setMonth(expire.getMonth() + 6); sqlDbQuery('INSERT INTO smbios VALUES ($1, $2, $3, $4) ON CONFLICT (id) DO UPDATE SET time = $2, expire = $3, doc = $4', [smbios._id, smbios.time, expire, JSON.stringify(smbios)], func); };
+ obj.RemoveSMBIOS = function (id) { sqlDbQuery('DELETE FROM smbios WHERE id = $1', [id], function (err, docs) { }); };
+ obj.GetSMBIOS = function (id, func) { sqlDbQuery('SELECT doc FROM smbios WHERE id = $1', [id], func); };
+
+ // Database actions on the Server Stats collection
+ obj.SetServerStats = function (data, func) { sqlDbQuery('INSERT INTO serverstats VALUES ($1, $2, $3) ON CONFLICT (time) DO UPDATE SET expire = $2, doc = $3', [data.time, data.expire, JSON.stringify(data)], func); };
+ obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); sqlDbQuery('SELECT doc FROM serverstats WHERE time > $1', [t], func); }; // TODO: Expire old entries
+
+ // Read a configuration file from the database
+ obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
+
+ // Write a configuration file to the database
+ obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
+
+ // List all configuration files
+ obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM main WHERE type = "cfile" ORDER BY id', func); }
+
+ // Get database information (TODO: Complete this)
+ obj.getDbStats = function (func) {
+ obj.stats = { c: 4 };
+ sqlDbQuery('SELECT COUNT(*) FROM main', null, function (err, response) { obj.stats.meshcentral = (err == null ? response[0]['COUNT(*)'] : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ sqlDbQuery('SELECT COUNT(*) FROM serverstats', null, function (err, response) { obj.stats.serverstats = (err == null ? response[0]['COUNT(*)'] : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ sqlDbQuery('SELECT COUNT(*) FROM power', null, function (err, response) { obj.stats.power = (err == null ? response[0]['COUNT(*)'] : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ sqlDbQuery('SELECT COUNT(*) FROM smbios', null, function (err, response) { obj.stats.smbios = (err == null ? response[0]['COUNT(*)'] : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ }
+
+ // Plugin operations
+ if (obj.pluginsActive) {
+ obj.addPlugin = function (plugin, func) { sqlDbQuery('INSERT INTO plugin VALUES (NULL, $1)', [JSON.stringify(plugin)], func); }; // Add a plugin
+ obj.getPlugins = function (func) { sqlDbQuery('SELECT JSON_INSERT(doc, "$._id", id) as doc FROM plugin', null, func); }; // Get all plugins
+ obj.getPlugin = function (id, func) { sqlDbQuery('SELECT JSON_INSERT(doc, "$._id", id) as doc FROM plugin WHERE id = $1', [id], func); }; // Get plugin
+ obj.deletePlugin = function (id, func) { sqlDbQuery('DELETE FROM plugin WHERE id = $1', [id], func); }; // Delete plugin
+ obj.setPluginStatus = function (id, status, func) { sqlDbQuery('UPDATE plugin SET doc=JSON_SET(doc,"$.status",$1) WHERE id=$2', [status,id], func); };
+ obj.updatePlugin = function (id, args, func) { delete args._id; sqlDbQuery('UPDATE plugin SET doc=json_patch(doc,$1) WHERE id=$2', [JSON.stringify(args),id], func); };
+ }
+ } else if (obj.databaseType == DB_ACEBASE) {
+ // Database actions on the main collection. AceBase: https://github.com/appy-one/acebase
+ obj.Set = function (data, func) {
+ data = common.escapeLinksFieldNameEx(data);
+ var xdata = performTypedRecordEncrypt(data);
+ obj.dbCounters.fileSet++;
+ obj.file.ref('meshcentral').child(encodeURIComponent(xdata._id)).set(common.aceEscapeFieldNames(xdata)).then(function (ref) { if (func) { func(); } })
+ };
+ obj.Get = function (id, func) {
+ obj.file.ref('meshcentral').child(encodeURIComponent(id)).get(function (snapshot) {
+ if (snapshot.exists()) { func(null, performTypedRecordDecrypt([common.aceUnEscapeFieldNames(snapshot.val())])); } else { func(null, []); }
+ });
+ };
+ obj.GetAll = function (func) {
+ obj.file.ref('meshcentral').get(function(snapshot) {
+ const val = snapshot.val();
+ const docs = Object.keys(val).map(function(key) { return val[key]; });
+ func(null, common.aceUnEscapeAllFieldNames(docs));
+ });
+ };
+ obj.GetHash = function (id, func) {
+ obj.file.ref('meshcentral').child(encodeURIComponent(id)).get({ include: ['hash'] }, function (snapshot) {
+ if (snapshot.exists()) { func(null, snapshot.val()); } else { func(null, null); }
+ });
+ };
+ obj.GetAllTypeNoTypeField = function (type, domain, func) {
+ obj.file.query('meshcentral').filter('type', '==', type).filter('domain', '==', domain).get({ exclude: ['type'] }, function (snapshots) {
+ const docs = [];
+ for (var i in snapshots) { const x = snapshots[i].val(); docs.push(x); }
+ func(null, common.aceUnEscapeAllFieldNames(docs));
+ });
+ }
+ obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
+ if (meshes.length == 0) { func(null, []); return; }
+ var query = obj.file.query('meshcentral').skip(skip).take(limit).filter('type', '==', type).filter('domain', '==', domain);
+ if (id) { query = query.filter('_id', '==', id); }
+ if (extrasids == null) {
+ query = query.filter('meshid', 'in', meshes);
+ query.get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); });
+ } else {
+ // TODO: This is a slow query as we did not find a filter-or-filter, so we query everything and filter manualy.
+ query.get(function (snapshots) {
+ const docs = [];
+ for (var i in snapshots) { const x = snapshots[i].val(); if ((extrasids.indexOf(x._id) >= 0) || (meshes.indexOf(x.meshid) >= 0)) { docs.push(x); } }
+ func(null, performTypedRecordDecrypt(docs));
+ });
+ }
+ };
+ obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
+ var query = obj.file.query('meshcentral').filter('type', '==', type).filter('domain', '==', domain).filter('nodeid', 'in', nodes);
+ if (id) { query = query.filter('_id', '==', id); }
+ query.get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); });
+ };
+ obj.GetAllType = function (type, func) {
+ obj.file.query('meshcentral').filter('type', '==', type).get(function (snapshots) {
+ const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); }
+ func(null, common.aceUnEscapeAllFieldNames(performTypedRecordDecrypt(docs)));
+ });
+ };
+ obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.query('meshcentral').filter('_id', 'in', ids).filter('domain', '==', domain).filter('type', '==', type).get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
+ obj.GetUserWithEmail = function (domain, email, func) { obj.file.query('meshcentral').filter('type', '==', 'user').filter('domain', '==', domain).filter('email', '==', email).get({ exclude: ['type'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
+ obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.query('meshcentral').filter('type', '==', 'user').filter('domain', '==', domain).filter('email', '==', email).filter('emailVerified', '==', true).get({ exclude: ['type'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
+ obj.GetNodeByComputerName = function (domain, rname, func) { obj.file.query('meshcentral').filter('type', '==', 'node').filter('domain', '==', domain).filter('rname', '==', rname).get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
+ obj.Remove = function (id, func) { obj.file.ref('meshcentral').child(encodeURIComponent(id)).remove().then(function () { if (func) { func(); } }); };
+ obj.RemoveAll = function (func) { obj.file.query('meshcentral').remove().then(function () { if (func) { func(); } }); };
+ obj.RemoveAllOfType = function (type, func) { obj.file.query('meshcentral').filter('type', '==', type).remove().then(function () { if (func) { func(); } }); };
+ obj.InsertMany = function (data, func) { var r = {}; for (var i in data) { const ref = obj.file.ref('meshcentral').child(encodeURIComponent(data[i]._id)); r[ref.key] = common.aceEscapeFieldNames(data[i]); } obj.file.ref('meshcentral').set(r).then(function (ref) { func(); }); }; // Insert records directly, no link escaping
+ obj.RemoveMeshDocuments = function (id) { obj.file.query('meshcentral').filter('meshid', '==', id).remove(); obj.file.ref('meshcentral').child(encodeURIComponent('nt' + id)).remove(); };
+ obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
+ obj.DeleteDomain = function (domain, func) { obj.file.query('meshcentral').filter('domain', '==', domain).remove().then(function () { if (func) { func(); } }); };
+ obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
+ obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
+ obj.getLocalAmtNodes = function (func) { obj.file.query('meshcentral').filter('type', '==', 'node').filter('host', 'exists').filter('host', '!=', null).filter('intelamt', 'exists').get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
+ obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { obj.file.query('meshcentral').filter('type', '==', 'node').filter('domain', '==', domainid).filter('mtype', '!=', mtype).filter('intelamt.uuid', '==', uuid).get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); }); };
+ obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { obj.file.query('meshcentral').filter('type', '==', type).filter('domain', '==', domainid).get({ snapshots: false }, function (snapshots) { func((snapshots.length > max), snapshots.length); }); } }
+
+ // Database actions on the events collection
+ obj.GetAllEvents = function (func) {
+ obj.file.ref('events').get(function (snapshot) {
+ const val = snapshot.val();
+ const docs = Object.keys(val).map(function(key) { return val[key]; });
+ func(null, docs);
+ })
+ };
+ obj.StoreEvent = function (event, func) {
+ if (typeof event.account == 'object') { event = Object.assign({}, event); event.account = common.aceEscapeFieldNames(event.account); }
+ obj.dbCounters.eventsSet++;
+ obj.file.ref('events').push(event).then(function (userRef) { if (func) { func(); } });
+ };
+ obj.GetEvents = function (ids, domain, filter, func) {
+ // This request is slow since we have not found a .filter() that will take two arrays and match a single item.
+ if (filter != null) {
+ obj.file.query('events').filter('domain', '==', domain).filter('action', '==', filter).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) {
+ const docs = [];
+ for (var i in snapshots) {
+ const doc = snapshots[i].val();
+ if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue;
+ var found = false;
+ for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches
+ if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); }
+ }
+ func(null, docs);
+ });
+ } else {
+ obj.file.query('events').filter('domain', '==', domain).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) {
+ const docs = [];
+ for (var i in snapshots) {
+ const doc = snapshots[i].val();
+ if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue;
+ var found = false;
+ for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches
+ if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); }
+ }
+ func(null, docs);
+ });
+ }
+ };
+ obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
+ // This request is slow since we have not found a .filter() that will take two arrays and match a single item.
+ // TODO: Request a new AceBase feature for a 'array:contains-one-of' filter:
+ // obj.file.indexes.create('events', 'ids', { type: 'array' });
+ // db.query('events').filter('ids', 'array:contains-one-of', ids)
+ if (filter != null) {
+ obj.file.query('events').filter('domain', '==', domain).filter('action', '==', filter).take(limit).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) {
+ const docs = [];
+ for (var i in snapshots) {
+ const doc = snapshots[i].val();
+ if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue;
+ var found = false;
+ for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches
+ if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); }
+ }
+ func(null, docs);
+ });
+ } else {
+ obj.file.query('events').filter('domain', '==', domain).take(limit).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) {
+ const docs = [];
+ for (var i in snapshots) {
+ const doc = snapshots[i].val();
+ if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue;
+ var found = false;
+ for (var j in doc.ids) { if (ids.indexOf(doc.ids[j]) >= 0) { found = true; } } // Check if one of the items in both arrays matches
+ if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); }
+ }
+ func(null, docs);
+ });
+ }
+ };
+ obj.GetUserEvents = function (ids, domain, userid, filter, func) {
+ if (filter != null) {
+ obj.file.query('events').filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).filter('action', '==', filter).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
+ } else {
+ obj.file.query('events').filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
+ }
+ };
+ obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
+ if (filter != null) {
+ obj.file.query('events').take(limit).filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).filter('action', '==', filter).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
+ } else {
+ obj.file.query('events').take(limit).filter('domain', '==', domain).filter('userid', 'in', userid).filter('ids', 'in', ids).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type', 'ids'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
+ }
+ };
+ obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
+ obj.file.query('events').filter('domain', '==', domain).filter('ids', 'in', ids).filter('msgid', 'in', msgids).filter('time', 'between', [start, end]).sort('time', false).get({ exclude: ['type', '_id', 'domain', 'node'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
+ };
+ obj.GetUserLoginEvents = function (domain, userid, func) {
+ obj.file.query('events').filter('domain', '==', domain).filter('action', 'in', ['authfail', 'login']).filter('userid', '==', userid).filter('msgArgs', 'exists').sort('time', false).get({ include: ['action', 'time', 'msgid', 'msgArgs', 'tokenName'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
+ };
+ obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
+ if (filter != null) {
+ obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).filter('action', '==', filter).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
+ } else {
+ obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
+ }
+ };
+ obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
+ if (filter != null) {
+ obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).filter('userid', '==', userid).filter('action', '==', filter).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
+ } else {
+ obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).filter('userid', '==', userid).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
+ }
+ obj.file.query('events').take(limit).filter('domain', '==', domain).filter('nodeid', '==', nodeid).filter('userid', '==', userid).sort('time', false).get({ exclude: ['type', 'etype', '_id', 'domain', 'ids', 'node', 'nodeid'] }, function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
+ };
+ obj.RemoveAllEvents = function (domain) {
+ obj.file.query('events').filter('domain', '==', domain).remove().then(function () { if (func) { func(); } });;
+ };
+ obj.RemoveAllNodeEvents = function (domain, nodeid) {
+ if ((domain == null) || (nodeid == null)) return;
+ obj.file.query('events').filter('domain', '==', domain).filter('nodeid', '==', nodeid).remove().then(function () { if (func) { func(); } });;
+ };
+ obj.RemoveAllUserEvents = function (domain, userid) {
+ if ((domain == null) || (userid == null)) return;
+ obj.file.query('events').filter('domain', '==', domain).filter('userid', '==', userid).remove().then(function () { if (func) { func(); } });;
+ };
+ obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) {
+ obj.file.query('events').filter('domain', '==', domainid).filter('userid', '==', userid).filter('time', '>', lastlogin).sort('time', false).get({ snapshots: false }, function (snapshots) { func(null, snapshots.length); });
+ }
+
+ // Database actions on the power collection
+ obj.getAllPower = function (func) {
+ obj.file.ref('power').get(function (snapshot) {
+ const val = snapshot.val();
+ const docs = Object.keys(val).map(function(key) { return val[key]; });
+ func(null, docs);
+ });
+ };
+ obj.storePowerEvent = function (event, multiServer, func) {
+ if (multiServer != null) { event.server = multiServer.serverid; }
+ obj.file.ref('power').push(event).then(function (userRef) { if (func) { func(); } });
+ };
+ obj.getPowerTimeline = function (nodeid, func) {
+ obj.file.query('power').filter('nodeid', 'in', ['*', nodeid]).sort('time').get({ exclude: ['_id', 'nodeid', 's'] }, function (snapshots) {
+ const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs);
+ });
+ };
+ obj.removeAllPowerEvents = function () {
+ obj.file.ref('power').remove().then(function () { if (func) { func(); } });
+ };
+ obj.removeAllPowerEventsForNode = function (nodeid) {
+ if (nodeid == null) return;
+ obj.file.query('power').filter('nodeid', '==', nodeid).remove().then(function () { if (func) { func(); } });
+ };
+
+ // Database actions on the SMBIOS collection
+ if (obj.smbiosfile != null) {
+ obj.GetAllSMBIOS = function (func) {
+ obj.file.ref('smbios').get(function (snapshot) {
+ const val = snapshot.val();
+ const docs = Object.keys(val).map(function(key) { return val[key]; });
+ func(null, docs);
+ });
+ };
+ obj.SetSMBIOS = function (smbios, func) {
+ obj.file.ref('meshcentral/' + encodeURIComponent(smbios._id)).set(smbios).then(function (ref) { if (func) { func(); } })
+ };
+ obj.RemoveSMBIOS = function (id) {
+ obj.file.query('smbios').filter('_id', '==', id).remove().then(function () { if (func) { func(); } });
+ };
+ obj.GetSMBIOS = function (id, func) {
+ obj.file.query('smbios').filter('_id', '==', id).get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); });
+ };
+ }
+
+ // Database actions on the Server Stats collection
+ obj.SetServerStats = function (data, func) {
+ obj.file.ref('stats').push(data).then(function (userRef) { if (func) { func(); } });
+ };
+ obj.GetServerStats = function (hours, func) {
+ var t = new Date();
+ t.setTime(t.getTime() - (60 * 60 * 1000 * hours));
+ obj.file.query('stats').filter('time', '>', t).get({ exclude: ['_id', 'cpu'] }, function (snapshots) {
+ const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs);
+ });
+ };
+
+ // Read a configuration file from the database
+ obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
+
+ // Write a configuration file to the database
+ obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
+
+ // List all configuration files
+ obj.listConfigFiles = function (func) {
+ obj.file.query('meshcentral').filter('type', '==', 'cfile').sort('_id').get(function (snapshots) {
+ const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs);
+ });
+ }
+
+ // Get database information
+ obj.getDbStats = function (func) {
+ obj.stats = { c: 5 };
+ obj.file.ref('meshcentral').count().then(function (count) { obj.stats.meshcentral = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ obj.file.ref('events').count().then(function (count) { obj.stats.events = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ obj.file.ref('power').count().then(function (count) { obj.stats.power = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ obj.file.ref('smbios').count().then(function (count) { obj.stats.smbios = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ obj.file.ref('stats').count().then(function (count) { obj.stats.serverstats = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ }
+
+ // Plugin operations
+ if (obj.pluginsActive) {
+ obj.addPlugin = function (plugin, func) { plugin.type = 'plugin'; obj.file.ref('plugin').child(encodeURIComponent(plugin._id)).set(plugin).then(function (ref) { if (func) { func(); } }) }; // Add a plugin
+ obj.getPlugins = function (func) {
+ obj.file.ref('plugin').get({ exclude: ['type'] }, function (snapshot) {
+ const val = snapshot.val();
+ const docs = Object.keys(val).map(function(key) { return val[key]; }).sort(function(a, b) { return a.name < b.name ? -1 : 1 });
+ func(null, docs);
+ });
+ }; // Get all plugins
+ obj.getPlugin = function (id, func) { obj.file.query('plugin').filter('_id', '==', id).get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs); }); }; // Get plugin
+ obj.deletePlugin = function (id, func) { obj.file.ref('plugin').child(encodeURIComponent(id)).remove().then(function () { if (func) { func(); } }); }; // Delete plugin
+ obj.setPluginStatus = function (id, status, func) { obj.file.ref('plugin').child(encodeURIComponent(id)).update({ status: status }).then(function (ref) { if (func) { func(); } }) };
+ obj.updatePlugin = function (id, args, func) { delete args._id; obj.file.ref('plugin').child(encodeURIComponent(id)).set(args).then(function (ref) { if (func) { func(); } }) };
+ }
+ } else if (obj.databaseType == DB_POSTGRESQL) {
+ // Database actions on the main collection (Postgres)
+ obj.Set = function (value, func) {
+ obj.dbCounters.fileSet++;
+ var extra = null, extraex = null;
+ value = common.escapeLinksFieldNameEx(value);
+ if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
+ if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
+ if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
+ sqlDbQuery('INSERT INTO main VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET type = $2, domain = $3, extra = $4, extraex = $5, doc = $6;', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, performTypedRecordEncrypt(value)], func);
+ }
+ obj.SetRaw = function (value, func) {
+ obj.dbCounters.fileSet++;
+ var extra = null, extraex = null;
+ if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
+ if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
+ if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
+ sqlDbQuery('INSERT INTO main VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET type = $2, domain = $3, extra = $4, extraex = $5, doc = $6;', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, performTypedRecordEncrypt(value)], func);
+ }
+ obj.Get = function (_id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = $1', [_id], function (err, docs) { if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); } func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetAll = function (func) { sqlDbQuery('SELECT domain, doc FROM main', null, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetHash = function (id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = $1', [id], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetAllTypeNoTypeField = function (type, domain, func) { sqlDbQuery('SELECT doc FROM main WHERE type = $1 AND domain = $2', [type, domain], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
+ if (limit == 0) { limit = 0xFFFFFFFF; }
+ if (id && (id != '')) {
+ sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra = ANY ($4)) LIMIT $5 OFFSET $6', [id, type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
+ } else {
+ if (extrasids == null) {
+ sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3)) LIMIT $4 OFFSET $5', [type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }, true);
+ } else {
+ sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra = ANY ($3)) OR (id = ANY ($4))) LIMIT $5 OFFSET $6', [type, domain, meshes, extrasids, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
+ }
+ }
+ };
+ obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
+ if (id && (id != '')) {
+ sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra = ANY ($4))', [id, type, domain, meshes], function (err, docs) { func(err, docs); });
+ } else {
+ if (extrasids == null) {
+ sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3))', [type, domain, meshes], function (err, docs) { func(err, docs); }, true);
+ } else {
+ sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (type = $1) AND (domain = $2) AND ((extra = ANY ($3)) OR (id = ANY ($4)))', [type, domain, meshes, extrasids], function (err, docs) { func(err, docs); });
+ }
+ }
+ };
+ obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
+ if (id && (id != '')) {
+ sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra = ANY ($4))', [id, type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
+ } else {
+ sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3))', [type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
+ }
+ };
+ obj.GetAllType = function (type, func) { sqlDbQuery('SELECT doc FROM main WHERE type = $1', [type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetAllIdsOfType = function (ids, domain, type, func) { sqlDbQuery('SELECT doc FROM main WHERE (id = ANY ($1)) AND domain = $2 AND type = $3', [ids, domain, type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetUserWithEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extra = $2', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetUserWithVerifiedEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extra = $2', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetNodeByComputerName = function (domain, rname, func) { sqlDbQuery('SELECT doc FROM main WHERE type = "$1" AND domain = $2 AND doc->>\'rname\' = $3 ORDER BY lastbootuptime', ['node', domain, rname], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });};
+ obj.Remove = function (id, func) { sqlDbQuery('DELETE FROM main WHERE id = $1', [id], func); };
+ obj.RemoveAll = function (func) { sqlDbQuery('DELETE FROM main', null, func); };
+ obj.RemoveAllOfType = function (type, func) { sqlDbQuery('DELETE FROM main WHERE type = $1', [type], func); };
+ obj.InsertMany = function (data, func) { var pendingOps = 0; for (var i in data) { pendingOps++; obj.SetRaw(data[i], function () { if (--pendingOps == 0) { func(); } }); } }; // Insert records directly, no link escaping
+ obj.RemoveMeshDocuments = function (id, func) { sqlDbQuery('DELETE FROM main WHERE extra = $1', [id], function () { sqlDbQuery('DELETE FROM main WHERE id = $1', ['nt' + id], func); }); };
+ obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
+ obj.DeleteDomain = function (domain, func) { sqlDbQuery('DELETE FROM main WHERE domain = $1', [domain], func); };
+ obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
+ obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
+ obj.getLocalAmtNodes = function (func) { sqlDbQuery('SELECT doc FROM main WHERE (type = \'node\') AND (extraex IS NULL)', null, function (err, docs) { var r = []; if (err == null) { for (var i in docs) { if (docs[i].host != null && docs[i].intelamt != null) { r.push(docs[i]); } } } func(err, r); }); };
+ obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extraex = $2', [domainid, 'uuid/' + uuid], func); };
+ obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { sqlDbExec('SELECT COUNT(id) FROM main WHERE domain = $1 AND type = $2', [domainid, type], function (err, response) { func((response['COUNT(id)'] == null) || (response['COUNT(id)'] > max), response['COUNT(id)']) }); } }
+
+ // Database actions on the events collection
+ obj.GetAllEvents = function (func) { sqlDbQuery('SELECT doc FROM events', null, func); };
+ obj.StoreEvent = function (event, func) {
+ obj.dbCounters.eventsSet++;
+ sqlDbQuery('INSERT INTO events VALUES (DEFAULT, $1, $2, $3, $4, $5, $6) RETURNING id', [event.time, ((typeof event.domain == 'string') ? event.domain : null), event.action, event.nodeid ? event.nodeid : null, event.userid ? event.userid : null, event], function (err, docs) {
+ if(func){ func(); }
+ if (docs.id) {
+ for (var i in event.ids) {
+ if (event.ids[i] != '*') {
+ obj.pendingTransfer++;
+ sqlDbQuery('INSERT INTO eventids VALUES ($1, $2)', [docs.id, event.ids[i]], function(){ if(func){ func(); } });
+ }
+ }
+ }
+ });
+ };
+ obj.GetEvents = function (ids, domain, filter, func) {
+ var query = "SELECT doc FROM events ";
+ var dataarray = [domain];
+ if (ids.indexOf('*') >= 0) {
+ query = query + "WHERE (domain = $1";
+ if (filter != null) {
+ query = query + " AND action = $2";
+ dataarray.push(filter);
+ }
+ query = query + ") ORDER BY time DESC";
+ } else {
+ query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND (target = ANY ($2))";
+ dataarray.push(ids);
+ if (filter != null) {
+ query = query + " AND action = $3";
+ dataarray.push(filter);
+ }
+ query = query + ") GROUP BY id ORDER BY time DESC";
+ }
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
+ var query = "SELECT doc FROM events ";
+ var dataarray = [domain];
+ if (ids.indexOf('*') >= 0) {
+ query = query + "WHERE (domain = $1";
+ if (filter != null) {
+ query = query + " AND action = $2) ORDER BY time DESC LIMIT $3";
+ dataarray.push(filter);
+ } else {
+ query = query + ") ORDER BY time DESC LIMIT $2";
+ }
+ } else {
+ if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND (target = ANY ($2))";
+ dataarray.push(ids);
+ if (filter != null) {
+ query = query + " AND action = $3) ORDER BY time DESC LIMIT $4";
+ dataarray.push(filter);
+ } else {
+ query = query + ") ORDER BY time DESC LIMIT $3";
+ }
+ }
+ dataarray.push(limit);
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetUserEvents = function (ids, domain, userid, filter, func) {
+ var query = "SELECT doc FROM events ";
+ var dataarray = [domain, userid];
+ if (ids.indexOf('*') >= 0) {
+ query = query + "WHERE (domain = $1 AND userid = $2";
+ if (filter != null) {
+ query = query + " AND action = $3";
+ dataarray.push(filter);
+ }
+ query = query + ") ORDER BY time DESC";
+ } else {
+ if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target = ANY ($3))";
+ dataarray.push(ids);
+ if (filter != null) {
+ query = query + " AND action = $4";
+ dataarray.push(filter);
+ }
+ query = query + ") GROUP BY id ORDER BY time DESC";
+ }
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
+ var query = "SELECT doc FROM events ";
+ var dataarray = [domain, userid];
+ if (ids.indexOf('*') >= 0) {
+ query = query + "WHERE (domain = $1 AND userid = $2";
+ if (filter != null) {
+ query = query + " AND action = $3) ORDER BY time DESC LIMIT $4 ";
+ dataarray.push(filter);
+ } else {
+ query = query + ") ORDER BY time DESC LIMIT $3";
+ }
+ } else {
+ if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target = ANY ($3))";
+ dataarray.push(ids);
+ if (filter != null) {
+ query = query + " AND action = $4) GROUP BY id ORDER BY time DESC LIMIT $5";
+ dataarray.push(filter);
+ } else {
+ query = query + ") GROUP BY id ORDER BY time DESC LIMIT $4";
+ }
+ }
+ dataarray.push(limit);
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
+ if (ids.indexOf('*') >= 0) {
+ sqlDbQuery('SELECT doc FROM events WHERE ((domain = $1) AND (time BETWEEN $2 AND $3)) ORDER BY time', [domain, start, end], func);
+ } else {
+ sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE ((domain = $1) AND (target = ANY ($2)) AND (time BETWEEN $3 AND $4)) GROUP BY id ORDER BY time', [domain, ids, start, end], func);
+ }
+ };
+ //obj.GetUserLoginEvents = function (domain, userid, func) { } // TODO
+ obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
+ var query = "SELECT doc FROM events WHERE (nodeid = $1 AND domain = $2";
+ var dataarray = [nodeid, domain];
+ if (filter != null) {
+ query = query + " AND action = $3) ORDER BY time DESC LIMIT $4";
+ dataarray.push(filter);
+ } else {
+ query = query + ") ORDER BY time DESC LIMIT $3";
+ }
+ dataarray.push(limit);
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
+ var query = "SELECT doc FROM events WHERE (nodeid = $1 AND domain = $2 AND ((userid = $3) OR (userid IS NULL))";
+ var dataarray = [nodeid, domain, userid];
+ if (filter != null) {
+ query = query + " AND action = $4) ORDER BY time DESC LIMIT $5";
+ dataarray.push(filter);
+ } else {
+ query = query + ") ORDER BY time DESC LIMIT $4";
+ }
+ dataarray.push(limit);
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.RemoveAllEvents = function (domain) { sqlDbQuery('DELETE FROM events', null, function (err, docs) { }); };
+ obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = $1 AND nodeid = $2', [domain, nodeid], function (err, docs) { }); };
+ obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = $1 AND userid = $2', [domain, userid], function (err, docs) { }); };
+ obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) { sqlDbQuery('SELECT COUNT(*) FROM events WHERE action = \'authfail\' AND domain = $1 AND userid = $2 AND time > $3', [domainid, userid, lastlogin], function (err, response, raw) { func(err == null ? parseInt(raw.rows[0].count) : 0); }); }
+
+ // Database actions on the power collection
+ obj.getAllPower = function (func) { sqlDbQuery('SELECT doc FROM power', null, func); };
+ obj.storePowerEvent = function (event, multiServer, func) { obj.dbCounters.powerSet++; if (multiServer != null) { event.server = multiServer.serverid; } sqlDbQuery('INSERT INTO power VALUES (DEFAULT, $1, $2, $3)', [event.time, event.nodeid ? event.nodeid : null, event], func); };
+ obj.getPowerTimeline = function (nodeid, func) { sqlDbQuery('SELECT doc FROM power WHERE ((nodeid = $1) OR (nodeid = \'*\')) ORDER BY time ASC', [nodeid], func); };
+ obj.removeAllPowerEvents = function () { sqlDbQuery('DELETE FROM power', null, function (err, docs) { }); };
+ obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; sqlDbQuery('DELETE FROM power WHERE nodeid = $1', [nodeid], function (err, docs) { }); };
+
+ // Database actions on the SMBIOS collection
+ obj.GetAllSMBIOS = function (func) { sqlDbQuery('SELECT doc FROM smbios', null, func); };
+ obj.SetSMBIOS = function (smbios, func) { var expire = new Date(smbios.time); expire.setMonth(expire.getMonth() + 6); sqlDbQuery('INSERT INTO smbios VALUES ($1, $2, $3, $4) ON CONFLICT (id) DO UPDATE SET time = $2, expire = $3, doc = $4', [smbios._id, smbios.time, expire, smbios], func); };
+ obj.RemoveSMBIOS = function (id) { sqlDbQuery('DELETE FROM smbios WHERE id = $1', [id], function (err, docs) { }); };
+ obj.GetSMBIOS = function (id, func) { sqlDbQuery('SELECT doc FROM smbios WHERE id = $1', [id], func); };
+
+ // Database actions on the Server Stats collection
+ obj.SetServerStats = function (data, func) { sqlDbQuery('INSERT INTO serverstats VALUES ($1, $2, $3) ON CONFLICT (time) DO UPDATE SET expire = $2, doc = $3', [data.time, data.expire, data], func); };
+ obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); sqlDbQuery('SELECT doc FROM serverstats WHERE time > $1', [t], func); }; // TODO: Expire old entries
+
+ // Read a configuration file from the database
+ obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
+
+ // Write a configuration file to the database
+ obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
+
+ // List all configuration files
+ obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM main WHERE type = "cfile" ORDER BY id', func); }
+
+ // Get database information (TODO: Complete this)
+ obj.getDbStats = function (func) {
+ obj.stats = { c: 4 };
+ sqlDbQuery('SELECT COUNT(*) FROM main', null, function (err, response, raw) { obj.stats.meshcentral = (err == null ? parseInt(raw.rows[0].count) : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ sqlDbQuery('SELECT COUNT(*) FROM serverstats', null, function (err, response, raw) { obj.stats.serverstats = (err == null ? parseInt(raw.rows[0].count) : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ sqlDbQuery('SELECT COUNT(*) FROM power', null, function (err, response, raw) { obj.stats.power = (err == null ? parseInt(raw.rows[0].count) : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ sqlDbQuery('SELECT COUNT(*) FROM smbios', null, function (err, response, raw) { obj.stats.smbios = (err == null ? parseInt(raw.rows[0].count) : 0); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ }
+
+ // Plugin operations
+ if (obj.pluginsActive) {
+ obj.addPlugin = function (plugin, func) { sqlDbQuery('INSERT INTO plugin VALUES (DEFAULT, $1)', [plugin], func); }; // Add a plugin
+ obj.getPlugins = function (func) { sqlDbQuery("SELECT doc::jsonb || ('{\"_id\":' || plugin.id || '}')::jsonb as doc FROM plugin", null, func); }; // Get all plugins
+ obj.getPlugin = function (id, func) { sqlDbQuery("SELECT doc::jsonb || ('{\"_id\":' || plugin.id || '}')::jsonb as doc FROM plugin WHERE id = $1", [id], func); }; // Get plugin
+ obj.deletePlugin = function (id, func) { sqlDbQuery('DELETE FROM plugin WHERE id = $1', [id], func); }; // Delete plugin
+ obj.setPluginStatus = function (id, status, func) { sqlDbQuery("UPDATE plugin SET doc= jsonb_set(doc::jsonb,'{status}',$1) WHERE id=$2", [status,id], func); };
+ obj.updatePlugin = function (id, args, func) { delete args._id; sqlDbQuery('UPDATE plugin SET doc= doc::jsonb || ($1) WHERE id=$2', [args,id], func); };
+ }
+ } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
+ // Database actions on the main collection (MariaDB or MySQL)
+ obj.Set = function (value, func) {
+ obj.dbCounters.fileSet++;
+ var extra = null, extraex = null;
+ value = common.escapeLinksFieldNameEx(value);
+ if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
+ if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
+ if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
+ sqlDbQuery('REPLACE INTO main VALUE (?, ?, ?, ?, ?, ?)', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, JSON.stringify(performTypedRecordEncrypt(value))], func);
+ }
+ obj.SetRaw = function (value, func) {
+ obj.dbCounters.fileSet++;
+ var extra = null, extraex = null;
+ if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
+ if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
+ if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
+ sqlDbQuery('REPLACE INTO main VALUE (?, ?, ?, ?, ?, ?)', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, JSON.stringify(performTypedRecordEncrypt(value))], func);
+ }
+ obj.Get = function (_id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = ?', [_id], function (err, docs) { if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); } func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetAll = function (func) { sqlDbQuery('SELECT domain, doc FROM main', null, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetHash = function (id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = ?', [id], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetAllTypeNoTypeField = function (type, domain, func) { sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ?', [type, domain], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
+ if (limit == 0) { limit = 0xFFFFFFFF; }
+ if ((meshes == null) || (meshes.length == 0)) { meshes = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ if ((extrasids == null) || (extrasids.length == 0)) { extrasids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ if (id && (id != '')) {
+ sqlDbQuery('SELECT doc FROM main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?) LIMIT ? OFFSET ?', [id, type, domain, meshes, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
+ } else {
+ sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND (extra IN (?) OR id IN (?)) LIMIT ? OFFSET ?', [type, domain, meshes, extrasids, limit, skip], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
+ }
+ };
+ obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
+ if ((meshes == null) || (meshes.length == 0)) { meshes = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ if ((extrasids == null) || (extrasids.length == 0)) { extrasids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ if (id && (id != '')) {
+ sqlDbQuery('SELECT COUNT(doc) FROM main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?)', [id, type, domain, meshes], function (err, docs) { func(err, docs); });
+ } else {
+ sqlDbQuery('SELECT COUNT(doc) FROM main WHERE type = ? AND domain = ? AND (extra IN (?) OR id IN (?))', [type, domain, meshes, extrasids], function (err, docs) { func(err, docs); });
+ }
+ };
+ obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
+ if ((nodes == null) || (nodes.length == 0)) { nodes = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ if (id && (id != '')) {
+ sqlDbQuery('SELECT doc FROM main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?)', [id, type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
+ } else {
+ sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND extra IN (?)', [type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, performTypedRecordDecrypt(docs)); });
+ }
+ };
+ obj.GetAllType = function (type, func) { sqlDbQuery('SELECT doc FROM main WHERE type = ?', [type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetAllIdsOfType = function (ids, domain, type, func) {
+ if ((ids == null) || (ids.length == 0)) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ sqlDbQuery('SELECT doc FROM main WHERE id IN (?) AND domain = ? AND type = ?', [ids, domain, type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
+ }
+ obj.GetUserWithEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = ? AND extra = ?', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetUserWithVerifiedEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = ? AND extra = ?', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
+ obj.GetNodeByComputerName = function (domain, rname, func) { sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND JSON_EXTRACT(doc, "$.rname") = ? ORDER BY lastbootuptime', ['node', domain, rname], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.Remove = function (id, func) { sqlDbQuery('DELETE FROM main WHERE id = ?', [id], func); };
+ obj.RemoveAll = function (func) { sqlDbQuery('DELETE FROM main', null, func); };
+ obj.RemoveAllOfType = function (type, func) { sqlDbQuery('DELETE FROM main WHERE type = ?', [type], func); };
+ obj.InsertMany = function (data, func) { var pendingOps = 0; for (var i in data) { pendingOps++; obj.SetRaw(data[i], function () { if (--pendingOps == 0) { func(); } }); } }; // Insert records directly, no link escaping
+ obj.RemoveMeshDocuments = function (id, func) { sqlDbQuery('DELETE FROM main WHERE extra = ?', [id], function () { sqlDbQuery('DELETE FROM main WHERE id = ?', ['nt' + id], func); } ); };
+ obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
+ obj.DeleteDomain = function (domain, func) { sqlDbQuery('DELETE FROM main WHERE domain = ?', [domain], func); };
+ obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
+ obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
+ obj.getLocalAmtNodes = function (func) { sqlDbQuery('SELECT doc FROM main WHERE (type = "node") AND (extraex IS NULL)', null, function (err, docs) { var r = []; if (err == null) { for (var i in docs) { if (docs[i].host != null && docs[i].intelamt != null) { r.push(docs[i]); } } } func(err, r); }); };
+ obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = ? AND extraex = ?', [domainid, 'uuid/' + uuid], func); };
+ obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { sqlDbExec('SELECT COUNT(id) FROM main WHERE domain = ? AND type = ?', [domainid, type], function (err, response) { func((response['COUNT(id)'] == null) || (response['COUNT(id)'] > max), response['COUNT(id)']) }); } }
+
+ // Database actions on the events collection
+ obj.GetAllEvents = function (func) { sqlDbQuery('SELECT doc FROM events', null, func); };
+ obj.StoreEvent = function (event, func) {
+ obj.dbCounters.eventsSet++;
+ var batchQuery = [['INSERT INTO events VALUE (?, ?, ?, ?, ?, ?, ?)', [null, event.time, ((typeof event.domain == 'string') ? event.domain : null), event.action, event.nodeid ? event.nodeid : null, event.userid ? event.userid : null, JSON.stringify(event)]]];
+ for (var i in event.ids) { if (event.ids[i] != '*') { batchQuery.push(['INSERT INTO eventids VALUE (LAST_INSERT_ID(), ?)', [event.ids[i]]]); } }
+ sqlDbBatchExec(batchQuery, function (err, docs) { if (func != null) { func(err, docs); } });
+ };
+ obj.GetEvents = function (ids, domain, filter, func) {
+ var query = "SELECT doc FROM events ";
+ var dataarray = [domain];
+ if (ids.indexOf('*') >= 0) {
+ query = query + "WHERE (domain = ?";
+ if (filter != null) {
+ query = query + " AND action = ?";
+ dataarray.push(filter);
+ }
+ query = query + ") ORDER BY time DESC";
+ } else {
+ if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND target IN (?)";
+ dataarray.push(ids);
+ if (filter != null) {
+ query = query + " AND action = ?";
+ dataarray.push(filter);
+ }
+ query = query + ") GROUP BY id ORDER BY time DESC";
+ }
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
+ var query = "SELECT doc FROM events ";
+ var dataarray = [domain];
+ if (ids.indexOf('*') >= 0) {
+ query = query + "WHERE (domain = ?";
+ if (filter != null) {
+ query = query + " AND action = ? ";
+ dataarray.push(filter);
+ }
+ query = query + ") ORDER BY time DESC LIMIT ?";
+ } else {
+ if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND target IN (?)";
+ dataarray.push(ids);
+ if (filter != null) {
+ query = query + " AND action = ?";
+ dataarray.push(filter);
+ }
+ query = query + ") GROUP BY id ORDER BY time DESC LIMIT ?";
+ }
+ dataarray.push(limit);
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetUserEvents = function (ids, domain, userid, filter, func) {
+ var query = "SELECT doc FROM events ";
+ var dataarray = [domain, userid];
+ if (ids.indexOf('*') >= 0) {
+ query = query + "WHERE (domain = ? AND userid = ?";
+ if (filter != null) {
+ query = query + " AND action = ?";
+ dataarray.push(filter);
+ }
+ query = query + ") ORDER BY time DESC";
+ } else {
+ if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND userid = ? AND target IN (?)";
+ dataarray.push(ids);
+ if (filter != null) {
+ query = query + " AND action = ?";
+ dataarray.push(filter);
+ }
+ query = query + ") GROUP BY id ORDER BY time DESC";
+ }
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
+ var query = "SELECT doc FROM events ";
+ var dataarray = [domain, userid];
+ if (ids.indexOf('*') >= 0) {
+ query = query + "WHERE (domain = ? AND userid = ?";
+ if (filter != null) {
+ query = query + " AND action = ?";
+ dataarray.push(filter);
+ }
+ query = query + ") ORDER BY time DESC LIMIT ?";
+ } else {
+ if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND userid = ? AND target IN (?)";
+ dataarray.push(ids);
+ if (filter != null) {
+ query = query + " AND action = ?";
+ dataarray.push(filter);
+ }
+ query = query + ") GROUP BY id ORDER BY time DESC LIMIT ?";
+ }
+ dataarray.push(limit);
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
+ if (ids.indexOf('*') >= 0) {
+ sqlDbQuery('SELECT doc FROM events WHERE ((domain = ?) AND (time BETWEEN ? AND ?)) ORDER BY time', [domain, start, end], func);
+ } else {
+ if (ids.length == 0) { ids = ''; } // MySQL can't handle a query with IN() on an empty array, we have to use an empty string instead.
+ sqlDbQuery('SELECT doc FROM events JOIN eventids ON id = fkid WHERE ((domain = ?) AND (target IN (?)) AND (time BETWEEN ? AND ?)) GROUP BY id ORDER BY time', [domain, ids, start, end], func);
+ }
+ };
+ //obj.GetUserLoginEvents = function (domain, userid, func) { } // TODO
+ obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
+ var query = "SELECT doc FROM events WHERE (nodeid = ? AND domain = ?";
+ var dataarray = [nodeid, domain];
+ if (filter != null) {
+ query = query + " AND action = ?) ORDER BY time DESC LIMIT ?";
+ dataarray.push(filter);
+ } else {
+ query = query + ") ORDER BY time DESC LIMIT ?";
+ }
+ dataarray.push(limit);
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
+ var query = "SELECT doc FROM events WHERE (nodeid = ? AND domain = ? AND ((userid = ?) OR (userid IS NULL))";
+ var dataarray = [nodeid, domain, userid];
+ if (filter != null) {
+ query = query + " AND action = ?) ORDER BY time DESC LIMIT ?";
+ dataarray.push(filter);
+ } else {
+ query = query + ") ORDER BY time DESC LIMIT ?";
+ }
+ dataarray.push(limit);
+ sqlDbQuery(query, dataarray, func);
+ };
+ obj.RemoveAllEvents = function (domain) { sqlDbQuery('DELETE FROM events', null, function (err, docs) { }); };
+ obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = ? AND nodeid = ?', [domain, nodeid], function (err, docs) { }); };
+ obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = ? AND userid = ?', [domain, userid], function (err, docs) { }); };
+ obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) { sqlDbExec('SELECT COUNT(id) FROM events WHERE action = "authfail" AND domain = ? AND userid = ? AND time > ?', [domainid, userid, lastlogin], function (err, response) { func(err == null ? response['COUNT(id)'] : 0); }); }
+
+ // Database actions on the power collection
+ obj.getAllPower = function (func) { sqlDbQuery('SELECT doc FROM power', null, func); };
+ obj.storePowerEvent = function (event, multiServer, func) { obj.dbCounters.powerSet++; if (multiServer != null) { event.server = multiServer.serverid; } sqlDbQuery('INSERT INTO power VALUE (?, ?, ?, ?)', [null, event.time, event.nodeid ? event.nodeid : null, JSON.stringify(event)], func); };
+ obj.getPowerTimeline = function (nodeid, func) { sqlDbQuery('SELECT doc FROM power WHERE ((nodeid = ?) OR (nodeid = "*")) ORDER BY time ASC', [nodeid], func); };
+ obj.removeAllPowerEvents = function () { sqlDbQuery('DELETE FROM power', null, function (err, docs) { }); };
+ obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; sqlDbQuery('DELETE FROM power WHERE nodeid = ?', [nodeid], function (err, docs) { }); };
+
+ // Database actions on the SMBIOS collection
+ obj.GetAllSMBIOS = function (func) { sqlDbQuery('SELECT doc FROM smbios', null, func); };
+ obj.SetSMBIOS = function (smbios, func) { var expire = new Date(smbios.time); expire.setMonth(expire.getMonth() + 6); sqlDbQuery('REPLACE INTO smbios VALUE (?, ?, ?, ?)', [smbios._id, smbios.time, expire, JSON.stringify(smbios)], func); };
+ obj.RemoveSMBIOS = function (id) { sqlDbQuery('DELETE FROM smbios WHERE id = ?', [id], function (err, docs) { }); };
+ obj.GetSMBIOS = function (id, func) { sqlDbQuery('SELECT doc FROM smbios WHERE id = ?', [id], func); };
+
+ // Database actions on the Server Stats collection
+ obj.SetServerStats = function (data, func) { sqlDbQuery('REPLACE INTO serverstats VALUE (?, ?, ?)', [data.time, data.expire, JSON.stringify(data)], func); };
+ obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); sqlDbQuery('SELECT doc FROM serverstats WHERE time > ?', [t], func); };
+
+ // Read a configuration file from the database
+ obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
+
+ // Write a configuration file to the database
+ obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
+
+ // List all configuration files
+ obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM main WHERE type = "cfile" ORDER BY id', func); }
+
+ // Get database information (TODO: Complete this)
+ obj.getDbStats = function (func) {
+ obj.stats = { c: 4 };
+ sqlDbExec('SELECT COUNT(id) FROM main', null, function (err, response) { obj.stats.meshcentral = Number(response['COUNT(id)']); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ sqlDbExec('SELECT COUNT(time) FROM serverstats', null, function (err, response) { obj.stats.serverstats = Number(response['COUNT(time)']); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ sqlDbExec('SELECT COUNT(id) FROM power', null, function (err, response) { obj.stats.power = Number(response['COUNT(id)']); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ sqlDbExec('SELECT COUNT(id) FROM smbios', null, function (err, response) { obj.stats.smbios = Number(response['COUNT(id)']); if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
+ }
+
+ // Plugin operations
+ if (obj.pluginsActive) {
+ obj.addPlugin = function (plugin, func) { sqlDbQuery('INSERT INTO plugin VALUE (?, ?)', [null, JSON.stringify(plugin)], func); }; // Add a plugin
+ obj.getPlugins = function (func) { sqlDbQuery('SELECT JSON_INSERT(doc, "$._id", id) as doc FROM plugin', null, func); }; // Get all plugins
+ obj.getPlugin = function (id, func) { sqlDbQuery('SELECT JSON_INSERT(doc, "$._id", id) as doc FROM plugin WHERE id = ?', [id], func); }; // Get plugin
+ obj.deletePlugin = function (id, func) { sqlDbQuery('DELETE FROM plugin WHERE id = ?', [id], func); }; // Delete plugin
+ obj.setPluginStatus = function (id, status, func) { sqlDbQuery('UPDATE meshcentral.plugin SET doc=JSON_SET(doc,"$.status",?) WHERE id=?', [status,id], func); };
+ obj.updatePlugin = function (id, args, func) { delete args._id; sqlDbQuery('UPDATE meshcentral.plugin SET doc=JSON_MERGE_PATCH(doc,?) WHERE id=?', [JSON.stringify(args),id], func); };
+ }
+ } else if (obj.databaseType == DB_MONGODB) {
+ // Database actions on the main collection (MongoDB)
+
+ // Bulk operations
+ if (parent.config.settings.mongodbbulkoperations) {
+ obj.Set = function (data, func) { // Fast Set operation using bulkWrite(), this is much faster then using replaceOne()
+ if (obj.filePendingSet == false) {
+ // Perform the operation now
+ obj.dbCounters.fileSet++;
+ obj.filePendingSet = true; obj.filePendingSets = null;
+ if (func != null) { obj.filePendingCbs = [func]; }
+ obj.file.bulkWrite([{ replaceOne: { filter: { _id: data._id }, replacement: performTypedRecordEncrypt(common.escapeLinksFieldNameEx(data)), upsert: true } }], fileBulkWriteCompleted);
+ } else {
+ // Add this operation to the pending list
+ obj.dbCounters.fileSetPending++;
+ if (obj.filePendingSets == null) { obj.filePendingSets = {} }
+ obj.filePendingSets[data._id] = data;
+ if (func != null) { if (obj.filePendingCb == null) { obj.filePendingCb = [func]; } else { obj.filePendingCb.push(func); } }
+ }
+ };
+
+ obj.Get = function (id, func) { // Fast Get operation using a bulk find() to reduce round trips to the database.
+ // Encode arguments into return function if any are present.
+ var func2 = func;
+ if (arguments.length > 2) {
+ var parms = [func];
+ for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
+ var func2 = function _func2(arg1, arg2) {
+ var userCallback = _func2.userArgs.shift();
+ _func2.userArgs.unshift(arg2);
+ _func2.userArgs.unshift(arg1);
+ userCallback.apply(obj, _func2.userArgs);
+ };
+ func2.userArgs = parms;
+ }
+
+ if (obj.filePendingGets == null) {
+ // No pending gets, perform the operation now.
+ obj.filePendingGets = {};
+ obj.filePendingGets[id] = [func2];
+ obj.file.find({ _id: id }).toArray(fileBulkReadCompleted);
+ } else {
+ // Add get to pending list.
+ if (obj.filePendingGet == null) { obj.filePendingGet = {}; }
+ if (obj.filePendingGet[id] == null) { obj.filePendingGet[id] = [func2]; } else { obj.filePendingGet[id].push(func2); }
+ }
+ };
+ } else {
+ obj.Set = function (data, func) {
+ obj.dbCounters.fileSet++;
+ data = common.escapeLinksFieldNameEx(data);
+ obj.file.replaceOne({ _id: data._id }, performTypedRecordEncrypt(data), { upsert: true }, func);
+ };
+ obj.Get = function (id, func) {
+ if (arguments.length > 2) {
+ var parms = [func];
+ for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
+ var func2 = function _func2(arg1, arg2) {
+ var userCallback = _func2.userArgs.shift();
+ _func2.userArgs.unshift(arg2);
+ _func2.userArgs.unshift(arg1);
+ userCallback.apply(obj, _func2.userArgs);
+ };
+ func2.userArgs = parms;
+ obj.file.find({ _id: id }).toArray(function (err, docs) {
+ if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
+ func2(err, performTypedRecordDecrypt(docs));
+ });
+ } else {
+ obj.file.find({ _id: id }).toArray(function (err, docs) {
+ if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ }
+ };
+ }
+ obj.GetAll = function (func) { obj.file.find({}).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetHash = function (id, func) { obj.file.find({ _id: id }).project({ _id: 0, hash: 1 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }).project({ type: 0 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
+ if (extrasids == null) {
+ const x = { type: type, domain: domain, meshid: { $in: meshes } };
+ if (id) { x._id = id; }
+ var f = obj.file.find(x, { type: 0 });
+ if (skip > 0) f = f.skip(skip); // Skip records
+ if (limit > 0) f = f.limit(limit); // Limit records
+ f.toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
+ } else {
+ const x = { type: type, domain: domain, $or: [ { meshid: { $in: meshes } }, { _id: { $in: extrasids } } ] };
+ if (id) { x._id = id; }
+ var f = obj.file.find(x, { type: 0 });
+ if (skip > 0) f = f.skip(skip); // Skip records
+ if (limit > 0) f = f.limit(limit); // Limit records
+ f.toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
+ }
+ };
+ obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
+ if (extrasids == null) {
+ const x = { type: type, domain: domain, meshid: { $in: meshes } };
+ if (id) { x._id = id; }
+ var f = obj.file.find(x, { type: 0 });
+ f.count(function (err, count) { func(err, count); });
+ } else {
+ const x = { type: type, domain: domain, $or: [{ meshid: { $in: meshes } }, { _id: { $in: extrasids } }] };
+ if (id) { x._id = id; }
+ var f = obj.file.find(x, { type: 0 });
+ f.count(function (err, count) { func(err, count); });
+ }
+ };
+ obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
+ var x = { type: type, domain: domain, nodeid: { $in: nodes } };
+ if (id) { x._id = id; }
+ obj.file.find(x, { type: 0 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
+ };
+ obj.GetAllType = function (type, func) { obj.file.find({ type: type }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.find({ type: type, domain: domain, _id: { $in: ids } }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetUserWithEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email, emailVerified: true }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetNodeByComputerName = function (domain, rname, func) { obj.file.find({ type: 'node', domain: domain, rname: rname }).sort({ lastbootuptime: -1 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+
+ // Bulk operations
+ if (parent.config.settings.mongodbbulkoperations) {
+ obj.Remove = function (id, func) { // Fast remove operation using a bulk find() to reduce round trips to the database.
+ if (obj.filePendingRemoves == null) {
+ // No pending removes, perform the operation now.
+ obj.dbCounters.fileRemove++;
+ obj.filePendingRemoves = {};
+ obj.filePendingRemoves[id] = [func];
+ obj.file.deleteOne({ _id: id }, fileBulkRemoveCompleted);
+ } else {
+ // Add remove to pending list.
+ obj.dbCounters.fileRemovePending++;
+ if (obj.filePendingRemove == null) { obj.filePendingRemove = {}; }
+ if (obj.filePendingRemove[id] == null) { obj.filePendingRemove[id] = [func]; } else { obj.filePendingRemove[id].push(func); }
+ }
+ };
+ } else {
+ obj.Remove = function (id, func) { obj.dbCounters.fileRemove++; obj.file.deleteOne({ _id: id }, func); };
+ }
+
+ obj.RemoveAll = function (func) { obj.file.deleteMany({}, { multi: true }, func); };
+ obj.RemoveAllOfType = function (type, func) { obj.file.deleteMany({ type: type }, { multi: true }, func); };
+ obj.InsertMany = function (data, func) { obj.file.insertMany(data, func); }; // Insert records directly, no link escaping
+ obj.RemoveMeshDocuments = function (id) { obj.file.deleteMany({ meshid: id }, { multi: true }); obj.file.deleteOne({ _id: 'nt' + id }); };
+ obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
+ obj.DeleteDomain = function (domain, func) { obj.file.deleteMany({ domain: domain }, { multi: true }, func); };
+ obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
+ obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
+ obj.getLocalAmtNodes = function (func) { obj.file.find({ type: 'node', host: { $exists: true, $ne: null }, intelamt: { $exists: true } }).toArray(func); };
+ obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { obj.file.find({ type: 'node', domain: domainid, mtype: mtype, 'intelamt.uuid': uuid }).toArray(func); };
+
+ // TODO: Starting in MongoDB 4.0.3, you should use countDocuments() instead of count() that is deprecated. We should detect MongoDB version and switch.
+ // https://docs.mongodb.com/manual/reference/method/db.collection.countDocuments/
+ //obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { obj.file.countDocuments({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max)); }); } }
+ obj.isMaxType = function (max, type, domainid, func) {
+ if (obj.file.countDocuments) {
+ if (max == null) { func(false); } else { obj.file.countDocuments({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); }
+ } else {
+ if (max == null) { func(false); } else { obj.file.count({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); }
+ }
+ }
+
+ // Database actions on the events collection
+ obj.GetAllEvents = function (func) { obj.eventsfile.find({}).toArray(func); };
+
+ // Bulk operations
+ if (parent.config.settings.mongodbbulkoperations) {
+ obj.StoreEvent = function (event, func) { // Fast MongoDB event store using bulkWrite()
+ if (obj.eventsFilePendingSet == false) {
+ // Perform the operation now
+ obj.dbCounters.eventsSet++;
+ obj.eventsFilePendingSet = true; obj.eventsFilePendingSets = null;
+ if (func != null) { obj.eventsFilePendingCbs = [func]; }
+ obj.eventsfile.bulkWrite([{ insertOne: { document: event } }], eventsFileBulkWriteCompleted);
+ } else {
+ // Add this operation to the pending list
+ obj.dbCounters.eventsSetPending++;
+ if (obj.eventsFilePendingSets == null) { obj.eventsFilePendingSets = [] }
+ obj.eventsFilePendingSets.push(event);
+ if (func != null) { if (obj.eventsFilePendingCb == null) { obj.eventsFilePendingCb = [func]; } else { obj.eventsFilePendingCb.push(func); } }
+ }
+ };
+ } else {
+ obj.StoreEvent = function (event, func) { obj.dbCounters.eventsSet++; obj.eventsfile.insertOne(event, func); };
+ }
+
+ obj.GetEvents = function (ids, domain, filter, func) {
+ var finddata = { domain: domain, ids: { $in: ids } };
+ if (filter != null) finddata.action = filter;
+ obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).toArray(func);
+ };
+ obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
+ var finddata = { domain: domain, ids: { $in: ids } };
+ if (filter != null) finddata.action = filter;
+ obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).toArray(func);
+ };
+ obj.GetUserEvents = function (ids, domain, userid, filter, func) {
+ var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
+ if (filter != null) finddata.action = filter;
+ obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).toArray(func);
+ };
+ obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
+ var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
+ if (filter != null) finddata.action = filter;
+ obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).toArray(func);
+ };
+ obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) { obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }], msgid: { $in: msgids }, time: { $gte: start, $lte: end } }).project({ type: 0, _id: 0, domain: 0, node: 0 }).sort({ time: 1 }).toArray(func); };
+ obj.GetUserLoginEvents = function (domain, userid, func) { obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }).project({ action: 1, time: 1, msgid: 1, msgArgs: 1, tokenName: 1 }).sort({ time: -1 }).toArray(func); };
+ obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
+ var finddata = { domain: domain, nodeid: nodeid };
+ if (filter != null) finddata.action = filter;
+ obj.eventsfile.find(finddata).project({ type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).toArray(func);
+ };
+ obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
+ var finddata = { domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } };
+ if (filter != null) finddata.action = filter;
+ obj.eventsfile.find(finddata).project({ type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).toArray(func);
+ };
+ obj.RemoveAllEvents = function (domain) { obj.eventsfile.deleteMany({ domain: domain }, { multi: true }); };
+ obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; obj.eventsfile.deleteMany({ domain: domain, nodeid: nodeid }, { multi: true }); };
+ obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; obj.eventsfile.deleteMany({ domain: domain, userid: userid }, { multi: true }); };
+ obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) {
+ if (obj.eventsfile.countDocuments) {
+ obj.eventsfile.countDocuments({ action: 'authfail', userid: userid, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null) ? count : 0); });
+ } else {
+ obj.eventsfile.count({ action: 'authfail', userid: userid, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null) ? count : 0); });
+ }
+ }
+
+ // Database actions on the power collection
+ obj.getAllPower = function (func) { obj.powerfile.find({}).toArray(func); };
+
+ // Bulk operations
+ if (parent.config.settings.mongodbbulkoperations) {
+ obj.storePowerEvent = function (event, multiServer, func) { // Fast MongoDB event store using bulkWrite()
+ if (multiServer != null) { event.server = multiServer.serverid; }
+ if (obj.powerFilePendingSet == false) {
+ // Perform the operation now
+ obj.dbCounters.powerSet++;
+ obj.powerFilePendingSet = true; obj.powerFilePendingSets = null;
+ if (func != null) { obj.powerFilePendingCbs = [func]; }
+ obj.powerfile.bulkWrite([{ insertOne: { document: event } }], powerFileBulkWriteCompleted);
+ } else {
+ // Add this operation to the pending list
+ obj.dbCounters.powerSetPending++;
+ if (obj.powerFilePendingSets == null) { obj.powerFilePendingSets = [] }
+ obj.powerFilePendingSets.push(event);
+ if (func != null) { if (obj.powerFilePendingCb == null) { obj.powerFilePendingCb = [func]; } else { obj.powerFilePendingCb.push(func); } }
+ }
+ };
+ } else {
+ obj.storePowerEvent = function (event, multiServer, func) { obj.dbCounters.powerSet++; if (multiServer != null) { event.server = multiServer.serverid; } obj.powerfile.insertOne(event, func); };
+ }
+
+ obj.getPowerTimeline = function (nodeid, func) { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }).project({ _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }).toArray(func); };
+ obj.removeAllPowerEvents = function () { obj.powerfile.deleteMany({}, { multi: true }); };
+ obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; obj.powerfile.deleteMany({ nodeid: nodeid }, { multi: true }); };
+
+ // Database actions on the SMBIOS collection
+ obj.GetAllSMBIOS = function (func) { obj.smbiosfile.find({}).toArray(func); };
+ obj.SetSMBIOS = function (smbios, func) { obj.smbiosfile.updateOne({ _id: smbios._id }, { $set: smbios }, { upsert: true }, func); };
+ obj.RemoveSMBIOS = function (id) { obj.smbiosfile.deleteOne({ _id: id }); };
+ obj.GetSMBIOS = function (id, func) { obj.smbiosfile.find({ _id: id }).toArray(func); };
+
+ // Database actions on the Server Stats collection
+ obj.SetServerStats = function (data, func) { obj.serverstatsfile.insertOne(data, func); };
+ obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); obj.serverstatsfile.find({ time: { $gt: t } }, { _id: 0, cpu: 0 }).toArray(func); };
+
+ // Read a configuration file from the database
+ obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
+
+ // Write a configuration file to the database
+ obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
+
+ // List all configuration files
+ obj.listConfigFiles = function (func) { obj.file.find({ type: 'cfile' }).sort({ _id: 1 }).toArray(func); }
+
+ // Get database information
+ obj.getDbStats = function (func) {
+ obj.stats = { c: 6 };
+ obj.getStats(function (r) { obj.stats.recordTypes = r; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } })
+ obj.file.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
+ obj.eventsfile.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
+ obj.powerfile.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
+ obj.smbiosfile.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
+ obj.serverstatsfile.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
+ }
+
+ // Correct database information of obj.getDbStats before returning it
+ function getDbStatsEx(data) {
+ var r = {};
+ if (data.recordTypes != null) { r = data.recordTypes; }
+ try { r.smbios = data['meshcentral.smbios'].count; } catch (ex) { }
+ try { r.power = data['meshcentral.power'].count; } catch (ex) { }
+ try { r.events = data['meshcentral.events'].count; } catch (ex) { }
+ try { r.serverstats = data['meshcentral.serverstats'].count; } catch (ex) { }
+ return r;
+ }
+
+ // Plugin operations
+ if (obj.pluginsActive) {
+ obj.addPlugin = function (plugin, func) { plugin.type = 'plugin'; obj.pluginsfile.insertOne(plugin, func); }; // Add a plugin
+ obj.getPlugins = function (func) { obj.pluginsfile.find({ type: 'plugin' }).project({ type: 0 }).sort({ name: 1 }).toArray(func); }; // Get all plugins
+ obj.getPlugin = function (id, func) { id = require('mongodb').ObjectId(id); obj.pluginsfile.find({ _id: id }).sort({ name: 1 }).toArray(func); }; // Get plugin
+ obj.deletePlugin = function (id, func) { id = require('mongodb').ObjectId(id); obj.pluginsfile.deleteOne({ _id: id }, func); }; // Delete plugin
+ obj.setPluginStatus = function (id, status, func) { id = require('mongodb').ObjectId(id); obj.pluginsfile.updateOne({ _id: id }, { $set: { status: status } }, func); };
+ obj.updatePlugin = function (id, args, func) { delete args._id; id = require('mongodb').ObjectId(id); obj.pluginsfile.updateOne({ _id: id }, { $set: args }, func); };
+ }
+
+ } else {
+ // Database actions on the main collection (NeDB and MongoJS)
+ obj.Set = function (data, func) {
+ obj.dbCounters.fileSet++;
+ data = common.escapeLinksFieldNameEx(data);
+ var xdata = performTypedRecordEncrypt(data); obj.file.update({ _id: xdata._id }, xdata, { upsert: true }, func);
+ };
+ obj.Get = function (id, func) {
+ if (arguments.length > 2) {
+ var parms = [func];
+ for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
+ var func2 = function _func2(arg1, arg2) {
+ var userCallback = _func2.userArgs.shift();
+ _func2.userArgs.unshift(arg2);
+ _func2.userArgs.unshift(arg1);
+ userCallback.apply(obj, _func2.userArgs);
+ };
+ func2.userArgs = parms;
+ obj.file.find({ _id: id }, function (err, docs) {
+ if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
+ func2(err, performTypedRecordDecrypt(docs));
+ });
+ } else {
+ obj.file.find({ _id: id }, function (err, docs) {
+ if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
+ func(err, performTypedRecordDecrypt(docs));
+ });
+ }
+ };
+ obj.GetAll = function (func) { obj.file.find({}, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetHash = function (id, func) { obj.file.find({ _id: id }, { _id: 0, hash: 1 }, func); };
+ obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ //obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, domain, type, id, skip, limit, func) {
+ //var x = { type: type, domain: domain, meshid: { $in: meshes } };
+ //if (id) { x._id = id; }
+ //obj.file.find(x, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
+ //};
+ obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
+ if (extrasids == null) {
+ const x = { type: type, domain: domain, meshid: { $in: meshes } };
+ if (id) { x._id = id; }
+ obj.file.find(x, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
+ } else {
+ const x = { type: type, domain: domain, $or: [{ meshid: { $in: meshes } }, { _id: { $in: extrasids } }] };
+ if (id) { x._id = id; }
+ obj.file.find(x, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
+ }
+ };
+ obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
+ var x = { type: type, domain: domain, nodeid: { $in: nodes } };
+ if (id) { x._id = id; }
+ obj.file.find(x, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
+ };
+ obj.GetAllType = function (type, func) { obj.file.find({ type: type }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.find({ type: type, domain: domain, _id: { $in: ids } }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetUserWithEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email }, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email, emailVerified: true }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetNodeByComputerName = function (domain, rname, func) { obj.file.find({ type: 'node', domain: domain, rname: rname }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.Remove = function (id, func) { obj.file.remove({ _id: id }, func); };
+ obj.RemoveAll = function (func) { obj.file.remove({}, { multi: true }, func); };
+ obj.RemoveAllOfType = function (type, func) { obj.file.remove({ type: type }, { multi: true }, func); };
+ obj.InsertMany = function (data, func) { obj.file.insert(data, func); }; // Insert records directly, no link escaping
+ obj.RemoveMeshDocuments = function (id) { obj.file.remove({ meshid: id }, { multi: true }); obj.file.remove({ _id: 'nt' + id }); };
+ obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
+ obj.DeleteDomain = function (domain, func) { obj.file.remove({ domain: domain }, { multi: true }, func); };
+ obj.SetUser = function (user) { if (user == null) return; if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
+ obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
+ obj.getLocalAmtNodes = function (func) { obj.file.find({ type: 'node', host: { $exists: true, $ne: null }, intelamt: { $exists: true } }, func); };
+ obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { obj.file.find({ type: 'node', domain: domainid, mtype: mtype, 'intelamt.uuid': uuid }, func); };
+ obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { obj.file.count({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); } }
+
+ // Database actions on the events collection
+ obj.GetAllEvents = function (func) { obj.eventsfile.find({}, func); };
+ obj.StoreEvent = function (event, func) { obj.eventsfile.insert(event, func); };
+ obj.GetEvents = function (ids, domain, filter, func) {
+ var finddata = { domain: domain, ids: { $in: ids } };
+ if (filter != null) finddata.action = filter;
+ if (obj.databaseType == DB_NEDB) {
+ obj.eventsfile.find(finddata, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func);
+ } else {
+ obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func);
+ }
+ };
+ obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
+ var finddata = { domain: domain, ids: { $in: ids } };
+ if (filter != null) finddata.action = filter;
+ if (obj.databaseType == DB_NEDB) {
+ obj.eventsfile.find(finddata, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func);
+ } else {
+ obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func);
+ }
+ };
+ obj.GetUserEvents = function (ids, domain, userid, filter, func) {
+ var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
+ if (filter != null) finddata.action = filter;
+ if (obj.databaseType == DB_NEDB) {
+ obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func);
+ } else {
+ obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func);
+ }
+ };
+ obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
+ var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
+ if (filter != null) finddata.action = filter;
+ if (obj.databaseType == DB_NEDB) {
+ obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func);
+ } else {
+ obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func);
+ }
+ };
+ obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
+ if (obj.databaseType == DB_NEDB) {
+ obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }], msgid: { $in: msgids }, time: { $gte: start, $lte: end } }, { type: 0, _id: 0, domain: 0, node: 0 }).sort({ time: 1 }).exec(func);
+ } else {
+ obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }], msgid: { $in: msgids }, time: { $gte: start, $lte: end } }, { type: 0, _id: 0, domain: 0, node: 0 }).sort({ time: 1 }, func);
+ }
+ };
+ obj.GetUserLoginEvents = function (domain, userid, func) {
+ if (obj.databaseType == DB_NEDB) {
+ obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }, { action: 1, time: 1, msgid: 1, msgArgs: 1, tokenName: 1 }).sort({ time: -1 }).exec(func);
+ } else {
+ obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }, { action: 1, time: 1, msgid: 1, msgArgs: 1, tokenName: 1 }).sort({ time: -1 }, func);
+ }
+ };
+ obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
+ var finddata = { domain: domain, nodeid: nodeid };
+ if (filter != null) finddata.action = filter;
+ if (obj.databaseType == DB_NEDB) {
+ obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).exec(func);
+ } else {
+ obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit, func);
+ }
+ };
+ obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
+ var finddata = { domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } };
+ if (filter != null) finddata.action = filter;
+ if (obj.databaseType == DB_NEDB) {
+ obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).exec(func);
+ } else {
+ obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit, func);
+ }
+ };
+ obj.RemoveAllEvents = function (domain) { obj.eventsfile.remove({ domain: domain }, { multi: true }); };
+ obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; obj.eventsfile.remove({ domain: domain, nodeid: nodeid }, { multi: true }); };
+ obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; obj.eventsfile.remove({ domain: domain, userid: userid }, { multi: true }); };
+ obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) { obj.eventsfile.count({ action: 'authfail', userid: userid, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null) ? count : 0); }); }
+
+ // Database actions on the power collection
+ obj.getAllPower = function (func) { obj.powerfile.find({}, func); };
+ obj.storePowerEvent = function (event, multiServer, func) { if (multiServer != null) { event.server = multiServer.serverid; } obj.powerfile.insert(event, func); };
+ obj.getPowerTimeline = function (nodeid, func) { if (obj.databaseType == DB_NEDB) { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }, { _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }).exec(func); } else { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }, { _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }, func); } };
+ obj.removeAllPowerEvents = function () { obj.powerfile.remove({}, { multi: true }); };
+ obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; obj.powerfile.remove({ nodeid: nodeid }, { multi: true }); };
+
+ // Database actions on the SMBIOS collection
+ if (obj.smbiosfile != null) {
+ obj.GetAllSMBIOS = function (func) { obj.smbiosfile.find({}, func); };
+ obj.SetSMBIOS = function (smbios, func) { obj.smbiosfile.update({ _id: smbios._id }, smbios, { upsert: true }, func); };
+ obj.RemoveSMBIOS = function (id) { obj.smbiosfile.remove({ _id: id }); };
+ obj.GetSMBIOS = function (id, func) { obj.smbiosfile.find({ _id: id }, func); };
+ }
+
+ // Database actions on the Server Stats collection
+ obj.SetServerStats = function (data, func) { obj.serverstatsfile.insert(data, func); };
+ obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); obj.serverstatsfile.find({ time: { $gt: t } }, { _id: 0, cpu: 0 }, func); };
+
+ // Read a configuration file from the database
+ obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
+
+ // Write a configuration file to the database
+ obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
+
+ // List all configuration files
+ obj.listConfigFiles = function (func) { obj.file.find({ type: 'cfile' }).sort({ _id: 1 }).exec(func); }
+
+ // Get database information
+ obj.getDbStats = function (func) {
+ obj.stats = { c: 5 };
+ obj.getStats(function (r) { obj.stats.recordTypes = r; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } })
+ obj.file.count({}, function (err, count) { obj.stats.meshcentral = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
+ obj.eventsfile.count({}, function (err, count) { obj.stats.events = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
+ obj.powerfile.count({}, function (err, count) { obj.stats.power = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
+ obj.serverstatsfile.count({}, function (err, count) { obj.stats.serverstats = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
+ }
+
+ // Correct database information of obj.getDbStats before returning it
+ function getDbStatsEx(data) {
+ var r = {};
+ if (data.recordTypes != null) { r = data.recordTypes; }
+ try { r.smbios = data['smbios'].count; } catch (ex) { }
+ try { r.power = data['power'].count; } catch (ex) { }
+ try { r.events = data['events'].count; } catch (ex) { }
+ try { r.serverstats = data['serverstats'].count; } catch (ex) { }
+ return r;
+ }
+
+ // Plugin operations
+ if (obj.pluginsActive) {
+ obj.addPlugin = function (plugin, func) { plugin.type = 'plugin'; obj.pluginsfile.insert(plugin, func); }; // Add a plugin
+ obj.getPlugins = function (func) { obj.pluginsfile.find({ 'type': 'plugin' }, { 'type': 0 }).sort({ name: 1 }).exec(func); }; // Get all plugins
+ obj.getPlugin = function (id, func) { obj.pluginsfile.find({ _id: id }).sort({ name: 1 }).exec(func); }; // Get plugin
+ obj.deletePlugin = function (id, func) { obj.pluginsfile.remove({ _id: id }, func); }; // Delete plugin
+ obj.setPluginStatus = function (id, status, func) { obj.pluginsfile.update({ _id: id }, { $set: { status: status } }, func); };
+ obj.updatePlugin = function (id, args, func) { delete args._id; obj.pluginsfile.update({ _id: id }, { $set: args }, func); };
+ }
+
+ }
+
+ // Get all configuration files
+ obj.getAllConfigFiles = function (password, func) {
+ obj.GetAllType('cfile', function (err, docs) {
+ if (err != null) { func(null); return; }
+ var r = null;
+ for (var i = 0; i < docs.length; i++) {
+ var name = docs[i]._id.split('/')[1];
+ var data = obj.decryptData(password, docs[i].data);
+ if (data != null) { if (r == null) { r = {}; } r[name] = data; }
+ }
+ func(r);
+ });
+ }
+
+ func(obj); // Completed function setup
+ }
+
+ // Return a human readable string with current backup configuration
+ obj.getBackupConfig = function () {
+ var r = '', backupPath = parent.backuppath;
+
+ let dbname = 'meshcentral';
+ if (parent.args.mongodbname) { dbname = parent.args.mongodbname; }
+ else if ((typeof parent.args.mariadb == 'object') && (typeof parent.args.mariadb.database == 'string')) { dbname = parent.args.mariadb.database; }
+ else if ((typeof parent.args.mysql == 'object') && (typeof parent.args.mysql.database == 'string')) { dbname = parent.args.mysql.database; }
+ else if ((typeof parent.args.postgres == 'object') && (typeof parent.args.postgres.database == 'string')) { dbname = parent.args.postgres.database; }
+ else if (typeof parent.config.settings.sqlite3 == 'string') {dbname = parent.config.settings.sqlite3 + '.sqlite'};
+
+ const currentDate = new Date();
+ const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2);
+ obj.newAutoBackupFile = parent.config.settings.autobackup.backupname + fileSuffix;
+
+ r += 'DB Name: ' + dbname + '\r\n';
+ r += 'DB Type: ' + DB_LIST[obj.databaseType] + '\r\n';
+
+ if (parent.config.settings.autobackup.backupintervalhours == -1) {
+ r += 'Backup disabled\r\n';
+ } else {
+ r += 'BackupPath: ' + backupPath + '\r\n';
+ r += 'BackupFile: ' + obj.newAutoBackupFile + '.zip\r\n';
+
+ if (parent.config.settings.autobackup.backuphour != null && parent.config.settings.autobackup.backuphour != -1) {
+ r += 'Backup between: ' + parent.config.settings.autobackup.backuphour + 'H-' + (parent.config.settings.autobackup.backuphour + 1) + 'H\r\n';
+ }
+ r += 'Backup Interval (Hours): ' + parent.config.settings.autobackup.backupintervalhours + '\r\n';
+ if (parent.config.settings.autobackup.keeplastdaysbackup != null) {
+ r += 'Keep Last Backups (Days): ' + parent.config.settings.autobackup.keeplastdaysbackup + '\r\n';
+ }
+ if (parent.config.settings.autobackup.zippassword != null) {
+ r += 'ZIP Password: ';
+ if (typeof parent.config.settings.autobackup.zippassword != 'string') { r += 'Bad zippassword type, Backups will not be encrypted\r\n'; }
+ else if (parent.config.settings.autobackup.zippassword == "") { r += 'Blank zippassword, Backups will fail\r\n'; }
+ else { r += 'Set\r\n'; }
+ }
+ if (parent.config.settings.autobackup.mongodumppath != null) {
+ r += 'MongoDump Path: ';
+ if (typeof parent.config.settings.autobackup.mongodumppath != 'string') { r += 'Bad mongodumppath type\r\n'; }
+ else { r += parent.config.settings.autobackup.mongodumppath + '\r\n'; }
+ }
+ if (parent.config.settings.autobackup.mysqldumppath != null) {
+ r += 'MySqlDump Path: ';
+ if (typeof parent.config.settings.autobackup.mysqldumppath != 'string') { r += 'Bad mysqldump type\r\n'; }
+ else { r += parent.config.settings.autobackup.mysqldumppath + '\r\n'; }
+ }
+ if (parent.config.settings.autobackup.pgdumppath != null) {
+ r += 'pgDump Path: ';
+ if (typeof parent.config.settings.autobackup.pgdumppath != 'string') { r += 'Bad pgdump type\r\n'; }
+ else { r += parent.config.settings.autobackup.pgdumppath + '\r\n'; }
+ }
+ if (parent.config.settings.autobackup.backupotherfolders) {
+ r += 'Backup other folders: ';
+ r += parent.filespath + ', ' + parent.recordpath + '\r\n';
+ }
+ if (parent.config.settings.autobackup.backupwebfolders) {
+ r += 'Backup webfolders: ';
+ if (parent.webViewsOverridePath) {r += parent.webViewsOverridePath };
+ if (parent.webPublicOverridePath) {r += ', '+ parent.webPublicOverridePath};
+ if (parent.webEmailsOverridePath) {r += ',' + parent.webEmailsOverridePath};
+ r+= '\r\n';
+ }
+ if (parent.config.settings.autobackup.backupignorefilesglob != []) {
+ r += 'Backup IgnoreFilesGlob: ';
+ { r += parent.config.settings.autobackup.backupignorefilesglob + '\r\n'; }
+ }
+ if (parent.config.settings.autobackup.backupskipfoldersglob != []) {
+ r += 'Backup SkipFoldersGlob: ';
+ { r += parent.config.settings.autobackup.backupskipfoldersglob + '\r\n'; }
+ }
+
+ if (typeof parent.config.settings.autobackup.s3 == 'object') {
+ r += 'S3 Backups: Enabled\r\n';
+ }
+ if (typeof parent.config.settings.autobackup.webdav == 'object') {
+ r += 'WebDAV Backups: Enabled\r\n';
+ r += 'WebDAV backup path: ' + ((typeof parent.config.settings.autobackup.webdav.foldername == 'string') ? parent.config.settings.autobackup.webdav.foldername : 'MeshCentral-Backups') + '\r\n';
+ r += 'WebDAV maximum files: '+ ((typeof parent.config.settings.autobackup.webdav.maxfiles == 'number') ? parent.config.settings.autobackup.webdav.maxfiles : 'no limit') + '\r\n';
+ }
+ if (typeof parent.config.settings.autobackup.googledrive == 'object') {
+ r += 'Google Drive Backups: Enabled\r\n';
+ }
+
+
+ }
+
+ return r;
+ }
+
+ function buildSqlDumpCommand() {
+ var props = (obj.databaseType == DB_MARIADB) ? parent.args.mariadb : parent.args.mysql;
+
+ var mysqldumpPath = 'mysqldump';
+ if (parent.config.settings.autobackup && parent.config.settings.autobackup.mysqldumppath) {
+ mysqldumpPath = path.normalize(parent.config.settings.autobackup.mysqldumppath);
+ }
+
+ var cmd = '\"' + mysqldumpPath + '\" --user=\'' + props.user + '\'';
+ // Windows will treat ' as part of the pw. Linux/Unix requires it to escape.
+ cmd += (parent.platform == 'win32') ? ' --password=\"' + props.password + '\"' : ' --password=\'' + props.password + '\'';
+ if (props.host) { cmd += ' -h ' + props.host; }
+ if (props.port) { cmd += ' -P ' + props.port; }
+
+ if (props.awsrds) { cmd += ' --single-transaction'; }
+
+ // SSL options different on mariadb/mysql
+ var sslOptions = '';
+ if (obj.databaseType == DB_MARIADB) {
+ if (props.ssl) {
+ sslOptions = ' --ssl';
+ if (props.ssl.cacertpath) sslOptions = ' --ssl-ca=' + props.ssl.cacertpath;
+ if (props.ssl.dontcheckserveridentity != true) {sslOptions += ' --ssl-verify-server-cert'} else {sslOptions += ' --ssl-verify-server-cert=false'};
+ if (props.ssl.clientcertpath) sslOptions += ' --ssl-cert=' + props.ssl.clientcertpath;
+ if (props.ssl.clientkeypath) sslOptions += ' --ssl-key=' + props.ssl.clientkeypath;
+ }
+ } else {
+ if (props.ssl) {
+ sslOptions = ' --ssl-mode=required';
+ if (props.ssl.cacertpath) sslOptions = ' --ssl-ca=' + props.ssl.cacertpath;
+ if (props.ssl.dontcheckserveridentity != true) sslOptions += ' --ssl-mode=verify_identity';
+ else sslOptions += ' --ssl-mode=required';
+ if (props.ssl.clientcertpath) sslOptions += ' --ssl-cert=' + props.ssl.clientcertpath;
+ if (props.ssl.clientkeypath) sslOptions += ' --ssl-key=' + props.ssl.clientkeypath;
+ }
+ }
+ cmd += sslOptions;
+
+ var dbname = (props.database) ? props.database : 'meshcentral';
+ cmd += ' ' + dbname
+
+ return cmd;
+ }
+
+ function buildMongoDumpCommand() {
+ const dburl = parent.args.mongodb;
+
+ var mongoDumpPath = 'mongodump';
+ if (parent.config.settings.autobackup && parent.config.settings.autobackup.mongodumppath) {
+ mongoDumpPath = path.normalize(parent.config.settings.autobackup.mongodumppath);
+ }
+
+ var cmd = '"' + mongoDumpPath + '"';
+ if (dburl) { cmd = '\"' + mongoDumpPath + '\" --uri=\"' + dburl + '\"'; }
+ if (parent.config.settings.autobackup?.mongodumpargs) {
+ cmd = '\"' + mongoDumpPath + '\" ' + parent.config.settings.autobackup.mongodumpargs;
+ if (!parent.config.settings.autobackup.mongodumpargs.includes("--db=")) {cmd += ' --db=' + (parent.config.settings.mongodbname ? parent.config.settings.mongodbname : 'meshcentral')};
+ }
+ return cmd;
+ }
+
+ // Check that the server is capable of performing a backup
+ // Tries configured custom location with fallback to default location
+ // Now runs after autobackup config init in meshcentral.js so config options are checked
+ obj.checkBackupCapability = function (func) {
+ if (parent.config.settings.autobackup.backupintervalhours == -1) { return; };
+ //block backup until validated. Gets put back if all checks are ok.
+ let backupInterval = parent.config.settings.autobackup.backupintervalhours;
+ parent.config.settings.autobackup.backupintervalhours = -1;
+ let backupPath = parent.backuppath;
+
+ if (backupPath.startsWith(parent.datapath)) {
+ func(1, "Backup path can't be set within meshcentral-data folder. No backups will be made.");
+ return;
+ }
+ // Check create/write backupdir
+ try { fs.mkdirSync(backupPath); }
+ catch (e) {
+ // EEXIST error = dir already exists
+ if (e.code != 'EEXIST' ) {
+ //Unable to create backuppath
+ console.error(e.message);
+ func(1, 'Unable to create ' + backupPath + '. No backups will be made. Error: ' + e.message);
+ return;
+ }
+ }
+ const currentDate = new Date();
+ const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2);
+ const testFile = path.join(backupPath, parent.config.settings.autobackup.backupname + fileSuffix + '.zip');
+ try { fs.writeFileSync( testFile, "DeleteMe"); }
+ catch (e) {
+ //Unable to create file
+ console.error (e.message);
+ func(1, "Backuppath (" + backupPath + ") can't be written to. No backups will be made. Error: " + e.message);
+ return;
+ }
+ try { fs.unlinkSync(testFile); parent.debug('backup', 'Backuppath ' + backupPath + ' accesscheck successful');}
+ catch (e) {
+ console.error (e.message);
+ func(1, "Backuppathtestfile (" + testFile + ") can't be deleted, check filerights. Error: " + e.message);
+ // Assume write rights, no delete rights. Continue with warning.
+ //return;
+ }
+
+ // Check database dumptools
+ if ((obj.databaseType == DB_MONGOJS) || (obj.databaseType == DB_MONGODB)) {
+ // Check that we have access to MongoDump
+ var cmd = buildMongoDumpCommand();
+ cmd += (parent.platform == 'win32') ? ' --archive=\"nul\"' : ' --archive=\"/dev/null\"';
+ const child_process = require('child_process');
+ child_process.exec(cmd, { cwd: backupPath }, function (error, stdout, stderr) {
+ if ((error != null) && (error != '')) {
+ func(1, "Mongodump error, backup will not be performed. Check path or use mongodumppath & mongodumpargs");
+ return;
+ } else {parent.config.settings.autobackup.backupintervalhours = backupInterval;}
+ });
+ } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
+ // Check that we have access to mysqldump
+ var cmd = buildSqlDumpCommand();
+ cmd += ' > ' + ((parent.platform == 'win32') ? '\"nul\"' : '\"/dev/null\"');
+ const child_process = require('child_process');
+ child_process.exec(cmd, { cwd: backupPath, timeout: 1000*30 }, function(error, stdout, stdin) {
+ if ((error != null) && (error != '')) {
+ func(1, "mysqldump error, backup will not be performed. Check path or use mysqldumppath");
+ return;
+ } else {parent.config.settings.autobackup.backupintervalhours = backupInterval;}
+
+ });
+ } else if (obj.databaseType == DB_POSTGRESQL) {
+ // Check that we have access to pg_dump
+ parent.config.settings.autobackup.pgdumppath = path.normalize(parent.config.settings.autobackup.pgdumppath ? parent.config.settings.autobackup.pgdumppath : 'pg_dump');
+ let cmd = '"' + parent.config.settings.autobackup.pgdumppath + '"'
+ + ' --dbname=postgresql://' + encodeURIComponent(parent.config.settings.postgres.user) + ":" + encodeURIComponent(parent.config.settings.postgres.password)
+ + "@" + parent.config.settings.postgres.host + ":" + parent.config.settings.postgres.port + "/" + encodeURIComponent(databaseName)
+ + ' > ' + ((parent.platform == 'win32') ? '\"nul\"' : '\"/dev/null\"');
+ const child_process = require('child_process');
+ child_process.exec(cmd, { cwd: backupPath }, function(error, stdout, stdin) {
+ if ((error != null) && (error != '')) {
+ func(1, "pg_dump error, backup will not be performed. Check path or use pgdumppath.");
+ return;
+ } else {parent.config.settings.autobackup.backupintervalhours = backupInterval;}
+ });
+ } else {
+ //all ok, enable backup
+ parent.config.settings.autobackup.backupintervalhours = backupInterval;}
+ }
+
+ // MongoDB pending bulk read operation, perform fast bulk document reads.
+ function fileBulkReadCompleted(err, docs) {
+ // Send out callbacks with results
+ if (docs != null) {
+ for (var i in docs) {
+ if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); }
+ const id = docs[i]._id;
+ if (obj.filePendingGets[id] != null) {
+ for (var j in obj.filePendingGets[id]) {
+ if (typeof obj.filePendingGets[id][j] == 'function') { obj.filePendingGets[id][j](err, performTypedRecordDecrypt([docs[i]])); }
+ }
+ delete obj.filePendingGets[id];
+ }
+ }
+ }
+
+ // If there are not results, send out a null callback
+ for (var i in obj.filePendingGets) { for (var j in obj.filePendingGets[i]) { obj.filePendingGets[i][j](err, []); } }
+
+ // Move on to process any more pending get operations
+ obj.filePendingGets = obj.filePendingGet;
+ obj.filePendingGet = null;
+ if (obj.filePendingGets != null) {
+ var findlist = [];
+ for (var i in obj.filePendingGets) { findlist.push(i); }
+ obj.file.find({ _id: { $in: findlist } }).toArray(fileBulkReadCompleted);
+ }
+ }
+
+ // MongoDB pending bulk remove operation, perform fast bulk document removes.
+ function fileBulkRemoveCompleted(err) {
+ // Send out callbacks
+ for (var i in obj.filePendingRemoves) {
+ for (var j in obj.filePendingRemoves[i]) {
+ if (typeof obj.filePendingRemoves[i][j] == 'function') { obj.filePendingRemoves[i][j](err); }
+ }
+ }
+
+ // Move on to process any more pending get operations
+ obj.filePendingRemoves = obj.filePendingRemove;
+ obj.filePendingRemove = null;
+ if (obj.filePendingRemoves != null) {
+ obj.dbCounters.fileRemoveBulk++;
+ var findlist = [], count = 0;
+ for (var i in obj.filePendingRemoves) { findlist.push(i); count++; }
+ obj.file.deleteMany({ _id: { $in: findlist } }, { multi: true }, fileBulkRemoveCompleted);
+ }
+ }
+
+ // MongoDB pending bulk write operation, perform fast bulk document replacement.
+ function fileBulkWriteCompleted() {
+ // Callbacks
+ if (obj.filePendingCbs != null) {
+ for (var i in obj.filePendingCbs) { if (typeof obj.filePendingCbs[i] == 'function') { obj.filePendingCbs[i](); } }
+ obj.filePendingCbs = null;
+ }
+ if (obj.filePendingSets != null) {
+ // Perform pending operations
+ obj.dbCounters.fileSetBulk++;
+ var ops = [];
+ obj.filePendingCbs = obj.filePendingCb;
+ obj.filePendingCb = null;
+ for (var i in obj.filePendingSets) { ops.push({ replaceOne: { filter: { _id: i }, replacement: performTypedRecordEncrypt(common.escapeLinksFieldNameEx(obj.filePendingSets[i])), upsert: true } }); }
+ obj.file.bulkWrite(ops, fileBulkWriteCompleted);
+ obj.filePendingSets = null;
+ } else {
+ // All done, no pending operations.
+ obj.filePendingSet = false;
+ }
+ }
+
+ // MongoDB pending bulk write operation, perform fast bulk document replacement.
+ function eventsFileBulkWriteCompleted() {
+ // Callbacks
+ if (obj.eventsFilePendingCbs != null) { for (var i in obj.eventsFilePendingCbs) { obj.eventsFilePendingCbs[i](); } obj.eventsFilePendingCbs = null; }
+ if (obj.eventsFilePendingSets != null) {
+ // Perform pending operations
+ obj.dbCounters.eventsSetBulk++;
+ var ops = [];
+ for (var i in obj.eventsFilePendingSets) { ops.push({ insertOne: { document: obj.eventsFilePendingSets[i] } }); }
+ obj.eventsFilePendingCbs = obj.eventsFilePendingCb;
+ obj.eventsFilePendingCb = null;
+ obj.eventsFilePendingSets = null;
+ obj.eventsfile.bulkWrite(ops, eventsFileBulkWriteCompleted);
+ } else {
+ // All done, no pending operations.
+ obj.eventsFilePendingSet = false;
+ }
+ }
+
+ // MongoDB pending bulk write operation, perform fast bulk document replacement.
+ function powerFileBulkWriteCompleted() {
+ // Callbacks
+ if (obj.powerFilePendingCbs != null) { for (var i in obj.powerFilePendingCbs) { obj.powerFilePendingCbs[i](); } obj.powerFilePendingCbs = null; }
+ if (obj.powerFilePendingSets != null) {
+ // Perform pending operations
+ obj.dbCounters.powerSetBulk++;
+ var ops = [];
+ for (var i in obj.powerFilePendingSets) { ops.push({ insertOne: { document: obj.powerFilePendingSets[i] } }); }
+ obj.powerFilePendingCbs = obj.powerFilePendingCb;
+ obj.powerFilePendingCb = null;
+ obj.powerFilePendingSets = null;
+ obj.powerfile.bulkWrite(ops, powerFileBulkWriteCompleted);
+ } else {
+ // All done, no pending operations.
+ obj.powerFilePendingSet = false;
+ }
+ }
+
+ // Perform a server backup
+ obj.performBackup = function (func) {
+ parent.debug('backup','Entering performBackup');
+ try {
+ if (obj.performingBackup) return 'Backup alreay in progress.';
+ if (parent.config.settings.autobackup.backupintervalhours == -1) { if (func) { func('Backup disabled.'); return 'Backup disabled.' }};
+ obj.performingBackup = true;
+ let backupPath = parent.backuppath;
+ let dataPath = parent.datapath;
+
+ const currentDate = new Date();
+ const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2);
+ obj.newAutoBackupFile = path.join(backupPath, parent.config.settings.autobackup.backupname + fileSuffix + '.zip');
+ parent.debug('backup','newAutoBackupFile=' + obj.newAutoBackupFile);
+
+ if ((obj.databaseType == DB_MONGOJS) || (obj.databaseType == DB_MONGODB)) {
+ // Perform a MongoDump
+ const dbname = (parent.args.mongodbname) ? (parent.args.mongodbname) : 'meshcentral';
+ const dburl = parent.args.mongodb;
+
+ obj.newDBDumpFile = path.join(backupPath, (dbname + '-mongodump-' + fileSuffix + '.archive'));
+
+ var cmd = buildMongoDumpCommand();
+ cmd += (dburl) ? ' --archive=\"' + obj.newDBDumpFile + '\"' :
+ ' --db=\"' + dbname + '\" --archive=\"' + obj.newDBDumpFile + '\"';
+ parent.debug('backup','Mongodump cmd: ' + cmd);
+ const child_process = require('child_process');
+ const dumpProcess = child_process.exec(
+ cmd,
+ { cwd: parent.parentpath },
+ (error)=> {if (error) {obj.backupStatus |= BACKUPFAIL_DBDUMP; console.error('ERROR: Unable to perform MongoDB backup: ' + error + '\r\n'); obj.createBackupfile(func);}}
+ );
+
+ dumpProcess.on('exit', (code) => {
+ if (code != 0) {console.log(`Mongodump child process exited with code ${code}`); obj.backupStatus |= BACKUPFAIL_DBDUMP;}
+ obj.createBackupfile(func);
+ });
+
+ } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
+ // Perform a MySqlDump backup
+ const newBackupFile = 'mysqldump-' + fileSuffix;
+ obj.newDBDumpFile = path.join(backupPath, newBackupFile + '.sql');
+
+ var cmd = buildSqlDumpCommand();
+ cmd += ' --result-file=\"' + obj.newDBDumpFile + '\"';
+ parent.debug('backup','Maria/MySQLdump cmd: ' + cmd);
+
+ const child_process = require('child_process');
+ const dumpProcess = child_process.exec(
+ cmd,
+ { cwd: parent.parentpath },
+ (error)=> {if (error) {obj.backupStatus |= BACKUPFAIL_DBDUMP; console.error('ERROR: Unable to perform MySQL backup: ' + error + '\r\n'); obj.createBackupfile(func);}}
+ );
+ dumpProcess.on('exit', (code) => {
+ if (code != 0) {console.error(`MySQLdump child process exited with code ${code}`); obj.backupStatus |= BACKUPFAIL_DBDUMP;}
+ obj.createBackupfile(func);
+ });
+
+ } else if (obj.databaseType == DB_SQLITE) {
+ //.db3 suffix to escape escape backupfile glob to exclude the sqlite db files
+ obj.newDBDumpFile = path.join(backupPath, databaseName + '-sqlitedump-' + fileSuffix + '.db3');
+ // do a VACUUM INTO in favor of the backup API to compress the export, see https://www.sqlite.org/backup.html
+ parent.debug('backup','SQLitedump: VACUUM INTO ' + obj.newDBDumpFile);
+ obj.file.exec('VACUUM INTO \'' + obj.newDBDumpFile + '\'', function (err) {
+ if (err) { console.error('SQLite backup error: ' + err); obj.backupStatus |=BACKUPFAIL_DBDUMP;};
+ //always finish/clean up
+ obj.createBackupfile(func);
+ });
+ } else if (obj.databaseType == DB_POSTGRESQL) {
+ // Perform a PostgresDump backup
+ const newBackupFile = 'pgdump-' + fileSuffix + '.sql';
+ obj.newDBDumpFile = path.join(backupPath, newBackupFile);
+ let cmd = '"' + parent.config.settings.autobackup.pgdumppath + '"'
+ + ' --dbname=postgresql://' + encodeURIComponent(parent.config.settings.postgres.user) + ":" + encodeURIComponent(parent.config.settings.postgres.password)
+ + "@" + parent.config.settings.postgres.host + ":" + parent.config.settings.postgres.port + "/" + encodeURIComponent(databaseName)
+ + " --file=" + obj.newDBDumpFile;
+ parent.debug('backup','Postgresqldump cmd: ' + cmd);
+ const child_process = require('child_process');
+ const dumpProcess = child_process.exec(
+ cmd,
+ { cwd: dataPath },
+ (error)=> {if (error) {obj.backupStatus |= BACKUPFAIL_DBDUMP; console.log('ERROR: Unable to perform PostgreSQL dump: ' + error.message + '\r\n'); obj.createBackupfile(func);}}
+ );
+ dumpProcess.on('exit', (code) => {
+ if (code != 0) {console.log(`PostgreSQLdump child process exited with code: ` + code); obj.backupStatus |= BACKUPFAIL_DBDUMP;}
+ obj.createBackupfile(func);
+ });
+ } else {
+ // NeDB/Acebase backup, no db dump needed, just make a file backup
+ obj.createBackupfile(func);
+ }
+ } catch (ex) { console.error(ex); parent.addServerWarning( 'Something went wrong during performBackup, check errorlog: ' +ex.message, true); };
+ return 'Starting auto-backup...';
+ };
+
+ obj.createBackupfile = function(func) {
+ parent.debug('backup', 'Entering createBackupfile');
+ let archiver = require('archiver');
+ let archive = null;
+ let zipLevel = Math.min(Math.max(Number(parent.config.settings.autobackup.zipcompression ? parent.config.settings.autobackup.zipcompression : 5),1),9);
+
+ //if password defined, create encrypted zip
+ if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.zippassword == 'string')) {
+ try {
+ //Only register format once, otherwise it triggers an error
+ if (archiver.isRegisteredFormat('zip-encrypted') == false) { archiver.registerFormat('zip-encrypted', require('archiver-zip-encrypted')); }
+ archive = archiver.create('zip-encrypted', { zlib: { level: zipLevel }, encryptionMethod: 'aes256', password: parent.config.settings.autobackup.zippassword });
+ if (func) { func('Creating encrypted ZIP'); }
+ } catch (ex) { // registering encryption failed, do not fall back to non-encrypted, fail backup and skip old backup removal as a precaution to not lose any backups
+ obj.backupStatus |= BACKUPFAIL_ZIPMODULE;
+ if (func) { func('Zipencryptionmodule failed, aborting');}
+ console.error('Zipencryptionmodule failed, aborting');
+ }
+ } else {
+ if (func) { func('Creating a NON-ENCRYPTED ZIP'); }
+ archive = archiver('zip', { zlib: { level: zipLevel } });
+ }
+
+ //original behavior, just a filebackup if dbdump fails : (obj.backupStatus == 0 || obj.backupStatus == BACKUPFAIL_DBDUMP)
+ if (obj.backupStatus == 0) {
+ // Zip the data directory with the dbdump|NeDB files
+ let output = fs.createWriteStream(obj.newAutoBackupFile);
+
+ // Archive finalized and closed
+ output.on('close', function () {
+ if (obj.backupStatus == 0) {
+ let mesg = 'Auto-backup completed: ' + obj.newAutoBackupFile + ', backup-size: ' + ((archive.pointer() / 1048576).toFixed(2)) + "Mb";
+ console.log(mesg);
+ if (func) { func(mesg); };
+ obj.performCloudBackup(obj.newAutoBackupFile, func);
+ obj.removeExpiredBackupfiles(func);
+
+ } else {
+ let mesg = 'Zipbackup failed (' + obj.backupStatus.toString(2).slice(-8) + '), deleting incomplete backup: ' + obj.newAutoBackupFile;
+ if (func) { func(mesg) }
+ else { parent.addServerWarning(mesg, true ) };
+ if (fs.existsSync(obj.newAutoBackupFile)) { fs.unlink(obj.newAutoBackupFile, function (err) { if (err) {console.error('Failed to clean up backupfile: ' + err.message)} }) };
+ };
+ if (obj.databaseType != DB_NEDB) {
+ //remove dump archive file, because zipped and otherwise fills up
+ if (fs.existsSync(obj.newDBDumpFile)) { fs.unlink(obj.newDBDumpFile, function (err) { if (err) {console.error('Failed to clean up dbdump file: ' + err.message) } }) };
+ };
+ obj.performingBackup = false;
+ obj.backupStatus = 0x0;
+ }
+ );
+ output.on('end', function () { });
+ output.on('error', function (err) {
+ if ((obj.backupStatus & BACKUPFAIL_ZIPCREATE) == 0) {
+ console.error('Output error: ' + err.message);
+ if (func) { func('Output error: ' + err.message); };
+ obj.backupStatus |= BACKUPFAIL_ZIPCREATE;
+ archive.abort();
+ };
+ });
+ archive.on('warning', function (err) {
+ //if files added to the archiver object aren't reachable anymore (e.g. sqlite-journal files)
+ //an ENOENT warning is given, but the archiver module has no option to/does not skip/resume
+ //so the backup needs te be aborted as it otherwise leaves an incomplete zip and never 'ends'
+ if ((obj.backupStatus & BACKUPFAIL_ZIPCREATE) == 0) {
+ console.log('Zip warning: ' + err.message);
+ if (func) { func('Zip warning: ' + err.message); };
+ obj.backupStatus |= BACKUPFAIL_ZIPCREATE;
+ archive.abort();
+ };
+ });
+ archive.on('error', function (err) {
+ if ((obj.backupStatus & BACKUPFAIL_ZIPCREATE) == 0) {
+ console.error('Zip error: ' + err.message);
+ if (func) { func('Zip error: ' + err.message); };
+ obj.backupStatus |= BACKUPFAIL_ZIPCREATE;
+ archive.abort();
+ }
+ });
+ archive.pipe(output);
+
+ let globIgnoreFiles;
+ //slice in case exclusion gets pushed
+ globIgnoreFiles = parent.config.settings.autobackup.backupignorefilesglob ? parent.config.settings.autobackup.backupignorefilesglob.slice() : [];
+ if (parent.config.settings.sqlite3) { globIgnoreFiles.push (datapathFoldername + '/' + databaseName + '.sqlite*'); }; //skip sqlite database file, and temp files with ext -journal, -wal & -shm
+ //archiver.glob doesn't seem to use the third param, archivesubdir. Bug?
+ //workaround: go up a dir and add data dir explicitly to keep the zip tidy
+ archive.glob((datapathFoldername + '/**'), {
+ cwd: datapathParentPath,
+ ignore: globIgnoreFiles,
+ skip: (parent.config.settings.autobackup.backupskipfoldersglob ? parent.config.settings.autobackup.backupskipfoldersglob : [])
+ });
+
+ if (parent.config.settings.autobackup.backupwebfolders) {
+ if (parent.webViewsOverridePath) { archive.directory(parent.webViewsOverridePath, 'meshcentral-views'); }
+ if (parent.webPublicOverridePath) { archive.directory(parent.webPublicOverridePath, 'meshcentral-public'); }
+ if (parent.webEmailsOverridePath) { archive.directory(parent.webEmailsOverridePath, 'meshcentral-emails'); }
+ };
+ if (parent.config.settings.autobackup.backupotherfolders) {
+ archive.directory(parent.filespath, 'meshcentral-files');
+ archive.directory(parent.recordpath, 'meshcentral-recordings');
+ };
+ //add dbdump to the root of the zip
+ if (obj.newDBDumpFile != null) archive.file(obj.newDBDumpFile, { name: path.basename(obj.newDBDumpFile) });
+ archive.finalize();
+ } else {
+ //failed somewhere before zipping
+ console.error('Backup failed ('+ obj.backupStatus.toString(2).slice(-8) + ')');
+ if (func) { func('Backup failed ('+ obj.backupStatus.toString(2).slice(-8) + ')') }
+ else {
+ parent.addServerWarning('Backup failed ('+ obj.backupStatus.toString(2).slice(-8) + ')', true);
+ }
+ //Just in case something's there
+ if (fs.existsSync(obj.newDBDumpFile)) { fs.unlink(obj.newDBDumpFile, function (err) { if (err) {console.error('Failed to clean up dbdump file: ' + err.message) } }); };
+ obj.backupStatus = 0x0;
+ obj.performingBackup = false;
+ };
+ };
+
+ // Remove expired backupfiles by filenamedate
+ obj.removeExpiredBackupfiles = function (func) {
+ if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.keeplastdaysbackup == 'number')) {
+ let cutoffDate = new Date();
+ cutoffDate.setDate(cutoffDate.getDate() - parent.config.settings.autobackup.keeplastdaysbackup);
+ fs.readdir(parent.backuppath, function (err, dir) {
+ try {
+ if (err == null) {
+ if (dir.length > 0) {
+ let fileName = parent.config.settings.autobackup.backupname;
+ let checked = 0;
+ let removed = 0;
+ for (var i in dir) {
+ var name = dir[i];
+ parent.debug('backup', "checking file: ", path.join(parent.backuppath, name));
+ if (name.startsWith(fileName) && name.endsWith('.zip')) {
+ var timex = name.substring(fileName.length, name.length - 4).split('-');
+ if (timex.length == 5) {
+ checked++;
+ var fileDate = new Date(parseInt(timex[0]), parseInt(timex[1]) - 1, parseInt(timex[2]), parseInt(timex[3]), parseInt(timex[4]));
+ if (fileDate && (cutoffDate > fileDate)) {
+ console.log("Removing expired backup file: ", path.join(parent.backuppath, name));
+ fs.unlink(path.join(parent.backuppath, name), function (err) { if (err) { console.error(err.message); if (func) {func('Error removing: ' + err.message); } } });
+ removed++;
+ }
+ }
+ else { parent.debug('backup', "file: " + name + " timestamp failure: ", timex); }
+ }
+ }
+ let mesg= 'Checked ' + checked + ' candidates in ' + parent.backuppath + '. Removed ' + removed + ' expired backupfiles using cutoffDate: '+ cutoffDate.toLocaleString('default', { dateStyle: 'short', timeStyle: 'short' });
+ parent.debug (mesg);
+ if (func) { func(mesg); }
+ } else { console.error('No files found in ' + parent.backuppath + '. There should be at least one.')}
+ }
+ else
+ { console.error(err); parent.addServerWarning( 'Reading files in backup directory ' + parent.backuppath + ' failed, check errorlog: ' + err.message, true); }
+ } catch (ex) { console.error(ex); parent.addServerWarning( 'Something went wrong during removeExpiredBackupfiles, check errorlog: ' +ex.message, true); }
+ });
+ }
+ }
+
+ async function webDAVBackup(filename, func) {
+ try {
+ const webDAV = await import ('webdav');
+ const wdConfig = parent.config.settings.autobackup.webdav;
+ const client = webDAV.createClient(wdConfig.url, {
+ username: wdConfig.username,
+ password: wdConfig.password,
+ maxContentLength: Infinity,
+ maxBodyLength: Infinity
+ });
+ if (await client.exists(wdConfig.foldername) === false) {
+ await client.createDirectory(wdConfig.foldername, { recursive: true});
+ } else {
+ // Clean up our WebDAV folder
+ if ((typeof wdConfig.maxfiles == 'number') && (wdConfig.maxfiles > 1)) {
+ const fileName = parent.config.settings.autobackup.backupname;
+ //only files matching our backupfilename
+ let files = await client.getDirectoryContents(wdConfig.foldername, { deep: false, glob: "/**/" + fileName + "*.zip" });
+ const xdateTimeSort = function (a, b) { if (a.xdate > b.xdate) return 1; if (a.xdate < b.xdate) return -1; return 0; }
+ for (const i in files) { files[i].xdate = new Date(files[i].lastmod); }
+ files.sort(xdateTimeSort);
+ while (files.length >= wdConfig.maxfiles) {
+ let delFile = files.shift().filename;
+ await client.deleteFile(delFile);
+ console.log('WebDAV file deleted: ' + delFile); if (func) { func('WebDAV file deleted: ' + delFile); }
+ }
+ }
+ }
+ // Upload to the WebDAV folder
+ const { pipeline } = require('stream/promises');
+ await pipeline(fs.createReadStream(filename), client.createWriteStream( wdConfig.foldername + path.basename(filename)));
+ console.log('WebDAV upload completed: ' + wdConfig.foldername + path.basename(filename)); if (func) { func('WebDAV upload completed: ' + wdConfig.foldername + path.basename(filename)); }
+ }
+ catch(err) {
+ console.error('WebDAV error: ' + err.message); if (func) { func('WebDAV error: ' + err.message);}
+ }
+ }
+
+ // Perform cloud backup
+ obj.performCloudBackup = function (filename, func) {
+ // WebDAV Backup
+ if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.webdav == 'object')) {
+ parent.debug( 'backup', 'Entering WebDAV backup'); if (func) { func('Entering WebDAV backup.'); }
+ webDAVBackup(filename, func);
+ }
+
+ // Google Drive Backup
+ if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.googledrive == 'object')) {
+ parent.debug( 'backup', 'Entering Google Drive backup');
+ obj.Get('GoogleDriveBackup', function (err, docs) {
+ if ((err != null) || (docs.length != 1) || (docs[0].state != 3)) return;
+ if (func) { func('Attempting Google Drive upload...'); }
+ const {google} = require('googleapis');
+ const oAuth2Client = new google.auth.OAuth2(docs[0].clientid, docs[0].clientsecret, "urn:ietf:wg:oauth:2.0:oob");
+ oAuth2Client.on('tokens', function (tokens) { if (tokens.refresh_token) { docs[0].token = tokens.refresh_token; parent.db.Set(docs[0]); } }); // Update the token in the database
+ oAuth2Client.setCredentials(docs[0].token);
+ const drive = google.drive({ version: 'v3', auth: oAuth2Client });
+ const createdTimeSort = function (a, b) { if (a.createdTime > b.createdTime) return 1; if (a.createdTime < b.createdTime) return -1; return 0; }
+
+ // Called once we know our folder id, clean up and upload a backup.
+ var useGoogleDrive = function (folderid) {
+ // List files to see if we need to delete older ones
+ if (typeof parent.config.settings.autobackup.googledrive.maxfiles == 'number') {
+ drive.files.list({
+ q: 'trashed = false and \'' + folderid + '\' in parents',
+ fields: 'nextPageToken, files(id, name, size, createdTime)',
+ }, function (err, res) {
+ if (err) {
+ console.log('GoogleDrive (files.list) error: ' + err);
+ if (func) { func('GoogleDrive (files.list) error: ' + err); }
+ return;
+ }
+ // Delete any old files if more than 10 files are present in the backup folder.
+ res.data.files.sort(createdTimeSort);
+ while (res.data.files.length >= parent.config.settings.autobackup.googledrive.maxfiles) { drive.files.delete({ fileId: res.data.files.shift().id }, function (err, res) { }); }
+ });
+ }
+
+ //console.log('Uploading...');
+ if (func) { func('Uploading to Google Drive...'); }
+
+ // Upload the backup
+ drive.files.create({
+ requestBody: { name: require('path').basename(filename), mimeType: 'text/plain', parents: [folderid] },
+ media: { mimeType: 'application/zip', body: require('fs').createReadStream(filename) },
+ }, function (err, res) {
+ if (err) {
+ console.log('GoogleDrive (files.create) error: ' + err);
+ if (func) { func('GoogleDrive (files.create) error: ' + err); }
+ return;
+ }
+ //console.log('Upload done.');
+ if (func) { func('Google Drive upload completed.'); }
+ });
+ }
+
+ // Fetch the folder name
+ var folderName = 'MeshCentral-Backups';
+ if (typeof parent.config.settings.autobackup.googledrive.foldername == 'string') { folderName = parent.config.settings.autobackup.googledrive.foldername; }
+
+ // Find our backup folder, create one if needed.
+ drive.files.list({
+ q: 'mimeType = \'application/vnd.google-apps.folder\' and name=\'' + folderName + '\' and trashed = false',
+ fields: 'nextPageToken, files(id, name)',
+ }, function (err, res) {
+ if (err) {
+ console.log('GoogleDrive error: ' + err);
+ if (func) { func('GoogleDrive error: ' + err); }
+ return;
+ }
+ if (res.data.files.length == 0) {
+ // Create a folder
+ drive.files.create({ resource: { 'name': folderName, 'mimeType': 'application/vnd.google-apps.folder' }, fields: 'id' }, function (err, file) {
+ if (err) {
+ console.log('GoogleDrive (folder.create) error: ' + err);
+ if (func) { func('GoogleDrive (folder.create) error: ' + err); }
+ return;
+ }
+ useGoogleDrive(file.data.id);
+ });
+ } else { useGoogleDrive(res.data.files[0].id); }
+ });
+ });
+ }
+
+ // S3 Backup
+ if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.s3 == 'object')) {
+ parent.debug( 'backup', 'Entering S3 backup');
+ var s3folderName = 'MeshCentral-Backups';
+ if (typeof parent.config.settings.autobackup.s3.foldername == 'string') { s3folderName = parent.config.settings.autobackup.s3.foldername; }
+ // Construct the config object
+ var accessKey = parent.config.settings.autobackup.s3.accesskey,
+ secretKey = parent.config.settings.autobackup.s3.secretkey,
+ endpoint = parent.config.settings.autobackup.s3.endpoint ? parent.config.settings.autobackup.s3.endpoint : 's3.amazonaws.com',
+ port = parent.config.settings.autobackup.s3.port ? parent.config.settings.autobackup.s3.port : 443,
+ useSsl = parent.config.settings.autobackup.s3.ssl ? parent.config.settings.autobackup.s3.ssl : true,
+ bucketName = parent.config.settings.autobackup.s3.bucketname,
+ pathPrefix = s3folderName,
+ threshold = parent.config.settings.autobackup.s3.maxfiles ? parent.config.settings.autobackup.s3.maxfiles : 0,
+ fileToUpload = filename;
+ // Create a MinIO client
+ const Minio = require('minio');
+ var minioClient = new Minio.Client({
+ endPoint: endpoint,
+ port: port,
+ useSSL: useSsl,
+ accessKey: accessKey,
+ secretKey: secretKey
+ });
+ // List objects in the specified bucket and path prefix
+ var listObjectsPromise = new Promise(function(resolve, reject) {
+ var items = [];
+ var stream = minioClient.listObjects(bucketName, pathPrefix, true);
+ stream.on('data', function(item) {
+ if (!item.name.endsWith('/')) { // Exclude directories
+ items.push(item);
+ }
+ });
+ stream.on('end', function() {
+ resolve(items);
+ });
+ stream.on('error', function(err) {
+ reject(err);
+ });
+ });
+ listObjectsPromise.then(function(objects) {
+ // Count the number of files
+ var fileCount = objects.length;
+ // Return if no files to carry on uploading
+ if (fileCount === 0) { return Promise.resolve(); }
+ // Sort the files by LastModified date (oldest first)
+ objects.sort(function(a, b) { return new Date(a.lastModified) - new Date(b.lastModified); });
+ // Check if the threshold is zero and return if
+ if (threshold === 0) { return Promise.resolve(); }
+ // Check if the number of files exceeds the threshold (maxfiles) is 0
+ if (fileCount >= threshold) {
+ // Calculate how many files need to be deleted to make space for the new file
+ var filesToDelete = fileCount - threshold + 1; // +1 to make space for the new file
+ if (func) { func('Deleting ' + filesToDelete + ' older ' + (filesToDelete == 1 ? 'file' : 'files') + ' from S3 ...'); }
+ // Create an array of promises for deleting files
+ var deletePromises = objects.slice(0, filesToDelete).map(function(fileToDelete) {
+ return new Promise(function(resolve, reject) {
+ minioClient.removeObject(bucketName, fileToDelete.name, function(err) {
+ if (err) {
+ reject(err);
+ } else {
+ if (func) { func('Deleted file: ' + fileToDelete.name + ' from S3'); }
+ resolve();
+ }
+ });
+ });
+ });
+ // Wait for all deletions to complete
+ return Promise.all(deletePromises);
+ } else {
+ return Promise.resolve(); // No deletion needed
+ }
+ }).then(function() {
+ // Determine the upload path by combining the pathPrefix with the filename
+ var fileName = require('path').basename(fileToUpload);
+ var uploadPath = require('path').join(pathPrefix, fileName);
+ // Upload a new file
+ var uploadPromise = new Promise(function(resolve, reject) {
+ if (func) { func('Uploading file ' + uploadPath + ' to S3'); }
+ minioClient.fPutObject(bucketName, uploadPath, fileToUpload, function(err, etag) {
+ if (err) {
+ reject(err);
+ } else {
+ if (func) { func('Uploaded file: ' + uploadPath + ' to S3'); }
+ resolve(etag);
+ }
+ });
+ });
+ return uploadPromise;
+ }).catch(function(error) {
+ if (func) { func('Error managing files in S3: ' + error); }
+ });
+ }
+ }
+
+ // Transfer NeDB data into the current database
+ obj.nedbtodb = function (func) {
+ var nedbDatastore = null;
+ try { nedbDatastore = require('@seald-io/nedb'); } catch (ex) { } // This is the NeDB with Node 23 support.
+ if (nedbDatastore == null) {
+ try { nedbDatastore = require('@yetzt/nedb'); } catch (ex) { } // This is the NeDB with fixed security dependencies.
+ if (nedbDatastore == null) { nedbDatastore = require('nedb'); } // So not to break any existing installations, if the old NeDB is present, use it.
+ }
+
+ var datastoreOptions = { filename: parent.getConfigFilePath('meshcentral.db'), autoload: true };
+
+ // If a DB encryption key is provided, perform database encryption
+ if ((typeof parent.args.dbencryptkey == 'string') && (parent.args.dbencryptkey.length != 0)) {
+ // Hash the database password into a AES256 key and setup encryption and decryption.
+ var nedbKey = parent.crypto.createHash('sha384').update(parent.args.dbencryptkey).digest('raw').slice(0, 32);
+ datastoreOptions.afterSerialization = function (plaintext) {
+ const iv = parent.crypto.randomBytes(16);
+ const aes = parent.crypto.createCipheriv('aes-256-cbc', nedbKey, iv);
+ var ciphertext = aes.update(plaintext);
+ ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
+ return ciphertext.toString('base64');
+ }
+ datastoreOptions.beforeDeserialization = function (ciphertext) {
+ const ciphertextBytes = Buffer.from(ciphertext, 'base64');
+ const iv = ciphertextBytes.slice(0, 16);
+ const data = ciphertextBytes.slice(16);
+ const aes = parent.crypto.createDecipheriv('aes-256-cbc', nedbKey, iv);
+ var plaintextBytes = Buffer.from(aes.update(data));
+ plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
+ return plaintextBytes.toString();
+ }
+ }
+
+ // Setup all NeDB collections
+ var nedbfile = new nedbDatastore(datastoreOptions);
+ var nedbeventsfile = new nedbDatastore({ filename: parent.getConfigFilePath('meshcentral-events.db'), autoload: true, corruptAlertThreshold: 1 });
+ var nedbpowerfile = new nedbDatastore({ filename: parent.getConfigFilePath('meshcentral-power.db'), autoload: true, corruptAlertThreshold: 1 });
+ var nedbserverstatsfile = new nedbDatastore({ filename: parent.getConfigFilePath('meshcentral-stats.db'), autoload: true, corruptAlertThreshold: 1 });
+
+ // Transfered record counts
+ var normalRecordsTransferCount = 0;
+ var eventRecordsTransferCount = 0;
+ var powerRecordsTransferCount = 0;
+ var statsRecordsTransferCount = 0;
+ obj.pendingTransfer = 0;
+
+ // Transfer the data from main database
+ nedbfile.find({}, function (err, docs) {
+ if ((err == null) && (docs.length > 0)) {
+ performTypedRecordDecrypt(docs)
+ for (var i in docs) {
+ obj.pendingTransfer++;
+ normalRecordsTransferCount++;
+ obj.Set(common.unEscapeLinksFieldName(docs[i]), function () { obj.pendingTransfer--; });
+ }
+ }
+
+ // Transfer events
+ nedbeventsfile.find({}, function (err, docs) {
+ if ((err == null) && (docs.length > 0)) {
+ for (var i in docs) {
+ obj.pendingTransfer++;
+ eventRecordsTransferCount++;
+ obj.StoreEvent(docs[i], function () { obj.pendingTransfer--; });
+ }
+ }
+
+ // Transfer power events
+ nedbpowerfile.find({}, function (err, docs) {
+ if ((err == null) && (docs.length > 0)) {
+ for (var i in docs) {
+ obj.pendingTransfer++;
+ powerRecordsTransferCount++;
+ obj.storePowerEvent(docs[i], null, function () { obj.pendingTransfer--; });
+ }
+ }
+
+ // Transfer server stats
+ nedbserverstatsfile.find({}, function (err, docs) {
+ if ((err == null) && (docs.length > 0)) {
+ for (var i in docs) {
+ obj.pendingTransfer++;
+ statsRecordsTransferCount++;
+ obj.SetServerStats(docs[i], function () { obj.pendingTransfer--; });
+ }
+ }
+
+ // Only exit when all the records are stored.
+ setInterval(function () {
+ if (obj.pendingTransfer == 0) { func("Done. " + normalRecordsTransferCount + " record(s), " + eventRecordsTransferCount + " event(s), " + powerRecordsTransferCount + " power change(s), " + statsRecordsTransferCount + " stat(s)."); }
+ }, 200)
+ });
+ });
+ });
+ });
+ }
+
+ function padNumber(number, digits) { return Array(Math.max(digits - String(number).length + 1, 0)).join(0) + number; }
+
+ // Called when a node has changed
+ function dbNodeChange(nodeChange, added) {
+ if (parent.webserver == null) return;
+ common.unEscapeLinksFieldName(nodeChange.fullDocument);
+ const node = performTypedRecordDecrypt([nodeChange.fullDocument])[0];
+ parent.DispatchEvent(['*', node.meshid], obj, { etype: 'node', action: (added ? 'addnode' : 'changenode'), node: parent.webserver.CloneSafeNode(node), nodeid: node._id, domain: node.domain, nolog: 1 });
+ }
+
+ // Called when a device group has changed
+ function dbMeshChange(meshChange, added) {
+ if (parent.webserver == null) return;
+ common.unEscapeLinksFieldName(meshChange.fullDocument);
+ const mesh = performTypedRecordDecrypt([meshChange.fullDocument])[0];
+
+ // Update the mesh object in memory
+ const mmesh = parent.webserver.meshes[mesh._id];
+ if (mmesh != null) {
+ // Update an existing device group
+ for (var i in mesh) { mmesh[i] = mesh[i]; }
+ for (var i in mmesh) { if (mesh[i] == null) { delete mmesh[i]; } }
+ } else {
+ // Device group not present, create it.
+ parent.webserver.meshes[mesh._id] = mesh;
+ }
+
+ // Send the mesh update
+ var mesh2 = Object.assign({}, mesh); // Shallow clone
+ if (mesh2.deleted) { mesh2.action = 'deletemesh'; } else { mesh2.action = (added ? 'createmesh' : 'meshchange'); }
+ mesh2.meshid = mesh2._id;
+ mesh2.nolog = 1;
+ delete mesh2.type;
+ delete mesh2._id;
+ parent.DispatchEvent(['*', mesh2.meshid], obj, parent.webserver.CloneSafeMesh(mesh2));
+ }
+
+ // Called when a user account has changed
+ function dbUserChange(userChange, added) {
+ if (parent.webserver == null) return;
+ common.unEscapeLinksFieldName(userChange.fullDocument);
+ const user = performTypedRecordDecrypt([userChange.fullDocument])[0];
+
+ // Update the user object in memory
+ const muser = parent.webserver.users[user._id];
+ if (muser != null) {
+ // Update an existing user
+ for (var i in user) { muser[i] = user[i]; }
+ for (var i in muser) { if (user[i] == null) { delete muser[i]; } }
+ } else {
+ // User not present, create it.
+ parent.webserver.users[user._id] = user;
+ }
+
+ // Send the user update
+ var targets = ['*', 'server-users', user._id];
+ if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
+ parent.DispatchEvent(targets, obj, { etype: 'user', userid: user._id, username: user.name, account: parent.webserver.CloneSafeUser(user), action: (added ? 'accountcreate' : 'accountchange'), domain: user.domain, nolog: 1 });
+ }
+
+ // Called when a user group has changed
+ function dbUGrpChange(ugrpChange, added) {
+ if (parent.webserver == null) return;
+ common.unEscapeLinksFieldName(ugrpChange.fullDocument);
+ const usergroup = ugrpChange.fullDocument;
+
+ // Update the user group object in memory
+ const uusergroup = parent.webserver.userGroups[usergroup._id];
+ if (uusergroup != null) {
+ // Update an existing user group
+ for (var i in usergroup) { uusergroup[i] = usergroup[i]; }
+ for (var i in uusergroup) { if (usergroup[i] == null) { delete uusergroup[i]; } }
+ } else {
+ // Usergroup not present, create it.
+ parent.webserver.userGroups[usergroup._id] = usergroup;
+ }
+
+ // Send the user group update
+ var usergroup2 = Object.assign({}, usergroup); // Shallow clone
+ usergroup2.action = (added ? 'createusergroup' : 'usergroupchange');
+ usergroup2.ugrpid = usergroup2._id;
+ usergroup2.nolog = 1;
+ delete usergroup2.type;
+ delete usergroup2._id;
+ parent.DispatchEvent(['*', usergroup2.ugrpid], obj, usergroup2);
+ }
+
+ function dbMergeSqlArray(arr) {
+ var x = '';
+ for (var i in arr) { if (x != '') { x += ','; } x += '\'' + arr[i] + '\''; }
+ return x;
+ }
+
+ return obj;
+};
From 09800f7f7e9ff3610ce5921fafc80de25727c620 Mon Sep 17 00:00:00 2001
From: stephannn <38951729+stephannn@users.noreply.github.com>
Date: Mon, 13 Oct 2025 15:29:42 +0200
Subject: [PATCH 4/8] Add files via upload
---
meshcentral.js | 8871 ++++++++++++++++++++++++------------------------
1 file changed, 4436 insertions(+), 4435 deletions(-)
diff --git a/meshcentral.js b/meshcentral.js
index 3c770582df..4566053047 100644
--- a/meshcentral.js
+++ b/meshcentral.js
@@ -1,4435 +1,4436 @@
-/**
-* @description MeshCentral main module
-* @author Ylian Saint-Hilaire
-* @copyright Intel Corporation 2018-2022
-* @license Apache-2.0
-* @version v0.0.1
-*/
-
-/*xjslint node: true */
-/*xjslint plusplus: true */
-/*xjslint maxlen: 256 */
-/*jshint node: true */
-/*jshint strict: false */
-/*jshint esversion: 6 */
-"use strict";
-
-const common = require('./common.js');
-
-// If app metrics is available
-if (process.argv[2] == '--launch') { try { require('appmetrics-dash').monitor({ url: '/', title: 'MeshCentral', port: 88, host: '127.0.0.1' }); } catch (ex) { } }
-
-function CreateMeshCentralServer(config, args) {
- const obj = {};
- obj.db = null;
- obj.webserver = null; // HTTPS main web server, typically on port 443
- obj.redirserver = null; // HTTP relay web server, typically on port 80
- obj.mpsserver = null; // Intel AMT CIRA server, typically on port 4433
- obj.mqttbroker = null; // MQTT server, not is not often used
- obj.swarmserver = null; // Swarm server, this is used only to update older MeshCentral v1 agents
- obj.smsserver = null; // SMS server, used to send user SMS messages
- obj.msgserver = null; // Messaging server, used to sent used messages
- obj.amtEventHandler = null;
- obj.pluginHandler = null;
- obj.amtScanner = null;
- obj.amtManager = null; // Intel AMT manager, used to oversee all Intel AMT devices, activate them and sync policies
- obj.meshScanner = null;
- obj.taskManager = null;
- obj.letsencrypt = null; // Let's encrypt server, used to get and renew TLS certificates
- obj.eventsDispatch = {};
- obj.fs = require('fs');
- obj.path = require('path');
- obj.crypto = require('crypto');
- obj.exeHandler = require('./exeHandler.js');
- obj.platform = require('os').platform();
- obj.args = args;
- obj.common = common;
- obj.configurationFiles = null;
- obj.certificates = null;
- obj.connectivityByNode = {}; // This object keeps a list of all connected CIRA and agents, by nodeid->value (value: 1 = Agent, 2 = CIRA, 4 = AmtDirect)
- obj.peerConnectivityByNode = {}; // This object keeps a list of all connected CIRA and agents of peers, by serverid->nodeid->value (value: 1 = Agent, 2 = CIRA, 4 = AmtDirect)
- obj.debugSources = [];
- obj.debugRemoteSources = null;
- obj.config = config; // Configuration file
- obj.dbconfig = {}; // Persistance values, loaded from database
- obj.certificateOperations = null;
- obj.defaultMeshCmd = null;
- obj.defaultMeshCores = {};
- obj.defaultMeshCoresDeflate = {};
- obj.defaultMeshCoresHash = {};
- obj.meshToolsBinaries = {}; // Mesh Tools Binaries, ToolName --> { hash:(sha384 hash), size:(binary size), path:(binary path) }
- obj.meshAgentBinaries = {}; // Mesh Agent Binaries, Architecture type --> { hash:(sha384 hash), size:(binary size), path:(binary path) }
- obj.meshAgentInstallScripts = {}; // Mesh Install Scripts, Script ID -- { hash:(sha384 hash), size:(binary size), path:(binary path) }
- obj.multiServer = null;
- obj.ipKvmManager = null;
- obj.maintenanceTimer = null;
- obj.serverId = null;
- obj.serverKey = Buffer.from(obj.crypto.randomBytes(48), 'binary');
- obj.loginCookieEncryptionKey = null;
- obj.invitationLinkEncryptionKey = null;
- obj.serverSelfWriteAllowed = true;
- obj.serverStatsCounter = Math.floor(Math.random() * 1000);
- obj.taskLimiter = obj.common.createTaskLimiterQueue(50, 20, 60); // (maxTasks, maxTaskTime, cleaningInterval) This is a task limiter queue to smooth out server work.
- obj.agentUpdateBlockSize = 65531; // MeshAgent update block size
- obj.serverWarnings = []; // List of warnings that should be shown to administrators
- obj.cookieUseOnceTable = {}; // List of cookies that are already expired
- obj.cookieUseOnceTableCleanCounter = 0; // Clean the cookieUseOnceTable each 20 additions
- obj.firstStats = true; // True until this server saves it's not stats to the database
-
- // Server version
- obj.currentVer = null;
- function getCurrentVersion() { try { obj.currentVer = JSON.parse(obj.fs.readFileSync(obj.path.join(__dirname, 'package.json'), 'utf8')).version; } catch (ex) { } return obj.currentVer; } // Fetch server version
- getCurrentVersion();
-
- // Setup the default configuration and files paths
- if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) {
- obj.parentpath = obj.path.join(__dirname, '../..');
- obj.datapath = obj.path.join(__dirname, '../../meshcentral-data');
- obj.filespath = obj.path.join(__dirname, '../../meshcentral-files');
- obj.backuppath = obj.path.join(__dirname, '../../meshcentral-backups');
- obj.recordpath = obj.path.join(__dirname, '../../meshcentral-recordings');
- obj.webViewsPath = obj.path.join(__dirname, 'views');
- obj.webPublicPath = obj.path.join(__dirname, 'public');
- obj.webEmailsPath = obj.path.join(__dirname, 'emails');
- if (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web/views'))) { obj.webViewsOverridePath = obj.path.join(__dirname, '../../meshcentral-web/views'); }
- if (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web/public'))) { obj.webPublicOverridePath = obj.path.join(__dirname, '../../meshcentral-web/public'); }
- if (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web/emails'))) { obj.webEmailsOverridePath = obj.path.join(__dirname, '../../meshcentral-web/emails'); }
- } else {
- obj.parentpath = __dirname;
- obj.datapath = obj.path.join(__dirname, '../meshcentral-data');
- obj.filespath = obj.path.join(__dirname, '../meshcentral-files');
- obj.backuppath = obj.path.join(__dirname, '../meshcentral-backups');
- obj.recordpath = obj.path.join(__dirname, '../meshcentral-recordings');
- obj.webViewsPath = obj.path.join(__dirname, 'views');
- obj.webPublicPath = obj.path.join(__dirname, 'public');
- obj.webEmailsPath = obj.path.join(__dirname, 'emails');
- if (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web/views'))) { obj.webViewsOverridePath = obj.path.join(__dirname, '../meshcentral-web/views'); }
- if (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web/public'))) { obj.webPublicOverridePath = obj.path.join(__dirname, '../meshcentral-web/public'); }
- if (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web/emails'))) { obj.webEmailsOverridePath = obj.path.join(__dirname, '../meshcentral-web/emails'); }
- }
-
- // Clean up any temporary files
- const removeTime = new Date(Date.now()).getTime() - (30 * 60 * 1000); // 30 minutes
- const dir = obj.fs.readdir(obj.path.join(obj.filespath, 'tmp'), function (err, files) {
- if (err != null) return;
- for (var i in files) { try { const filepath = obj.path.join(obj.filespath, 'tmp', files[i]); if (obj.fs.statSync(filepath).mtime.getTime() < removeTime) { obj.fs.unlink(filepath, function () { }); } } catch (ex) { } }
- });
-
- // Look to see if data and/or file path is specified
- if (obj.config.settings && (typeof obj.config.settings.datapath == 'string')) { obj.datapath = obj.config.settings.datapath; }
- if (obj.config.settings && (typeof obj.config.settings.filespath == 'string')) { obj.filespath = obj.config.settings.filespath; }
-
- // Create data and files folders if needed
- try { obj.fs.mkdirSync(obj.datapath); } catch (ex) { }
- try { obj.fs.mkdirSync(obj.filespath); } catch (ex) { }
-
- // Windows Specific Code, setup service and event log
- obj.service = null;
- obj.servicelog = null;
- if (obj.platform == 'win32') {
- const nodewindows = require('node-windows');
- obj.service = nodewindows.Service;
- const eventlogger = nodewindows.EventLogger;
- obj.servicelog = new eventlogger('MeshCentral');
- }
-
- // Start the Meshcentral server
- obj.Start = function () {
- var i;
- try { require('./pass').hash('test', function () { }, 0); } catch (ex) { console.log('Old version of node, must upgrade.'); return; } // TODO: Not sure if this test works or not.
-
- // Check for invalid arguments
- const validArguments = ['_', 'user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'rediraliasport', 'cert', 'mpscert', 'deletedomain', 'deletedefaultdomain', 'showall', 'showusers', 'showitem', 'listuserids', 'showusergroups', 'shownodes', 'showallmeshes', 'showmeshes', 'showevents', 'showsmbios', 'showpower', 'clearpower', 'showiplocations', 'help', 'exactports', 'xinstall', 'xuninstall', 'install', 'uninstall', 'start', 'stop', 'restart', 'debug', 'filespath', 'datapath', 'noagentupdate', 'launch', 'noserverbackup', 'mongodb', 'mongodbcol', 'wanonly', 'lanonly', 'nousers', 'mpspass', 'ciralocalfqdn', 'dbexport', 'dbexportmin', 'dbimport', 'dbmerge', 'dbfix', 'dbencryptkey', 'selfupdate', 'tlsoffload', 'usenodedefaulttlsciphers', 'tlsciphers', 'userallowedip', 'userblockedip', 'swarmallowedip', 'agentallowedip', 'agentblockedip', 'fastcert', 'swarmport', 'logintoken', 'logintokenkey', 'logintokengen', 'mailtokengen', 'admin', 'unadmin', 'sessionkey', 'sessiontime', 'minify', 'minifycore', 'dblistconfigfiles', 'dbshowconfigfile', 'dbpushconfigfiles', 'oldencrypt', 'dbpullconfigfiles', 'dbdeleteconfigfiles', 'vaultpushconfigfiles', 'vaultpullconfigfiles', 'vaultdeleteconfigfiles', 'configkey', 'loadconfigfromdb', 'npmpath', 'serverid', 'recordencryptionrecode', 'vault', 'token', 'unsealkey', 'name', 'log', 'dbstats', 'translate', 'createaccount', 'setuptelegram', 'resetaccount', 'pass', 'removesubdomain', 'adminaccount', 'domain', 'email', 'configfile', 'maintenancemode', 'nedbtodb', 'removetestagents', 'agentupdatetest', 'hashpassword', 'hashpass', 'indexmcrec', 'mpsdebug', 'dumpcores', 'dev', 'mysql', 'mariadb', 'trustedproxy'];
- for (var arg in obj.args) { obj.args[arg.toLocaleLowerCase()] = obj.args[arg]; if (validArguments.indexOf(arg.toLocaleLowerCase()) == -1) { console.log('Invalid argument "' + arg + '", use --help.'); return; } }
- const ENVVAR_PREFIX = "meshcentral_"
- let envArgs = []
- for (let [envvar, envval] of Object.entries(process.env)) {
- if (envvar.toLocaleLowerCase().startsWith(ENVVAR_PREFIX)) {
- let argname = envvar.slice(ENVVAR_PREFIX.length).toLocaleLowerCase()
- if (!!argname && !(validArguments.indexOf(argname) == -1)) {
- envArgs = envArgs.concat([`--${argname}`, envval])
- }
- }
- }
- envArgs = require('minimist')(envArgs)
- obj.args = Object.assign(envArgs, obj.args)
- if (obj.args.mongodb == true) { console.log('Must specify: --mongodb [connectionstring] \r\nSee https://docs.mongodb.com/manual/reference/connection-string/ for MongoDB connection string.'); return; }
- if (obj.args.mysql == true) { console.log('Must specify: --mysql [connectionstring] \r\nExample mysql://user:password@127.0.0.1:3306/database'); return; }
- if (obj.args.mariadb == true) { console.log('Must specify: --mariadb [connectionstring] \r\nExample mariadb://user:password@127.0.0.1:3306/database'); return; }
- for (i in obj.config.settings) { obj.args[i] = obj.config.settings[i]; } // Place all settings into arguments, arguments have already been placed into settings so arguments take precedence.
-
- if ((obj.args.help == true) || (obj.args['?'] == true)) {
- console.log('MeshCentral v' + getCurrentVersion() + ', remote computer management web portal.');
- console.log('This software is open source under Apache 2.0 license.');
- console.log('Details at: https://www.meshcentral.com\r\n');
- if ((obj.platform == 'win32') || (obj.platform == 'linux')) {
- console.log('Run as a background service');
- console.log(' --install/uninstall Install MeshCentral as a background service.');
- console.log(' --start/stop/restart Control MeshCentral background service.');
- console.log('');
- console.log('Run standalone, console application');
- }
- console.log(' --user [username] Always login as [username] if account exists.');
- console.log(' --port [number] Web server port number.');
- console.log(' --redirport [number] Creates an additional HTTP server to redirect users to the HTTPS server.');
- console.log(' --exactports Server must run with correct ports or exit.');
- console.log(' --noagentupdate Server will not update mesh agent native binaries.');
- console.log(' --nedbtodb Transfer all NeDB records into current database.');
- console.log(' --listuserids Show a list of a user identifiers in the database.');
- console.log(' --cert [name], (country), (org) Create a web server certificate with [name] server name.');
- console.log(' country and organization can optionally be set.');
- console.log('');
- console.log('Server recovery commands, use only when MeshCentral is offline.');
- console.log(' --createaccount [userid] Create a new user account.');
- console.log(' --resetaccount [userid] Unlock an account, disable 2FA and set a new account password.');
- console.log(' --adminaccount [userid] Promote account to site administrator.');
- return;
- }
-
- // Fix a NeDB database
- if (obj.args.dbfix) {
- var lines = null, badJsonCount = 0, fieldNames = [], fixedDb = [];
- try { lines = obj.fs.readFileSync(obj.getConfigFilePath(obj.args.dbfix), { encoding: 'utf8' }).split('\n'); } catch (ex) { console.log('Invalid file: ' + obj.args.dbfix + ': ' + ex); process.exit(); }
- for (var i = 0; i < lines.length; i++) {
- var x = null;
- try { x = JSON.parse(lines[i]); } catch (ex) { badJsonCount++; }
- if (x != null) { fixedDb.push(lines[i]); for (var j in x) { if (fieldNames.indexOf(j) == -1) { fieldNames.push(j); } } }
- }
- console.log('Lines: ' + lines.length + ', badJSON: ' + badJsonCount + ', Feilds: ' + fieldNames);
- obj.fs.writeFileSync(obj.getConfigFilePath(obj.args.dbfix) + '-fixed', fixedDb.join('\n'), { encoding: 'utf8' });
- return;
- }
-
- // Check for invalid cert name
- if ((obj.args.cert != null) && ((typeof obj.args.cert != "string") || (obj.args.cert.indexOf('@') >= 0) || (obj.args.cert.indexOf('/') >= 0) || (obj.args.cert.indexOf(':') >= 0))) { console.log("Invalid certificate name"); process.exit(); return; }
-
- // Perform a password hash
- if (obj.args.hashpassword) { require('./pass').hash(obj.args.hashpassword, function (err, salt, hash, tag) { console.log(salt + ',' + hash); process.exit(); }); return; }
-
- // Dump to mesh cores
- if (obj.args.dumpcores) { obj.updateMeshCore(function () { console.log('Done.'); }, true); return; }
-
- // Setup Telegram
- if (obj.args.setuptelegram) { require('./meshmessaging.js').SetupTelegram(obj); return; }
-
- // Perform web site translations into different languages
- if (obj.args.translate) {
- // Check NodeJS version
- const NodeJSVer = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
- if (NodeJSVer < 8) { console.log("Translation feature requires Node v8 or above, current version is " + process.version + "."); process.exit(); return; }
-
- // Check if translate.json is in the "meshcentral-data" folder, if so use that and translate default pages.
- var translationFile = null, customTranslation = false;
- if (require('fs').existsSync(obj.path.join(obj.datapath, 'translate.json'))) { translationFile = obj.path.join(obj.datapath, 'translate.json'); console.log("Using translate.json in meshcentral-data."); customTranslation = true; }
- if (translationFile == null) { if (require('fs').existsSync(obj.path.join(__dirname, 'translate', 'translate.json'))) { translationFile = obj.path.join(__dirname, 'translate', 'translate.json'); console.log("Using default translate.json."); } }
- if (translationFile == null) { console.log("Unable to find translate.json."); process.exit(); return; }
-
- // Perform translation operations
- var didSomething = false;
- process.chdir(obj.path.join(__dirname, 'translate'));
- const translateEngine = require('./translate/translate.js')
- if (customTranslation == true) {
- // Translate all of the default files using custom translation file
- translateEngine.startEx(['', '', 'minifyall']);
- translateEngine.startEx(['', '', 'translateall', translationFile]);
- translateEngine.startEx(['', '', 'extractall', translationFile]);
- didSomething = true;
- } else {
- // Translate all of the default files
- translateEngine.startEx(['', '', 'minifyall']);
- translateEngine.startEx(['', '', 'translateall']);
- translateEngine.startEx(['', '', 'extractall']);
- didSomething = true;
- }
-
- // Check if "meshcentral-web" exists, if so, translate all pages in that folder.
- if (obj.webViewsOverridePath != null) {
- didSomething = true;
- var files = obj.fs.readdirSync(obj.webViewsOverridePath);
- for (var i in files) {
- var file = obj.path.join(obj.webViewsOverridePath, files[i]);
- if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) {
- translateEngine.startEx(['', '', 'minify', file]);
- }
- }
- files = obj.fs.readdirSync(obj.webViewsOverridePath);
- for (var i in files) {
- var file = obj.path.join(obj.webViewsOverridePath, files[i]);
- if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) {
- translateEngine.startEx(['', '', 'translate', '*', translationFile, file, '--subdir:translations']);
- }
- }
- }
-
- // Check domains and see if "meshcentral-web-DOMAIN" exists, if so, translate all pages in that folder
- for (i in obj.config.domains) {
- if (i == "") continue;
- var path = obj.path.join(obj.datapath, '..', 'meshcentral-web-' + i, 'views');
- if (require('fs').existsSync(path)) {
- didSomething = true;
- var files = obj.fs.readdirSync(path);
- for (var a in files) {
- var file = obj.path.join(path, files[a]);
- if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) {
- translateEngine.startEx(['', '', 'minify', file]);
- }
- }
- files = obj.fs.readdirSync(path);
- for (var a in files) {
- var file = obj.path.join(path, files[a]);
- if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) {
- translateEngine.startEx(['', '', 'translate', '*', translationFile, file, '--subdir:translations']);
- }
- }
- }
- }
- /*
- if (obj.webPublicOverridePath != null) {
- didSomething = true;
- var files = obj.fs.readdirSync(obj.webPublicOverridePath);
- for (var i in files) {
- var file = obj.path.join(obj.webPublicOverridePath, files[i]);
- if (file.endsWith('.htm') && !file.endsWith('-min.htm')) {
- translateEngine.startEx(['', '', 'translate', '*', translationFile, file, '--subdir:translations']);
- }
- }
- }
- */
-
- if (didSomething == false) { console.log("Nothing to do."); }
- console.log('Finished Translating.')
- process.exit();
- return;
- }
-
- // Setup the Node+NPM path if possible, this makes it possible to update the server even if NodeJS and NPM are not in default paths.
- if (obj.args.npmpath == null) {
- try {
- var nodepath = process.argv[0];
- var npmpath = obj.path.join(obj.path.dirname(process.argv[0]), 'npm');
- if (obj.fs.existsSync(nodepath) && obj.fs.existsSync(npmpath)) {
- if (nodepath.indexOf(' ') >= 0) { nodepath = '"' + nodepath + '"'; }
- if (npmpath.indexOf(' ') >= 0) { npmpath = '"' + npmpath + '"'; }
- if (obj.platform == 'win32') { obj.args.npmpath = npmpath; } else { obj.args.npmpath = (nodepath + ' ' + npmpath); }
- }
- } catch (ex) { }
- }
-
- // Linux background service systemd handling
- if (obj.platform == 'linux') {
- if (obj.args.install == true) {
- // Install MeshCentral in Systemd
- console.log('Installing MeshCentral as background Service...');
- var systemdConf = null;
- const userinfo = require('os').userInfo();
- if (require('fs').existsSync('/etc/systemd/system')) { systemdConf = '/etc/systemd/system/meshcentral.service'; }
- else if (require('fs').existsSync('/lib/systemd/system')) { systemdConf = '/lib/systemd/system/meshcentral.service'; }
- else if (require('fs').existsSync('/usr/lib/systemd/system')) { systemdConf = '/usr/lib/systemd/system/meshcentral.service'; }
- else { console.log('Unable to find systemd configuration folder.'); process.exit(); return; }
- console.log('Writing config file...');
- require('child_process').exec('which node', {}, function (error, stdout, stderr) {
- if ((error != null) || (stdout.indexOf('\n') == -1)) { console.log('ERROR: Unable to get node location: ' + error); process.exit(); return; }
- const nodePath = stdout.substring(0, stdout.indexOf('\n'));
- const config = '[Unit]\nDescription=MeshCentral Server\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nType=simple\nLimitNOFILE=1000000\nExecStart=' + nodePath + ' ' + __dirname + '/meshcentral\nWorkingDirectory=' + userinfo.homedir + '\nEnvironment=NODE_ENV=production\nUser=' + userinfo.username + '\nGroup=' + userinfo.username + '\nRestart=always\n# Restart service after 10 seconds if node service crashes\nRestartSec=10\n# Set port permissions capability\nAmbientCapabilities=cap_net_bind_service\n\n[Install]\nWantedBy=multi-user.target\n';
- require('child_process').exec('echo \"' + config + '\" | sudo tee ' + systemdConf, {}, function (error, stdout, stderr) {
- if ((error != null) && (error != '')) { console.log('ERROR: Unable to write config file: ' + error); process.exit(); return; }
- console.log('Enabling service...');
- require('child_process').exec('sudo systemctl enable meshcentral.service', {}, function (error, stdout, stderr) {
- if ((error != null) && (error != '')) { console.log('ERROR: Unable to enable MeshCentral as a service: ' + error); process.exit(); return; }
- if (stdout.length > 0) { console.log(stdout); }
- console.log('Starting service...');
- require('child_process').exec('sudo systemctl start meshcentral.service', {}, function (error, stdout, stderr) {
- if ((error != null) && (error != '')) { console.log('ERROR: Unable to start MeshCentral as a service: ' + error); process.exit(); return; }
- if (stdout.length > 0) { console.log(stdout); }
- console.log('Done.');
- });
- });
- });
- });
- return;
- } else if (obj.args.uninstall == true) {
- // Uninstall MeshCentral in Systemd
- console.log('Uninstalling MeshCentral background service...');
- var systemdConf = null;
- if (require('fs').existsSync('/etc/systemd/system')) { systemdConf = '/etc/systemd/system/meshcentral.service'; }
- else if (require('fs').existsSync('/lib/systemd/system')) { systemdConf = '/lib/systemd/system/meshcentral.service'; }
- else if (require('fs').existsSync('/usr/lib/systemd/system')) { systemdConf = '/usr/lib/systemd/system/meshcentral.service'; }
- else { console.log('Unable to find systemd configuration folder.'); process.exit(); return; }
- console.log('Stopping service...');
- require('child_process').exec('sudo systemctl stop meshcentral.service', {}, function (err, stdout, stderr) {
- if ((err != null) && (err != '')) { console.log('ERROR: Unable to stop MeshCentral as a service: ' + err); }
- if (stdout.length > 0) { console.log(stdout); }
- console.log('Disabling service...');
- require('child_process').exec('sudo systemctl disable meshcentral.service', {}, function (err, stdout, stderr) {
- if ((err != null) && (err != '')) { console.log('ERROR: Unable to disable MeshCentral as a service: ' + err); }
- if (stdout.length > 0) { console.log(stdout); }
- console.log('Removing config file...');
- require('child_process').exec('sudo rm ' + systemdConf, {}, function (err, stdout, stderr) {
- if ((err != null) && (err != '')) { console.log('ERROR: Unable to delete MeshCentral config file: ' + err); }
- console.log('Done.');
- });
- });
- });
- return;
- } else if (obj.args.start == true) {
- // Start MeshCentral in Systemd
- require('child_process').exec('sudo systemctl start meshcentral.service', {}, function (err, stdout, stderr) {
- if ((err != null) && (err != '')) { console.log('ERROR: Unable to start MeshCentral: ' + err); process.exit(); return; }
- console.log('Done.');
- });
- return;
- } else if (obj.args.stop == true) {
- // Stop MeshCentral in Systemd
- require('child_process').exec('sudo systemctl stop meshcentral.service', {}, function (err, stdout, stderr) {
- if ((err != null) && (err != '')) { console.log('ERROR: Unable to stop MeshCentral: ' + err); process.exit(); return; }
- console.log('Done.');
- });
- return;
- } else if (obj.args.restart == true) {
- // Restart MeshCentral in Systemd
- require('child_process').exec('sudo systemctl restart meshcentral.service', {}, function (err, stdout, stderr) {
- if ((err != null) && (err != '')) { console.log('ERROR: Unable to restart MeshCentral: ' + err); process.exit(); return; }
- console.log('Done.');
- });
- return;
- }
- }
-
- // FreeBSD background service handling, MUST USE SPAWN FOR SERVICE COMMANDS!
- if (obj.platform == 'freebsd') {
- if (obj.args.install == true) {
- // Install MeshCentral in rc.d
- console.log('Installing MeshCentral as background Service...');
- var systemdConf = "/usr/local/etc/rc.d/meshcentral";
- const userinfo = require('os').userInfo();
- console.log('Writing config file...');
- require('child_process').exec('which node', {}, function (error, stdout, stderr) {
- if ((error != null) || (stdout.indexOf('\n') == -1)) { console.log('ERROR: Unable to get node location: ' + error); process.exit(); return; }
- const nodePath = stdout.substring(0, stdout.indexOf('\n'));
- const config = '#!/bin/sh\n# MeshCentral FreeBSD Service Script\n# PROVIDE: meshcentral\n# REQUIRE: NETWORKING\n# KEYWORD: shutdown\n. /etc/rc.subr\nname=meshcentral\nuser=' + userinfo.username + '\nrcvar=meshcentral_enable\n: \\${meshcentral_enable:=\\"NO\\"}\n: \\${meshcentral_args:=\\"\\"}\npidfile=/var/run/meshcentral/meshcentral.pid\ncommand=\\"/usr/sbin/daemon\\"\nmeshcentral_chdir=\\"' + obj.parentpath + '\\"\ncommand_args=\\"-r -u \\${user} -P \\${pidfile} -S -T meshcentral -m 3 ' + nodePath + ' ' + __dirname + ' \\${meshcentral_args}\\"\nload_rc_config \\$name\nrun_rc_command \\"\\$1\\"\n';
- require('child_process').exec('echo \"' + config + '\" | tee ' + systemdConf + ' && chmod +x ' + systemdConf, {}, function (error, stdout, stderr) {
- if ((error != null) && (error != '')) { console.log('ERROR: Unable to write config file: ' + error); process.exit(); return; }
- console.log('Enabling service...');
- require('child_process').exec('sysrc meshcentral_enable="YES"', {}, function (error, stdout, stderr) {
- if ((error != null) && (error != '')) { console.log('ERROR: Unable to enable MeshCentral as a service: ' + error); process.exit(); return; }
- if (stdout.length > 0) { console.log(stdout); }
- console.log('Starting service...');
- const service = require('child_process').spawn('service', ['meshcentral', 'start']);
- service.stdout.on('data', function (data) { console.log(data.toString()); });
- service.stderr.on('data', function (data) { console.log(data.toString()); });
- service.on('exit', function (code) {
- console.log((code === 0) ? 'Done.' : 'ERROR: Unable to start MeshCentral as a service');
- process.exit(); // Must exit otherwise we just hang
- });
- });
- });
- });
- return;
- } else if (obj.args.uninstall == true) {
- // Uninstall MeshCentral in rc.d
- console.log('Uninstalling MeshCentral background service...');
- var systemdConf = "/usr/local/etc/rc.d/meshcentral";
- console.log('Stopping service...');
- const service = require('child_process').spawn('service', ['meshcentral', 'stop']);
- service.stdout.on('data', function (data) { console.log(data.toString()); });
- service.stderr.on('data', function (data) { console.log(data.toString()); });
- service.on('exit', function (code) {
- if (code !== 0) { console.log('ERROR: Unable to stop MeshCentral as a service'); }
- console.log('Disabling service...');
- require('child_process').exec('sysrc -x meshcentral_enable', {}, function (err, stdout, stderr) {
- if ((err != null) && (err != '')) { console.log('ERROR: Unable to disable MeshCentral as a service: ' + err); }
- if (stdout.length > 0) { console.log(stdout); }
- console.log('Removing config file...');
- require('child_process').exec('rm ' + systemdConf, {}, function (err, stdout, stderr) {
- if ((err != null) && (err != '')) { console.log('ERROR: Unable to delete MeshCentral config file: ' + err); }
- console.log('Done.');
- process.exit(); // Must exit otherwise we just hang
- });
- });
- });
- return;
- } else if (obj.args.start == true) {
- // Start MeshCentral in rc.d
- const service = require('child_process').spawn('service', ['meshcentral', 'start']);
- service.stdout.on('data', function (data) { console.log(data.toString()); });
- service.stderr.on('data', function (data) { console.log(data.toString()); });
- service.on('exit', function (code) {
- console.log((code === 0) ? 'Done.' : 'ERROR: Unable to start MeshCentral as a service: ' + error);
- process.exit(); // Must exit otherwise we just hang
- });
- return;
- } else if (obj.args.stop == true) {
- // Stop MeshCentral in rc.d
- const service = require('child_process').spawn('service', ['meshcentral', 'stop']);
- service.stdout.on('data', function (data) { console.log(data.toString()); });
- service.stderr.on('data', function (data) { console.log(data.toString()); });
- service.on('exit', function (code) {
- console.log((code === 0) ? 'Done.' : 'ERROR: Unable to stop MeshCentral as a service: ' + error);
- process.exit(); // Must exit otherwise we just hang
- });
- return;
- } else if (obj.args.restart == true) {
- // Restart MeshCentral in rc.d
- const service = require('child_process').spawn('service', ['meshcentral', 'restart']);
- service.stdout.on('data', function (data) { console.log(data.toString()); });
- service.stderr.on('data', function (data) { console.log(data.toString()); });
- service.on('exit', function (code) {
- console.log((code === 0) ? 'Done.' : 'ERROR: Unable to restart MeshCentral as a service: ' + error);
- process.exit(); // Must exit otherwise we just hang
- });
- return;
- }
- }
-
- // Index a recorded file
- if (obj.args.indexmcrec != null) {
- if (typeof obj.args.indexmcrec != 'string') {
- console.log('Usage: --indexmrec [filename.mcrec]');
- } else if (obj.fs.existsSync(obj.args.indexmcrec)) {
- console.log('Indexing file: ' + obj.args.indexmcrec);
- require(require('path').join(__dirname, 'mcrec.js')).indexFile(obj.args.indexmcrec);
- } else {
- console.log('Unable to find file: ' + obj.args.indexmcrec);
- }
- return;
- }
-
- // Windows background service handling
- if ((obj.platform == 'win32') && (obj.service != null)) {
- // Build MeshCentral parent path and Windows Service path
- var mcpath = __dirname;
- if (mcpath.endsWith('\\node_modules\\meshcentral') || mcpath.endsWith('/node_modules/meshcentral')) { mcpath = require('path').join(mcpath, '..', '..'); }
- const servicepath = obj.path.join(mcpath, 'WinService');
-
- // Check if we need to install, start, stop, remove ourself as a background service
- if (((obj.args.xinstall == true) || (obj.args.xuninstall == true) || (obj.args.start == true) || (obj.args.stop == true) || (obj.args.restart == true))) {
- var env = [], xenv = ['user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'exactport', 'rediraliasport', 'debug'];
- for (i in xenv) { if (obj.args[xenv[i]] != null) { env.push({ name: 'mesh' + xenv[i], value: obj.args[xenv[i]] }); } } // Set some args as service environment variables.
-
- var serviceFilePath = null;
- if (obj.fs.existsSync(obj.path.join(servicepath, 'winservice.js'))) { serviceFilePath = obj.path.join(servicepath, 'winservice.js'); }
- else if (obj.fs.existsSync(obj.path.join(__dirname, '../WinService/winservice.js'))) { serviceFilePath = obj.path.join(__dirname, '../WinService/winservice.js'); }
- else if (obj.fs.existsSync(obj.path.join(__dirname, 'winservice.js'))) { serviceFilePath = obj.path.join(__dirname, 'winservice.js'); }
- if (serviceFilePath == null) { console.log('Unable to find winservice.js'); return; }
-
- const svc = new obj.service({ name: 'MeshCentral', description: 'MeshCentral Remote Management Server', script: servicepath, env: env, wait: 2, grow: 0.5 });
- svc.on('install', function () { console.log('MeshCentral service installed.'); svc.start(); });
- svc.on('uninstall', function () { console.log('MeshCentral service uninstalled.'); process.exit(); });
- svc.on('start', function () { console.log('MeshCentral service started.'); process.exit(); });
- svc.on('stop', function () { console.log('MeshCentral service stopped.'); if (obj.args.stop) { process.exit(); } if (obj.args.restart) { console.log('Holding 5 seconds...'); setTimeout(function () { svc.start(); }, 5000); } });
- svc.on('alreadyinstalled', function () { console.log('MeshCentral service already installed.'); process.exit(); });
- svc.on('invalidinstallation', function () { console.log('Invalid MeshCentral service installation.'); process.exit(); });
-
- if (obj.args.xinstall == true) { try { svc.install(); } catch (ex) { logException(ex); } }
- if (obj.args.stop == true || obj.args.restart == true) { try { svc.stop(); } catch (ex) { logException(ex); } }
- if (obj.args.start == true) { try { svc.start(); } catch (ex) { logException(ex); } }
- if (obj.args.xuninstall == true) { try { svc.uninstall(); } catch (ex) { logException(ex); } }
- return;
- }
-
- // Windows service install using the external winservice.js
- if (obj.args.install == true) {
- console.log('Installing MeshCentral as Windows Service...');
- if (obj.fs.existsSync(servicepath) == false) { try { obj.fs.mkdirSync(servicepath); } catch (ex) { console.log('ERROR: Unable to create WinService folder: ' + ex); process.exit(); return; } }
- try { obj.fs.createReadStream(obj.path.join(__dirname, 'winservice.js')).pipe(obj.fs.createWriteStream(obj.path.join(servicepath, 'winservice.js'))); } catch (ex) { console.log('ERROR: Unable to copy winservice.js: ' + ex); process.exit(); return; }
- require('child_process').exec('node winservice.js --install', { maxBuffer: 512000, timeout: 120000, cwd: servicepath }, function (error, stdout, stderr) {
- if ((error != null) && (error != '')) { console.log('ERROR: Unable to install MeshCentral as a service: ' + error); process.exit(); return; }
- console.log(stdout);
- });
- return;
- } else if (obj.args.uninstall == true) {
- console.log('Uninstalling MeshCentral Windows Service...');
- if (obj.fs.existsSync(servicepath) == true) {
- require('child_process').exec('node winservice.js --uninstall', { maxBuffer: 512000, timeout: 120000, cwd: servicepath }, function (error, stdout, stderr) {
- if ((error != null) && (error != '')) { console.log('ERROR: Unable to uninstall MeshCentral service: ' + error); process.exit(); return; }
- console.log(stdout);
- try { obj.fs.unlinkSync(obj.path.join(servicepath, 'winservice.js')); } catch (ex) { }
- try { obj.fs.rmdirSync(servicepath); } catch (ex) { }
- });
- } else if (obj.fs.existsSync(obj.path.join(__dirname, '../WinService')) == true) {
- require('child_process').exec('node winservice.js --uninstall', { maxBuffer: 512000, timeout: 120000, cwd: obj.path.join(__dirname, '../WinService') }, function (error, stdout, stderr) {
- if ((error != null) && (error != '')) { console.log('ERROR: Unable to uninstall MeshCentral service: ' + error); process.exit(); return; }
- console.log(stdout);
- try { obj.fs.unlinkSync(obj.path.join(__dirname, '../WinService/winservice.js')); } catch (ex) { }
- try { obj.fs.rmdirSync(obj.path.join(__dirname, '../WinService')); } catch (ex) { }
- });
- } else {
- require('child_process').exec('node winservice.js --uninstall', { maxBuffer: 512000, timeout: 120000, cwd: __dirname }, function (error, stdout, stderr) {
- if ((error != null) && (error != '')) { console.log('ERROR: Unable to uninstall MeshCentral service: ' + error); process.exit(); return; }
- console.log(stdout);
- });
- }
- return;
- }
- }
-
- // If "--launch" is in the arguments, launch now
- if (obj.args.launch) {
- if (obj.args.vault) { obj.StartVault(); } else { obj.StartEx(); }
- } else {
- // if "--launch" is not specified, launch the server as a child process.
- const startArgs = [];
- for (i in process.argv) {
- if (i > 0) {
- const arg = process.argv[i];
- if ((arg.length > 0) && ((arg.indexOf(' ') >= 0) || (arg.indexOf('&') >= 0))) { startArgs.push(arg); } else { startArgs.push(arg); }
- }
- }
- startArgs.push('--launch', process.pid);
- obj.launchChildServer(startArgs);
- }
- };
-
- // Launch MeshCentral as a child server and monitor it.
- obj.launchChildServer = function (startArgs) {
- const child_process = require('child_process');
- const isInspectorAttached = (()=> { try { return require('node:inspector').url() !== undefined; } catch (_) { return false; } }).call();
- const logFromChildProcess = isInspectorAttached ? () => {} : console.log.bind(console);
- try { if (process.traceDeprecation === true) { startArgs.unshift('--trace-deprecation'); } } catch (ex) { }
- try { if (process.traceProcessWarnings === true) { startArgs.unshift('--trace-warnings'); } } catch (ex) { }
- if (startArgs[0] != "--disable-proto=delete") startArgs.unshift("--disable-proto=delete")
- childProcess = child_process.execFile(process.argv[0], startArgs, { maxBuffer: Infinity, cwd: obj.parentpath }, function (error, stdout, stderr) {
- if (childProcess.xrestart == 1) {
- setTimeout(function () { obj.launchChildServer(startArgs); }, 500); // This is an expected restart.
- } else if (childProcess.xrestart == 2) {
- console.log('Expected exit...');
- process.exit(); // User CTRL-C exit.
- } else if (childProcess.xrestart == 3) {
- // Server self-update exit
- var version = '';
- if (typeof obj.args.selfupdate == 'string') { version = '@' + obj.args.selfupdate; }
- else if (typeof obj.args.specificupdate == 'string') { version = '@' + obj.args.specificupdate; delete obj.args.specificupdate; }
- const child_process = require('child_process');
- const npmpath = ((typeof obj.args.npmpath == 'string') ? obj.args.npmpath : 'npm');
- const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
- const env = Object.assign({}, process.env); // Shallow clone
- if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
- // always use --save-exact - https://stackoverflow.com/a/64507176/1210734
- const xxprocess = child_process.exec(npmpath + ' install --save-exact --no-audit meshcentral' + version + npmproxy, { maxBuffer: Infinity, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) {
- if ((error != null) && (error != '')) { console.log('Update failed: ' + error); }
- });
- xxprocess.data = '';
- xxprocess.stdout.on('data', function (data) { xxprocess.data += data; });
- xxprocess.stderr.on('data', function (data) { xxprocess.data += data; });
- xxprocess.on('close', function (code) {
- if (code == 0) { console.log('Update completed...'); }
-
- // Run the server updated script if present
- if (typeof obj.config.settings.runonserverupdated == 'string') {
- const child_process = require('child_process');
- var parentpath = __dirname;
- if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) { parentpath = require('path').join(__dirname, '../..'); }
- child_process.exec(obj.config.settings.runonserverupdated + ' ' + getCurrentVersion(), { maxBuffer: 512000, timeout: 120000, cwd: parentpath }, function (error, stdout, stderr) { });
- }
-
- if (obj.args.cleannpmcacheonupdate === true) {
- // Perform NPM cache clean
- console.log('Cleaning NPM cache...');
- const xxxprocess = child_process.exec(npmpath + ' cache clean --force', { maxBuffer: Infinity, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { });
- xxxprocess.on('close', function (code) { setTimeout(function () { obj.launchChildServer(startArgs); }, 1000); });
- } else {
- // Run the updated server
- setTimeout(function () { obj.launchChildServer(startArgs); }, 1000);
- }
- });
- } else {
- if (error != null) {
- // This is an un-expected restart
- console.log(error);
- console.log('ERROR: MeshCentral failed with critical error, check mesherrors.txt. Restarting in 5 seconds...');
- setTimeout(function () { obj.launchChildServer(startArgs); }, 5000);
-
- // Run the server error script if present
- if (typeof obj.config.settings.runonservererror == 'string') {
- const child_process = require('child_process');
- var parentpath = __dirname;
- if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) { parentpath = require('path').join(__dirname, '../..'); }
- child_process.exec(obj.config.settings.runonservererror + ' ' + getCurrentVersion(), { maxBuffer: 512000, timeout: 120000, cwd: parentpath }, function (error, stdout, stderr) { });
- }
- }
- }
- });
- childProcess.stdout.on('data', function (data) {
- if (data[data.length - 1] == '\n') { data = data.substring(0, data.length - 1); }
- if (data.indexOf('Updating settings folder...') >= 0) { childProcess.xrestart = 1; }
- else if (data.indexOf('Updating server certificates...') >= 0) { childProcess.xrestart = 1; }
- else if (data.indexOf('Server Ctrl-C exit...') >= 0) { childProcess.xrestart = 2; }
- else if (data.indexOf('Starting self upgrade...') >= 0) { childProcess.xrestart = 3; }
- else if (data.indexOf('Server restart...') >= 0) { childProcess.xrestart = 1; }
- else if (data.indexOf('Starting self upgrade to: ') >= 0) { obj.args.specificupdate = data.substring(26).split('\r')[0].split('\n')[0]; childProcess.xrestart = 3; }
- var datastr = data;
- while (datastr.endsWith('\r') || datastr.endsWith('\n')) { datastr = datastr.substring(0, datastr.length - 1); }
- logFromChildProcess(datastr);
- });
- childProcess.stderr.on('data', function (data) {
- var datastr = data;
- while (datastr.endsWith('\r') || datastr.endsWith('\n')) { datastr = datastr.substring(0, datastr.length - 1); }
- logFromChildProcess('ERR: ' + datastr);
- if (data.startsWith('le.challenges[tls-sni-01].loopback')) { return; } // Ignore this error output from GreenLock
- if (data[data.length - 1] == '\n') { data = data.substring(0, data.length - 1); }
- obj.logError(data);
- });
- childProcess.on('close', function (code) { if ((code != 0) && (code != 123)) { /* console.log("Exited with code " + code); */ } });
- };
-
- obj.logError = function (err) {
- try {
- var errlogpath = null;
- if (typeof obj.args.mesherrorlogpath == 'string') { errlogpath = obj.path.join(obj.args.mesherrorlogpath, 'mesherrors.txt'); } else { errlogpath = obj.getConfigFilePath('mesherrors.txt'); }
- obj.fs.appendFileSync(errlogpath, '-------- ' + new Date().toLocaleString() + ' ---- ' + getCurrentVersion() + ' --------\r\n\r\n' + err + '\r\n\r\n\r\n');
- } catch (ex) { console.log('ERROR: Unable to write to mesherrors.txt.'); }
- };
-
- // Get current and latest MeshCentral server versions using NPM
- obj.getLatestServerVersion = function (callback) {
- if (callback == null) return;
- try {
- if (typeof obj.args.selfupdate == 'string') { callback(getCurrentVersion(), obj.args.selfupdate); return; } // If we are targetting a specific version, return that one as current.
- const child_process = require('child_process');
- const npmpath = ((typeof obj.args.npmpath == 'string') ? obj.args.npmpath : 'npm');
- const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
- const env = Object.assign({}, process.env); // Shallow clone
- if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
- const xxprocess = child_process.exec(npmpath + npmproxy + ' view meshcentral dist-tags.latest', { maxBuffer: 512000, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { });
- xxprocess.data = '';
- xxprocess.stdout.on('data', function (data) { xxprocess.data += data; });
- xxprocess.stderr.on('data', function (data) { });
- xxprocess.on('close', function (code) {
- var latestVer = null;
- if (code == 0) { try { latestVer = xxprocess.data.split(' ').join('').split('\r').join('').split('\n').join(''); } catch (ex) { } }
- callback(getCurrentVersion(), latestVer);
- });
- } catch (ex) { callback(getCurrentVersion(), null, ex); } // If the system is running out of memory, an exception here can easily happen.
- };
-
- // Get current version and all MeshCentral server tags using NPM
- obj.getServerTags = function (callback) {
- if (callback == null) return;
- try {
- if (typeof obj.args.selfupdate == 'string') { callback({ current: getCurrentVersion(), latest: obj.args.selfupdate }); return; } // If we are targetting a specific version, return that one as current.
- const child_process = require('child_process');
- const npmpath = ((typeof obj.args.npmpath == 'string') ? obj.args.npmpath : 'npm');
- const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
- const env = Object.assign({}, process.env); // Shallow clone
- if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
- const xxprocess = child_process.exec(npmpath + npmproxy + ' dist-tag ls meshcentral', { maxBuffer: 512000, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { });
- xxprocess.data = '';
- xxprocess.stdout.on('data', function (data) { xxprocess.data += data; });
- xxprocess.stderr.on('data', function (data) { });
- xxprocess.on('close', function (code) {
- var tags = { current: getCurrentVersion() };
- if (code == 0) {
- try {
- var lines = xxprocess.data.split('\r\n').join('\n').split('\n');
- for (var i in lines) { var s = lines[i].split(': '); if ((s.length == 2) && (obj.args.npmtag == null) || (obj.args.npmtag == s[0])) { tags[s[0]] = s[1]; } }
- } catch (ex) { }
- }
- callback(tags);
- });
- } catch (ex) { callback({ current: getCurrentVersion() }, ex); } // If the system is running out of memory, an exception here can easily happen.
- };
-
- // Use NPM to get list of versions
- obj.getServerVersions = function (callback) {
- try {
- const child_process = require('child_process');
- const npmpath = ((typeof obj.args.npmpath == 'string') ? obj.args.npmpath : 'npm');
- const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
- const env = Object.assign({}, process.env); // Shallow clone
- if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
- const xxprocess = child_process.exec(npmpath + npmproxy + ' view meshcentral versions --json', { maxBuffer: 512000, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { });
- xxprocess.data = '';
- xxprocess.stdout.on('data', function (data) { xxprocess.data += data; });
- xxprocess.stderr.on('data', function (data) { });
- xxprocess.on('close', function (code) {
- (code == 0) ? callback(xxprocess.data) : callback('{}');
- });
- } catch (ex) { callback('{}'); }
- };
-
- // Initiate server self-update
- obj.performServerUpdate = function (version) {
- if (obj.serverSelfWriteAllowed != true) return false;
- if ((version == null) || (version == '') || (typeof version != 'string')) { console.log('Starting self upgrade...'); } else { console.log('Starting self upgrade to: ' + version); }
- process.exit(200);
- return true;
- };
-
- // Initiate server self-update
- obj.performServerCertUpdate = function () { console.log('Updating server certificates...'); process.exit(200); };
-
- // Start by loading configuration from Vault
- obj.StartVault = function () {
- // Check that the configuration can only be loaded from one place
- if ((obj.args.vault != null) && (obj.args.loadconfigfromdb != null)) { console.log("Can't load configuration from both database and Vault."); process.exit(); return; }
-
- // Fix arguments if needed
- if (typeof obj.args.vault == 'string') {
- obj.args.vault = { endpoint: obj.args.vault };
- if (typeof obj.args.token == 'string') { obj.args.vault.token = obj.args.token; }
- if (typeof obj.args.unsealkey == 'string') { obj.args.vault.unsealkey = obj.args.unsealkey; }
- if (typeof obj.args.name == 'string') { obj.args.vault.name = obj.args.name; }
- }
-
- // Load configuration for HashiCorp's Vault if needed
- if (obj.args.vault) {
- if (obj.args.vault.endpoint == null) { console.log('Missing Vault endpoint.'); process.exit(); return; }
- if (obj.args.vault.token == null) { console.log('Missing Vault token.'); process.exit(); return; }
- if (obj.args.vault.unsealkey == null) { console.log('Missing Vault unsealkey.'); process.exit(); return; }
- if (obj.args.vault.name == null) { obj.args.vault.name = 'meshcentral'; }
-
- // Get new instance of the client
- const vault = require("node-vault")({ endpoint: obj.args.vault.endpoint, token: obj.args.vault.token });
- vault.unseal({ key: obj.args.vault.unsealkey })
- .then(function () {
- if (obj.args.vaultdeleteconfigfiles) {
- vault.delete('secret/data/' + obj.args.vault.name)
- .then(function (r) { console.log('Done.'); process.exit(); })
- .catch(function (x) { console.log(x); process.exit(); });
- } else if (obj.args.vaultpushconfigfiles) {
- // Push configuration files into Vault
- if ((obj.args.vaultpushconfigfiles == '*') || (obj.args.vaultpushconfigfiles === true)) { obj.args.vaultpushconfigfiles = obj.datapath; }
- obj.fs.readdir(obj.args.vaultpushconfigfiles, function (err, files) {
- if (err != null) { console.log('ERROR: Unable to read from folder ' + obj.args.vaultpushconfigfiles); process.exit(); return; }
- var configFound = false;
- for (var i in files) { if (files[i] == 'config.json') { configFound = true; } }
- if (configFound == false) { console.log('ERROR: No config.json in folder ' + obj.args.vaultpushconfigfiles); process.exit(); return; }
- var configFiles = {};
- for (var i in files) {
- const file = files[i];
- if ((file == 'config.json') || file.endsWith('.key') || file.endsWith('.crt') || (file == 'terms.txt') || file.endsWith('.jpg') || file.endsWith('.png')) {
- const path = obj.path.join(obj.args.vaultpushconfigfiles, files[i]), binary = Buffer.from(obj.fs.readFileSync(path, { encoding: 'binary' }), 'binary');
- console.log('Pushing ' + file + ', ' + binary.length + ' bytes.');
- if (file.endsWith('.json') || file.endsWith('.key') || file.endsWith('.crt')) { configFiles[file] = binary.toString(); } else { configFiles[file] = binary.toString('base64'); }
- }
- }
- vault.write('secret/data/' + obj.args.vault.name, { "data": configFiles })
- .then(function (r) { console.log('Done.'); process.exit(); })
- .catch(function (x) { console.log(x); process.exit(); });
- });
- } else {
- // Read configuration files from Vault
- vault.read('secret/data/' + obj.args.vault.name)
- .then(function (r) {
- if ((r == null) || (r.data == null) || (r.data.data == null)) { console.log('Unable to read configuration from Vault.'); process.exit(); return; }
- var configFiles = obj.configurationFiles = r.data.data;
-
- // Decode Base64 when needed
- for (var file in configFiles) { if (!file.endsWith('.json') && !file.endsWith('.key') && !file.endsWith('.crt')) { configFiles[file] = Buffer.from(configFiles[file], 'base64'); } }
-
- // Save all of the files
- if (obj.args.vaultpullconfigfiles) {
- for (var i in configFiles) {
- var fullFileName = obj.path.join(obj.args.vaultpullconfigfiles, i);
- try { obj.fs.writeFileSync(fullFileName, configFiles[i]); } catch (ex) { console.log('Unable to write to ' + fullFileName); process.exit(); return; }
- console.log('Pulling ' + i + ', ' + configFiles[i].length + ' bytes.');
- }
- console.log('Done.');
- process.exit();
- }
-
- // Parse the new configuration file
- var config2 = null;
- try { config2 = JSON.parse(configFiles['config.json']); } catch (ex) { console.log('Error, unable to parse config.json from Vault.'); process.exit(); return; }
-
- // Set the command line arguments to the config file if they are not present
- if (!config2.settings) { config2.settings = {}; }
- for (var i in args) { config2.settings[i] = args[i]; }
- obj.args = args = config2.settings;
-
- // Lower case all keys in the config file
- obj.common.objKeysToLower(config2, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'telegram/proxy']);
-
- // Grad some of the values from the original config.json file if present.
- if ((config.settings.vault != null) && (config2.settings != null)) { config2.settings.vault = config.settings.vault; }
-
- // We got a new config.json from the database, let's use it.
- config = obj.config = config2;
- obj.StartEx();
- })
- .catch(function (x) { console.log(x); process.exit(); });
- }
- }).catch(function (x) { console.log(x); process.exit(); });
- return;
- }
- }
-
- // Look for easy command line instructions and do them here.
- obj.StartEx = async function () {
- var i;
- //var wincmd = require('node-windows');
- //wincmd.list(function (svc) { console.log(svc); }, true);
-
- // Setup syslog support. Not supported on Windows.
- if ((require('os').platform() != 'win32') && ((config.settings.syslog != null) || (config.settings.syslogjson != null) || (config.settings.syslogauth != null))) {
- if (config.settings.syslog === true) { config.settings.syslog = 'meshcentral'; }
- if (config.settings.syslogjson === true) { config.settings.syslogjson = 'meshcentral-json'; }
- if (config.settings.syslogauth === true) { config.settings.syslogauth = 'meshcentral-auth'; }
- if (typeof config.settings.syslog == 'string') {
- obj.syslog = require('modern-syslog');
- console.log('Starting ' + config.settings.syslog + ' syslog.');
- obj.syslog.init(config.settings.syslog, obj.syslog.LOG_PID | obj.syslog.LOG_ODELAY, obj.syslog.LOG_LOCAL0);
- obj.syslog.log(obj.syslog.LOG_INFO, "MeshCentral v" + getCurrentVersion() + " Server Start");
- }
- if (typeof config.settings.syslogjson == 'string') {
- obj.syslogjson = require('modern-syslog');
- console.log('Starting ' + config.settings.syslogjson + ' JSON syslog.');
- obj.syslogjson.init(config.settings.syslogjson, obj.syslogjson.LOG_PID | obj.syslogjson.LOG_ODELAY, obj.syslogjson.LOG_LOCAL0);
- obj.syslogjson.log(obj.syslogjson.LOG_INFO, "MeshCentral v" + getCurrentVersion() + " Server Start");
- }
- if (typeof config.settings.syslogauth == 'string') {
- obj.syslogauth = require('modern-syslog');
- console.log('Starting ' + config.settings.syslogauth + ' auth syslog.');
- obj.syslogauth.init(config.settings.syslogauth, obj.syslogauth.LOG_PID | obj.syslogauth.LOG_ODELAY, obj.syslogauth.LOG_LOCAL0);
- obj.syslogauth.log(obj.syslogauth.LOG_INFO, "MeshCentral v" + getCurrentVersion() + " Server Start");
- }
- }
- // Setup TCP syslog support, this works on all OS's.
- if (config.settings.syslogtcp != null) {
- const syslog = require('syslog');
- if (config.settings.syslogtcp === true) {
- obj.syslogtcp = syslog.createClient(514, 'localhost');
- } else {
- const sp = config.settings.syslogtcp.split(':');
- obj.syslogtcp = syslog.createClient(parseInt(sp[1]), sp[0]);
- }
- obj.syslogtcp.log("MeshCentral v" + getCurrentVersion() + " Server Start", obj.syslogtcp.LOG_INFO);
- }
-
- // Check top level configuration for any unrecognized values
- if (config) { for (var i in config) { if ((typeof i == 'string') && (i.length > 0) && (i[0] != '_') && (['settings', 'domaindefaults', 'domains', 'configfiles', 'smtp', 'letsencrypt', 'peers', 'sms', 'messaging', 'sendgrid', 'sendmail', 'firebase', 'firebaserelay', '$schema'].indexOf(i) == -1)) { addServerWarning('Unrecognized configuration option \"' + i + '\".', 3, [i]); } } }
-
- // Read IP lists from files if applicable
- config.settings.userallowedip = obj.args.userallowedip = readIpListFromFile(obj.args.userallowedip);
- config.settings.userblockedip = obj.args.userblockedip = readIpListFromFile(obj.args.userblockedip);
- config.settings.agentallowedip = obj.args.agentallowedip = readIpListFromFile(obj.args.agentallowedip);
- config.settings.agentblockedip = obj.args.agentblockedip = readIpListFromFile(obj.args.agentblockedip);
- config.settings.swarmallowedip = obj.args.swarmallowedip = readIpListFromFile(obj.args.swarmallowedip);
-
- // Check IP lists and ranges
- if (typeof obj.args.userallowedip == 'string') { if (obj.args.userallowedip == '') { config.settings.userallowedip = obj.args.userallowedip = null; } else { config.settings.userallowedip = obj.args.userallowedip = obj.args.userallowedip.split(' ').join('').split(','); } }
- if (typeof obj.args.userblockedip == 'string') { if (obj.args.userblockedip == '') { config.settings.userblockedip = obj.args.userblockedip = null; } else { config.settings.userblockedip = obj.args.userblockedip = obj.args.userblockedip.split(' ').join('').split(','); } }
- if (typeof obj.args.agentallowedip == 'string') { if (obj.args.agentallowedip == '') { config.settings.agentallowedip = obj.args.agentallowedip = null; } else { config.settings.agentallowedip = obj.args.agentallowedip = obj.args.agentallowedip.split(' ').join('').split(','); } }
- if (typeof obj.args.agentblockedip == 'string') { if (obj.args.agentblockedip == '') { config.settings.agentblockedip = obj.args.agentblockedip = null; } else { config.settings.agentblockedip = obj.args.agentblockedip = obj.args.agentblockedip.split(' ').join('').split(','); } }
- if (typeof obj.args.swarmallowedip == 'string') { if (obj.args.swarmallowedip == '') { obj.args.swarmallowedip = null; } else { obj.args.swarmallowedip = obj.args.swarmallowedip.split(' ').join('').split(','); } }
- if ((typeof obj.args.agentupdateblocksize == 'number') && (obj.args.agentupdateblocksize >= 1024) && (obj.args.agentupdateblocksize <= 65531)) { obj.agentUpdateBlockSize = obj.args.agentupdateblocksize; }
- if (typeof obj.args.trustedproxy == 'string') { obj.args.trustedproxy = obj.args.trustedproxy.split(' ').join('').split(','); }
- if (typeof obj.args.tlsoffload == 'string') { obj.args.tlsoffload = obj.args.tlsoffload.split(' ').join('').split(','); }
-
- // Check IP lists and ranges and if DNS return IP addresses
- config.settings.userallowedip = await resolveDomainsToIps(config.settings.userallowedip);
- config.settings.userblockedip = await resolveDomainsToIps(config.settings.userblockedip);
- config.settings.agentallowedip = await resolveDomainsToIps(config.settings.agentallowedip);
- config.settings.agentblockedip = await resolveDomainsToIps(config.settings.agentblockedip);
- config.settings.swarmallowedip = await resolveDomainsToIps(config.settings.swarmallowedip);
-
- // Check the "cookieIpCheck" value
- if ((obj.args.cookieipcheck === false) || (obj.args.cookieipcheck == 'none')) { obj.args.cookieipcheck = 'none'; }
- else if ((typeof obj.args.cookieipcheck != 'string') || (obj.args.cookieipcheck.toLowerCase() != 'strict')) { obj.args.cookieipcheck = 'lax'; }
- else { obj.args.cookieipcheck = 'strict'; }
-
- // Check the "cookieSameSite" value
- if (typeof obj.args.cookiesamesite != 'string') { delete obj.args.cookiesamesite; }
- else if (['none', 'lax', 'strict'].indexOf(obj.args.cookiesamesite.toLowerCase()) == -1) { delete obj.args.cookiesamesite; } else { obj.args.cookiesamesite = obj.args.cookiesamesite.toLowerCase(); }
-
- // Check if WebSocket compression is supported. It's known to be broken in NodeJS v11.11 to v12.15, and v13.2
- const verSplit = process.version.substring(1).split('.');
- const ver = parseInt(verSplit[0]) + (parseInt(verSplit[1]) / 100);
- if (((ver >= 11.11) && (ver <= 12.15)) || (ver == 13.2)) {
- if ((obj.args.wscompression === true) || (obj.args.agentwscompression === true)) { addServerWarning('WebSocket compression is disabled, this feature is broken in NodeJS v11.11 to v12.15 and v13.2', 4); }
- obj.args.wscompression = obj.args.agentwscompression = false;
- obj.config.settings.wscompression = obj.config.settings.agentwscompression = false;
- }
-
- // Local console tracing
- if (typeof obj.args.debug == 'string') { obj.debugSources = obj.args.debug.toLowerCase().split(','); }
- else if (typeof obj.args.debug == 'object') { obj.debugSources = obj.args.debug; }
- else if (obj.args.debug === true) { obj.debugSources = '*'; }
-
- require('./db.js').CreateDB(obj,
- function (db) {
- obj.db = db;
- obj.db.SetupDatabase(function (dbversion) {
- // See if any database operations needs to be completed
- if (obj.args.deletedomain) { obj.db.DeleteDomain(obj.args.deletedomain, function () { console.log('Deleted domain ' + obj.args.deletedomain + '.'); process.exit(); }); return; }
- if (obj.args.deletedefaultdomain) { obj.db.DeleteDomain('', function () { console.log('Deleted default domain.'); process.exit(); }); return; }
- if (obj.args.showall) { obj.db.GetAll(function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
- if (obj.args.showusers) { obj.db.GetAllType('user', function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
- if (obj.args.showitem) { obj.db.Get(obj.args.showitem, function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
- if (obj.args.listuserids) { obj.db.GetAllType('user', function (err, docs) { for (var i in docs) { console.log(docs[i]._id); } process.exit(); }); return; }
- if (obj.args.showusergroups) { obj.db.GetAllType('ugrp', function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
- if (obj.args.shownodes) { obj.db.GetAllType('node', function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
- if (obj.args.showallmeshes) { obj.db.GetAllType('mesh', function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
- if (obj.args.showmeshes) { obj.db.GetAllType('mesh', function (err, docs) { var x = []; for (var i in docs) { if (docs[i].deleted == null) { x.push(docs[i]); } } console.log(JSON.stringify(x, null, 2)); process.exit(); }); return; }
- if (obj.args.showevents) { obj.db.GetAllEvents(function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
- if (obj.args.showsmbios) { obj.db.GetAllSMBIOS(function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
- if (obj.args.showpower) { obj.db.getAllPower(function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
- if (obj.args.clearpower) { obj.db.removeAllPowerEvents(function () { process.exit(); }); return; }
- if (obj.args.showiplocations) { obj.db.GetAllType('iploc', function (err, docs) { console.log(docs); process.exit(); }); return; }
- if (obj.args.logintoken) { obj.getLoginToken(obj.args.logintoken, function (r) { console.log(r); process.exit(); }); return; }
- if (obj.args.logintokenkey) { obj.showLoginTokenKey(function (r) { console.log(r); process.exit(); }); return; }
- if (obj.args.recordencryptionrecode) { obj.db.performRecordEncryptionRecode(function (count) { console.log('Re-encoded ' + count + ' record(s).'); process.exit(); }); return; }
- if (obj.args.dbstats) { obj.db.getDbStats(function (stats) { console.log(stats); process.exit(); }); return; }
- if (obj.args.createaccount) { // Create a new user account
- if ((typeof obj.args.createaccount != 'string') || ((obj.args.pass == null) && (obj.args.hashpass == null)) || (obj.args.pass == '') || (obj.args.hashpass == '') || (obj.args.createaccount.indexOf(' ') >= 0)) { console.log("Usage: --createaccount [userid] --pass [password] --domain (domain) --email (email) --name (name)."); process.exit(); return; }
- var userid = 'user/' + (obj.args.domain ? obj.args.domain : '') + '/' + obj.args.createaccount.toLowerCase(), domainid = obj.args.domain ? obj.args.domain : '';
- if (obj.args.createaccount.startsWith('user/')) { userid = obj.args.createaccount; domainid = obj.args.createaccount.split('/')[1]; }
- if (userid.split('/').length != 3) { console.log("Invalid userid."); process.exit(); return; }
- obj.db.Get(userid, function (err, docs) {
- if (err != null) { console.log("Database error: " + err); process.exit(); return; }
- if ((docs != null) && (docs.length != 0)) { console.log('User already exists.'); process.exit(); return; }
- if ((domainid != '') && ((config.domains == null) || (config.domains[domainid] == null))) { console.log("Invalid domain."); process.exit(); return; }
- const user = { _id: userid, type: 'user', name: (typeof obj.args.name == 'string') ? obj.args.name : (userid.split('/')[2]), domain: domainid, creation: Math.floor(Date.now() / 1000), links: {} };
- if (typeof obj.args.email == 'string') { user.email = obj.args.email; user.emailVerified = true; }
- if (obj.args.hashpass) {
- // Create an account using a pre-hashed password. Use --hashpassword to pre-hash a password.
- var hashpasssplit = obj.args.hashpass.split(',');
- if (hashpasssplit.length != 2) { console.log("Invalid hashed password."); process.exit(); return; }
- user.salt = hashpasssplit[0];
- user.hash = hashpasssplit[1];
- obj.db.Set(user, function () { console.log("Done. This command will only work if MeshCentral is stopped."); process.exit(); return; });
- } else {
- // Hash the password and create the account.
- require('./pass').hash(obj.args.pass, function (err, salt, hash, tag) { if (err) { console.log("Unable create account password: " + err); process.exit(); return; } user.salt = salt; user.hash = hash; obj.db.Set(user, function () { console.log("Done."); process.exit(); return; }); }, 0);
- }
- });
- return;
- }
- if (obj.args.resetaccount) { // Unlock a user account, set a new password and remove 2FA
- if ((typeof obj.args.resetaccount != 'string') || (obj.args.resetaccount.indexOf(' ') >= 0)) { console.log("Usage: --resetaccount [userid] --domain (domain) --pass [password]."); process.exit(); return; }
- var userid = 'user/' + (obj.args.domain ? obj.args.domain : '') + '/' + obj.args.resetaccount.toLowerCase();
- if (obj.args.resetaccount.startsWith('user/')) { userid = obj.args.resetaccount; }
- if (userid.split('/').length != 3) { console.log("Invalid userid."); process.exit(); return; }
- obj.db.Get(userid, function (err, docs) {
- if (err != null) { console.log("Database error: " + err); process.exit(); return; }
- if ((docs == null) || (docs.length == 0)) { console.log("Unknown userid, usage: --resetaccount [userid] --domain (domain) --pass [password]."); process.exit(); return; }
- const user = docs[0]; if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { user.siteadmin -= 32; } // Unlock the account.
- delete user.phone; delete user.otpekey; delete user.otpsecret; delete user.otpkeys; delete user.otphkeys; delete user.otpdev; delete user.otpsms; delete user.otpmsg; user.otpduo; // Disable 2FA
- delete user.msghandle; // Disable users 2fa messaging too
- var config = getConfig(false);
- if (config.domains[user.domain].auth || config.domains[user.domain].authstrategies) {
- console.log('This users domain has external authentication methods enabled so the password will not be changed if you set one')
- obj.db.Set(user, function () { console.log("Done."); process.exit(); return; });
- } else {
- if (obj.args.hashpass && (typeof obj.args.hashpass == 'string')) {
- // Reset an account using a pre-hashed password. Use --hashpassword to pre-hash a password.
- var hashpasssplit = obj.args.hashpass.split(',');
- if (hashpasssplit.length != 2) { console.log("Invalid hashed password."); process.exit(); return; }
- user.salt = hashpasssplit[0];
- user.hash = hashpasssplit[1];
- obj.db.Set(user, function () { console.log("Done. This command will only work if MeshCentral is stopped."); process.exit(); return; });
- } else if (obj.args.pass && (typeof obj.args.pass == 'string')) {
- // Hash the password and reset the account.
- require('./pass').hash(String(obj.args.pass), user.salt, function (err, hash, tag) {
- if (err) { console.log("Unable to reset password: " + err); process.exit(); return; }
- user.hash = hash;
- obj.db.Set(user, function () { console.log("Done."); process.exit(); return; });
- }, 0);
- } else {
- console.log('Not setting a users password');
- obj.db.Set(user, function () { console.log("Done."); process.exit(); return; });
- }
- }
- });
- return;
- }
- if (obj.args.adminaccount) { // Set a user account to server administrator
- if ((typeof obj.args.adminaccount != 'string') || (obj.args.adminaccount.indexOf(' ') >= 0)) { console.log("Invalid userid, usage: --adminaccount [username] --domain (domain)"); process.exit(); return; }
- var userid = 'user/' + (obj.args.domain ? obj.args.domain : '') + '/' + obj.args.adminaccount.toLowerCase();
- if (obj.args.adminaccount.startsWith('user/')) { userid = obj.args.adminaccount; }
- if (userid.split('/').length != 3) { console.log("Invalid userid."); process.exit(); return; }
- obj.db.Get(userid, function (err, docs) {
- if (err != null) { console.log("Database error: " + err); process.exit(); return; }
- if ((docs == null) || (docs.length == 0)) { console.log("Unknown userid, usage: --adminaccount [userid] --domain (domain)."); process.exit(); return; }
- docs[0].siteadmin = 0xFFFFFFFF; // Set user as site administrator
- obj.db.Set(docs[0], function () { console.log("Done. This command will only work if MeshCentral is stopped."); process.exit(); return; });
- });
- return;
- }
- if (obj.args.removesubdomain) { // Remove all references to a sub domain from the database
- if ((typeof obj.args.removesubdomain != 'string') || (obj.args.removesubdomain.indexOf(' ') >= 0)) { console.log("Invalid sub domain, usage: --removesubdomain [domain]"); process.exit(); return; }
- obj.db.removeDomain(obj.args.removesubdomain, function () { console.log("Done."); process.exit(); return; });
- return;
- }
- if (obj.args.removetestagents) { // Remove all test agents from the database
- db.GetAllType('node', function (err, docs) {
- if ((err != null) || (docs.length == 0)) {
- console.log('Unable to get any nodes from the database');
- process.exit(0);
- } else {
- // Load all users
- const allusers = {}, removeCount = 0;
- obj.db.GetAllType('user', function (err, docs) {
- obj.common.unEscapeAllLinksFieldName(docs);
- for (i in docs) { allusers[docs[i]._id] = docs[i]; }
- });
-
- // Look at all devices
- for (var i in docs) {
- if ((docs[i] != null) && (docs[i].agent != null) && (docs[i].agent.id == 23)) {
- // Remove this test node
- const node = docs[i];
-
- // Delete this node including network interface information, events and timeline
- removeCount++;
- db.Remove(node._id); // Remove node with that id
- db.Remove('if' + node._id); // Remove interface information
- db.Remove('nt' + node._id); // Remove notes
- db.Remove('lc' + node._id); // Remove last connect time
- db.Remove('si' + node._id); // Remove system information
- if (db.RemoveSMBIOS) { db.RemoveSMBIOS(node._id); } // Remove SMBios data
- db.RemoveAllNodeEvents(node._id); // Remove all events for this node
- db.removeAllPowerEventsForNode(node._id); // Remove all power events for this node
- if (typeof node.pmt == 'string') { db.Remove('pmt_' + node.pmt); } // Remove Push Messaging Token
- db.Get('ra' + node._id, function (err, nodes) {
- if ((nodes != null) && (nodes.length == 1)) { db.Remove('da' + nodes[0].daid); } // Remove diagnostic agent to real agent link
- db.Remove('ra' + node._id); // Remove real agent to diagnostic agent link
- });
-
- // Remove any user node links
- if (node.links != null) {
- for (var i in node.links) {
- if (i.startsWith('user/')) {
- var cuser = allusers[i];
- if ((cuser != null) && (cuser.links != null) && (cuser.links[node._id] != null)) {
- // Remove the user link & save the user
- delete cuser.links[node._id];
- if (Object.keys(cuser.links).length == 0) { delete cuser.links; }
- db.SetUser(cuser);
- }
- }
- }
- }
-
- }
- }
- if (removeCount == 0) {
- console.log("Done, no devices removed.");
- process.exit(0);
- } else {
- console.log("Removed " + removeCount + " device(s), holding 10 seconds...");
- setTimeout(function () { console.log("Done."); process.exit(0); }, 10000)
- }
- }
- });
- return;
- }
-
- // Import NeDB data into database
- if (obj.args.nedbtodb) {
- if (db.databaseType == 1) { console.log("NeDB is current database, can't perform transfer."); process.exit(); return; }
- console.log("Transfering NeDB data into database...");
- db.nedbtodb(function (msg) { console.log(msg); process.exit(); })
- return;
- }
-
- // Show a list of all configuration files in the database
- if (obj.args.dblistconfigfiles) {
- obj.db.GetAllType('cfile', function (err, docs) {
- if (err == null) {
- if (docs.length == 0) {
- console.log("No files found.");
- } else {
- for (var i in docs) {
- if (typeof obj.args.dblistconfigfiles == 'string') {
- const data = obj.db.decryptData(obj.args.dblistconfigfiles, docs[i].data);
- if (data == null) {
- console.log(docs[i]._id.split('/')[1] + ', ' + Buffer.from(docs[i].data, 'base64').length + ' encrypted bytes - Unable to decrypt.');
- } else {
- console.log(docs[i]._id.split('/')[1] + ', ' + data.length + ' bytes, decoded correctly.');
- }
- } else {
- console.log(docs[i]._id.split('/')[1] + ', ' + Buffer.from(docs[i].data, 'base64').length + ' encrypted bytes.');
- }
- }
- }
- } else { console.log('Unable to read from database.'); } process.exit();
- });
- return;
- }
-
- // Display the content of a configuration file in the database
- if (obj.args.dbshowconfigfile) {
- if (typeof obj.args.configkey != 'string') { console.log("Error, --configkey is required."); process.exit(); return; }
- obj.db.getConfigFile(obj.args.dbshowconfigfile, function (err, docs) {
- if (err == null) {
- if (docs.length == 0) { console.log("File not found."); } else {
- const data = obj.db.decryptData(obj.args.configkey, docs[0].data);
- if (data == null) { console.log("Invalid config key."); } else { console.log(data); }
- }
- } else { console.log("Unable to read from database."); }
- process.exit();
- }); return;
- }
-
- // Delete all configuration files from database
- if (obj.args.dbdeleteconfigfiles) {
- console.log("Deleting all configuration files from the database..."); obj.db.RemoveAllOfType('cfile', function () { console.log('Done.'); process.exit(); });
- }
-
- // Push all relevent files from meshcentral-data into the database
- if (obj.args.dbpushconfigfiles) {
- if (typeof obj.args.configkey != 'string') { console.log("Error, --configkey is required."); process.exit(); return; }
- if ((obj.args.dbpushconfigfiles !== true) && (typeof obj.args.dbpushconfigfiles != 'string')) {
- console.log("Usage: --dbpulldatafiles (path) This will import files from folder into the database");
- console.log(" --dbpulldatafiles This will import files from meshcentral-data into the db.");
- process.exit();
- } else {
- if ((obj.args.dbpushconfigfiles == '*') || (obj.args.dbpushconfigfiles === true)) { obj.args.dbpushconfigfiles = obj.datapath; }
- obj.fs.readdir(obj.args.dbpushconfigfiles, function (err, files) {
- if (err != null) { console.log('ERROR: Unable to read from folder ' + obj.args.dbpushconfigfiles); process.exit(); return; }
- var configFound = false;
- for (var i in files) { if (files[i] == 'config.json') { configFound = true; } }
- if (configFound == false) { console.log('ERROR: No config.json in folder ' + obj.args.dbpushconfigfiles); process.exit(); return; }
- obj.db.RemoveAllOfType('cfile', function () {
- obj.fs.readdir(obj.args.dbpushconfigfiles, function (err, files) {
- var lockCount = 1
- for (var i in files) {
- const file = files[i];
- if ((file == 'config.json') || file.endsWith('.key') || file.endsWith('.crt') || (file == 'terms.txt') || file.endsWith('.jpg') || file.endsWith('.png')) {
- const path = obj.path.join(obj.args.dbpushconfigfiles, files[i]), binary = Buffer.from(obj.fs.readFileSync(path, { encoding: 'binary' }), 'binary');
- console.log('Pushing ' + file + ', ' + binary.length + ' bytes.');
- lockCount++;
- if (obj.args.oldencrypt) {
- obj.db.setConfigFile(file, obj.db.oldEncryptData(obj.args.configkey, binary), function () { if ((--lockCount) == 0) { console.log('Done.'); process.exit(); } });
- } else {
- obj.db.setConfigFile(file, obj.db.encryptData(obj.args.configkey, binary), function () { if ((--lockCount) == 0) { console.log('Done.'); process.exit(); } });
- }
- }
- }
- if (--lockCount == 0) { process.exit(); }
- });
- });
- });
- }
- return;
- }
-
- // Pull all database files into meshcentral-data
- if (obj.args.dbpullconfigfiles) {
- if (typeof obj.args.configkey != 'string') { console.log("Error, --configkey is required."); process.exit(); return; }
- if (typeof obj.args.dbpullconfigfiles != 'string') {
- console.log("Usage: --dbpulldatafiles (path)");
- process.exit();
- } else {
- obj.db.GetAllType('cfile', function (err, docs) {
- if (err == null) {
- if (docs.length == 0) {
- console.log("File not found.");
- } else {
- for (var i in docs) {
- const file = docs[i]._id.split('/')[1], binary = obj.db.decryptData(obj.args.configkey, docs[i].data);
- if (binary == null) {
- console.log("Invalid config key.");
- } else {
- const fullFileName = obj.path.join(obj.args.dbpullconfigfiles, file);
- try { obj.fs.writeFileSync(fullFileName, binary); } catch (ex) { console.log('Unable to write to ' + fullFileName); process.exit(); return; }
- console.log('Pulling ' + file + ', ' + binary.length + ' bytes.');
- }
- }
- }
- } else {
- console.log("Unable to read from database.");
- }
- process.exit();
- });
- }
- return;
- }
-
- if (obj.args.dbexport) {
- // Export the entire database to a JSON file
- if (obj.args.dbexport == true) { obj.args.dbexport = obj.getConfigFilePath('meshcentral.db.json'); }
- obj.db.GetAll(function (err, docs) {
- obj.fs.writeFileSync(obj.args.dbexport, JSON.stringify(docs));
- console.log('Exported ' + docs.length + ' objects(s) to ' + obj.args.dbexport + '.'); process.exit();
- });
- return;
- }
- if (obj.args.dbexportmin) {
- // Export a minimal database to a JSON file. Export only users, meshes and nodes.
- // This is a useful command to look at the database.
- if (obj.args.dbexportmin == true) { obj.args.dbexportmin = obj.getConfigFilePath('meshcentral.db.json'); }
- obj.db.GetAllType({ $in: ['user', 'node', 'mesh'] }, function (err, docs) {
- obj.fs.writeFileSync(obj.args.dbexportmin, JSON.stringify(docs));
- console.log('Exported ' + docs.length + ' objects(s) to ' + obj.args.dbexportmin + '.'); process.exit();
- });
- return;
- }
- if (obj.args.dbimport) {
- // Import the entire database from a JSON file
- if (obj.args.dbimport == true) { obj.args.dbimport = obj.getConfigFilePath('meshcentral.db.json'); }
- var json = null, json2 = '', badCharCount = 0;
- try { json = obj.fs.readFileSync(obj.args.dbimport, { encoding: 'utf8' }); } catch (ex) { console.log('Invalid JSON file: ' + obj.args.dbimport + ': ' + ex); process.exit(); }
- for (i = 0; i < json.length; i++) { if (json.charCodeAt(i) >= 32) { json2 += json[i]; } else { var tt = json.charCodeAt(i); if (tt != 10 && tt != 13) { badCharCount++; } } } // Remove all bad chars
- if (badCharCount > 0) { console.log(badCharCount + ' invalid character(s) where removed.'); }
- try { json = JSON.parse(json2); } catch (ex) { console.log('Invalid JSON format: ' + obj.args.dbimport + ': ' + e); process.exit(); }
- if ((json == null) || (typeof json.length != 'number') || (json.length < 1)) { console.log('Invalid JSON format: ' + obj.args.dbimport + '.'); }
- // Escape MongoDB invalid field chars
- for (i in json) {
- const doc = json[i];
- for (var j in doc) { if (j.indexOf('.') >= 0) { console.log("Invalid field name (" + j + ") in document: " + json[i]); return; } }
- //if ((json[i].type == 'ifinfo') && (json[i].netif2 != null)) { for (var j in json[i].netif2) { var esc = obj.common.escapeFieldName(j); if (esc !== j) { json[i].netif2[esc] = json[i].netif2[j]; delete json[i].netif2[j]; } } }
- //if ((json[i].type == 'mesh') && (json[i].links != null)) { for (var j in json[i].links) { var esc = obj.common.escapeFieldName(j); if (esc !== j) { json[i].links[esc] = json[i].links[j]; delete json[i].links[j]; } } }
- }
- //for (i in json) { if ((json[i].type == "node") && (json[i].host != null)) { json[i].rname = json[i].host; delete json[i].host; } } // DEBUG: Change host to rname
- setTimeout(function () { // If the Mongo database is being created for the first time, there is a race condition here. This will get around it.
- obj.db.RemoveAll(function () {
- obj.db.InsertMany(json, function (err) {
- if (err != null) { console.log(err); } else { console.log('Imported ' + json.length + ' objects(s) from ' + obj.args.dbimport + '.'); } process.exit();
- });
- });
- }, 100);
- return;
- }
- /*
- if (obj.args.dbimport) {
- // Import the entire database from a very large JSON file
- obj.db.RemoveAll(function () {
- if (obj.args.dbimport == true) { obj.args.dbimport = obj.getConfigFilePath('meshcentral.db.json'); }
- var json = null, json2 = "", badCharCount = 0;
- const StreamArray = require('stream-json/streamers/StreamArray');
- const jsonStream = StreamArray.withParser();
- jsonStream.on('data', function (data) { obj.db.Set(data.value); });
- jsonStream.on('end', () => { console.log('Done.'); process.exit(); });
- obj.fs.createReadStream(obj.args.dbimport).pipe(jsonStream.input);
- });
- return;
- }
- */
- if (obj.args.dbmerge) {
- // Import the entire database from a JSON file
- if (obj.args.dbmerge == true) { obj.args.dbmerge = obj.getConfigFilePath('meshcentral.db.json'); }
- var json = null, json2 = "", badCharCount = 0;
- try { json = obj.fs.readFileSync(obj.args.dbmerge, { encoding: 'utf8' }); } catch (ex) { console.log('Invalid JSON file: ' + obj.args.dbmerge + ': ' + ex); process.exit(); }
- for (i = 0; i < json.length; i++) { if (json.charCodeAt(i) >= 32) { json2 += json[i]; } else { var tt = json.charCodeAt(i); if (tt != 10 && tt != 13) { badCharCount++; } } } // Remove all bad chars
- if (badCharCount > 0) { console.log(badCharCount + ' invalid character(s) where removed.'); }
- try { json = JSON.parse(json2); } catch (ex) { console.log('Invalid JSON format: ' + obj.args.dbmerge + ': ' + ex); process.exit(); }
- if ((json == null) || (typeof json.length != 'number') || (json.length < 1)) { console.log('Invalid JSON format: ' + obj.args.dbimport + '.'); }
-
- // Get all users from current database
- obj.db.GetAllType('user', function (err, docs) {
- const users = {}, usersCount = 0;
- for (var i in docs) { users[docs[i]._id] = docs[i]; usersCount++; }
-
- // Fetch all meshes from the database
- obj.db.GetAllType('mesh', function (err, docs) {
- obj.common.unEscapeAllLinksFieldName(docs);
- const meshes = {}, meshesCount = 0;
- for (var i in docs) { meshes[docs[i]._id] = docs[i]; meshesCount++; }
- console.log('Loaded ' + usersCount + ' users and ' + meshesCount + ' meshes.');
- // Look at each object in the import file
- const objectToAdd = [];
- for (var i in json) {
- const newobj = json[i];
- if (newobj.type == 'user') {
- // Check if the user already exists
- var existingUser = users[newobj._id];
- if (existingUser) {
- // Merge the links
- if (typeof newobj.links == 'object') {
- for (var j in newobj.links) {
- if ((existingUser.links == null) || (existingUser.links[j] == null)) {
- if (existingUser.links == null) { existingUser.links = {}; }
- existingUser.links[j] = newobj.links[j];
- }
- }
- }
- if (existingUser.name == 'admin') { existingUser.links = {}; }
- objectToAdd.push(existingUser); // Add this user
- } else {
- objectToAdd.push(newobj); // Add this user
- }
- } else if (newobj.type == 'mesh') {
- // Add this object
- objectToAdd.push(newobj);
- } // Don't add nodes.
- }
- console.log('Importing ' + objectToAdd.length + ' object(s)...');
- var pendingCalls = 1;
- for (var i in objectToAdd) {
- pendingCalls++;
- obj.db.Set(objectToAdd[i], function (err) { if (err != null) { console.log(err); } else { if (--pendingCalls == 0) { process.exit(); } } });
- }
- if (--pendingCalls == 0) { process.exit(); }
- });
- });
- return;
- }
-
- // Check if the database is capable of performing a backup
- // Moved behind autobackup config init in startex4: obj.db.checkBackupCapability(function (err, msg) { if (msg != null) { obj.addServerWarning(msg, true) } });
-
- // Load configuration for database if needed
- if (obj.args.loadconfigfromdb) {
- var key = null;
- if (typeof obj.args.configkey == 'string') { key = obj.args.configkey; }
- else if (typeof obj.args.loadconfigfromdb == 'string') { key = obj.args.loadconfigfromdb; }
- if (key == null) { console.log("Error, --configkey is required."); process.exit(); return; }
- obj.db.getAllConfigFiles(key, function (configFiles) {
- if (configFiles == null) { console.log("Error, no configuration files found or invalid configkey."); process.exit(); return; }
- if (!configFiles['config.json']) { console.log("Error, could not file config.json from database."); process.exit(); return; }
- if (typeof configFiles['config.json'] == 'object') { configFiles['config.json'] = configFiles['config.json'].toString(); }
- if (configFiles['config.json'].charCodeAt(0) == 65279) { configFiles['config.json'] = configFiles['config.json'].substring(1); }
- obj.configurationFiles = configFiles;
-
- // Parse the new configuration file
- var config2 = null;
- try { config2 = JSON.parse(configFiles['config.json']); } catch (ex) { console.log('Error, unable to parse config.json from database.', ex); process.exit(); return; }
-
- // Set the command line arguments to the config file if they are not present
- if (!config2.settings) { config2.settings = {}; }
- for (i in args) { config2.settings[i] = args[i]; }
-
- // Lower case all keys in the config file
- common.objKeysToLower(config2, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'telegram/proxy']);
-
- // Grab some of the values from the original config.json file if present.
- config2['mysql'] = config['mysql'];
- config2['mariadb'] = config['mariadb'];
- config2['mongodb'] = config['mongodb'];
- config2['mongodbcol'] = config['mongodbcol'];
- config2['dbencryptkey'] = config['dbencryptkey'];
- config2['acebase'] = config['acebase'];
- config2['sqlite3'] = config['sqlite3'];
-
- // We got a new config.json from the database, let's use it.
- config = obj.config = config2;
- obj.StartEx1b();
- });
- } else {
- config = obj.config = getConfig(obj.args.vault == null);
- obj.StartEx1b();
- }
- });
- }
- );
- };
-
- // Time to start the server of real.
- obj.StartEx1b = async function () {
- var i;
-
- // Add NodeJS version warning if needed
- if (Number(process.version.match(/^v(\d+\.\d+)/)[1]) < 16) { addServerWarning("MeshCentral will require Node v16 or above in the future, your current version is " + process.version + "."); }
-
- // Setup certificate operations
- obj.certificateOperations = require('./certoperations.js').CertificateOperations(obj);
-
- // Linux format /var/log/auth.log
- if (obj.config.settings.authlog != null) {
- obj.fs.open(obj.config.settings.authlog, 'a', function (err, fd) {
- if (err == null) { obj.authlogfile = fd; } else { console.log('ERROR: Unable to open: ' + obj.config.settings.authlog); }
- })
- }
-
- // Start CrowdSec bouncer if needed: https://www.crowdsec.net/
- if (typeof obj.args.crowdsec == 'object') { obj.crowdSecBounser = require('./crowdsec.js').CreateCrowdSecBouncer(obj, obj.args.crowdsec); }
-
- // Check if self update is allowed. If running as a Windows service, self-update is not possible.
- if (obj.fs.existsSync(obj.path.join(__dirname, 'daemon'))) { obj.serverSelfWriteAllowed = false; }
-
- // If we are targetting a specific version, update now.
- if ((obj.serverSelfWriteAllowed == true) && (typeof obj.args.selfupdate == 'string')) {
- obj.args.selfupdate = obj.args.selfupdate.toLowerCase();
- if (getCurrentVersion() !== obj.args.selfupdate) { obj.performServerUpdate(); return; } // We are targetting a specific version, run self update now.
- }
-
- // Write the server state
- obj.updateServerState('state', 'starting');
- if (process.pid) { obj.updateServerState('server-pid', process.pid); }
- if (process.ppid) { obj.updateServerState('server-parent-pid', process.ppid); }
-
- // Read environment variables. For a subset of arguments, we allow them to be read from environment variables.
- const xenv = ['user', 'port', 'mpsport', 'mpsaliasport', 'redirport', 'rediraliasport', 'exactport', 'debug'];
- for (i in xenv) { if ((obj.args[xenv[i]] == null) && (process.env['mesh' + xenv[i]])) { obj.args[xenv[i]] = obj.common.toNumber(process.env['mesh' + xenv[i]]); } }
-
- // Validate the domains, this is used for multi-hosting
- if (obj.config.domains == null) { obj.config.domains = {}; }
- if (obj.config.domains[''] == null) { obj.config.domains[''] = {}; }
- if (obj.config.domains[''].dns != null) { console.log("ERROR: Default domain can't have a DNS name."); return; }
- var xdomains = {}; for (i in obj.config.domains) { xdomains[i.toLowerCase()] = obj.config.domains[i]; } obj.config.domains = xdomains;
- var bannedDomains = ['public', 'private', 'images', 'scripts', 'styles', 'views']; // List of banned domains
- for (i in obj.config.domains) { for (var j in bannedDomains) { if (i == bannedDomains[j]) { console.log("ERROR: Domain '" + i + "' is not allowed domain name in config.json."); delete obj.config.domains[i]; } } }
- for (i in obj.config.domains) { if ((i.length > 64) || (Buffer.from(i).length > 64)) { console.log("ERROR: Domain '" + i + "' is longer that 64 bytes, this is not allowed."); delete obj.config.domains[i]; } }
- for (i in obj.config.domains) {
- // Remove any domains that start with underscore
- if (i.startsWith('_')) { delete obj.config.domains[i]; continue; }
-
- // Apply default domain settings if present
- if (typeof obj.config.domaindefaults == 'object') { for (var j in obj.config.domaindefaults) { if (obj.config.domains[i][j] == null) { obj.config.domains[i][j] = obj.config.domaindefaults[j]; } } }
-
- // Perform domain setup
- if (typeof obj.config.domains[i] != 'object') { console.log("ERROR: Invalid domain configuration in config.json."); process.exit(); return; }
- if ((i.length > 0) && (i[0] == '_')) { delete obj.config.domains[i]; continue; } // Remove any domains with names that start with _
- if (typeof config.domains[i].auth == 'string') { config.domains[i].auth = config.domains[i].auth.toLowerCase(); }
- if (obj.config.domains[i].limits == null) { obj.config.domains[i].limits = {}; }
- if (obj.config.domains[i].preventduplicatedevices == null) { obj.config.domains[i].preventduplicatedevices = false; }
- if (obj.config.domains[i].dns == null) { obj.config.domains[i].url = (i == '') ? '/' : ('/' + i + '/'); } else { obj.config.domains[i].url = '/'; }
- obj.config.domains[i].id = i;
- if ((typeof obj.config.domains[i].maxdeviceview != 'number') || (obj.config.domains[i].maxdeviceview < 1)) { delete obj.config.domains[i].maxdeviceview; }
- if (typeof obj.config.domains[i].loginkey == 'string') { obj.config.domains[i].loginkey = [obj.config.domains[i].loginkey]; }
- if ((obj.config.domains[i].loginkey != null) && (obj.common.validateAlphaNumericArray(obj.config.domains[i].loginkey, 1, 128) == false)) { console.log("ERROR: Invalid login key, must be alpha-numeric string with no spaces."); process.exit(); return; }
- if (typeof obj.config.domains[i].agentkey == 'string') { obj.config.domains[i].agentkey = [obj.config.domains[i].agentkey]; }
- if ((obj.config.domains[i].agentkey != null) && (obj.common.validateAlphaNumericArray(obj.config.domains[i].agentkey, 1, 128) == false)) { console.log("ERROR: Invalid agent key, must be alpha-numeric string with no spaces."); process.exit(); return; }
- obj.config.domains[i].userallowedip = obj.config.domains[i].userallowedip = readIpListFromFile(obj.config.domains[i].userallowedip);
- obj.config.domains[i].userblockedip = obj.config.domains[i].userblockedip = readIpListFromFile(obj.config.domains[i].userblockedip);
- obj.config.domains[i].agentallowedip = obj.config.domains[i].agentallowedip = readIpListFromFile(obj.config.domains[i].agentallowedip);
- obj.config.domains[i].agentblockedip = obj.config.domains[i].agentblockedip = readIpListFromFile(obj.config.domains[i].agentblockedip);
- if (typeof obj.config.domains[i].userallowedip == 'string') { if (obj.config.domains[i].userallowedip == '') { delete obj.config.domains[i].userallowedip; } else { obj.config.domains[i].userallowedip = obj.config.domains[i].userallowedip.split(' ').join('').split(','); } }
- if (typeof obj.config.domains[i].userblockedip == 'string') { if (obj.config.domains[i].userblockedip == '') { delete obj.config.domains[i].userblockedip; } else { obj.config.domains[i].userblockedip = obj.config.domains[i].userblockedip.split(' ').join('').split(','); } }
- if (typeof obj.config.domains[i].agentallowedip == 'string') { if (obj.config.domains[i].agentallowedip == '') { delete obj.config.domains[i].agentallowedip; } else { obj.config.domains[i].agentallowedip = obj.config.domains[i].agentallowedip.split(' ').join('').split(','); } }
- if (typeof obj.config.domains[i].agentblockedip == 'string') { if (obj.config.domains[i].agentblockedip == '') { delete obj.config.domains[i].agentblockedip; } else { obj.config.domains[i].agentblockedip = obj.config.domains[i].agentblockedip.split(' ').join('').split(','); } }
- // Check IP lists and ranges and if DNS return IP addresses
- obj.config.domains[i].userallowedip = await resolveDomainsToIps(obj.config.domains[i].userallowedip);
- obj.config.domains[i].userblockedip = await resolveDomainsToIps(obj.config.domains[i].userblockedip);
- obj.config.domains[i].agentallowedip = await resolveDomainsToIps(obj.config.domains[i].agentallowedip);
- obj.config.domains[i].agentblockedip = await resolveDomainsToIps(obj.config.domains[i].agentblockedip);
- if (typeof obj.config.domains[i].ignoreagenthashcheck == 'string') { if (obj.config.domains[i].ignoreagenthashcheck == '') { delete obj.config.domains[i].ignoreagenthashcheck; } else { obj.config.domains[i].ignoreagenthashcheck = obj.config.domains[i].ignoreagenthashcheck.split(','); } }
- if (typeof obj.config.domains[i].allowedorigin == 'string') { if (obj.config.domains[i].allowedorigin == '') { delete obj.config.domains[i].allowedorigin; } else { obj.config.domains[i].allowedorigin = obj.config.domains[i].allowedorigin.split(','); } }
- if ((obj.config.domains[i].passwordrequirements != null) && (typeof obj.config.domains[i].passwordrequirements == 'object')) {
- if (typeof obj.config.domains[i].passwordrequirements.skip2factor == 'string') {
- obj.config.domains[i].passwordrequirements.skip2factor = obj.config.domains[i].passwordrequirements.skip2factor.split(',');
- } else {
- delete obj.config.domains[i].passwordrequirements.skip2factor;
- }
- // Fix the list of users to add "user/domain/" if needed
- if (Array.isArray(obj.config.domains[i].passwordrequirements.logintokens)) {
- var newValues = [];
- for (var j in obj.config.domains[i].passwordrequirements.logintokens) {
- var splitVal = obj.config.domains[i].passwordrequirements.logintokens[j].split('/');;
- if (splitVal.length == 1) { newValues.push('user/' + i + '/' + splitVal[0]); }
- if (splitVal.length == 2) { newValues.push('user/' + splitVal[0] + '/' + splitVal[1]); }
- if (splitVal.length == 3) { newValues.push(splitVal[0] + '/' + splitVal[1] + '/' + splitVal[2]); }
- }
- obj.config.domains[i].passwordrequirements.logintokens = newValues;
- }
- }
- if ((obj.config.domains[i].auth == 'ldap') && (typeof obj.config.domains[i].ldapoptions != 'object')) {
- if (i == '') { console.log("ERROR: Default domain is LDAP, but is missing LDAPOptions."); } else { console.log("ERROR: Domain '" + i + "' is LDAP, but is missing LDAPOptions."); }
- process.exit();
- return;
- }
- if ((obj.config.domains[i].auth == 'ldap') || (obj.config.domains[i].auth == 'sspi')) { obj.config.domains[i].newaccounts = 0; } // No new accounts allowed in SSPI/LDAP authentication modes.
- if (obj.config.domains[i].sitestyle == null) { obj.config.domains[i].sitestyle = 2; } // Default to site style #2
-
- // Convert newAccountsRights from a array of strings to flags number.
- obj.config.domains[i].newaccountsrights = obj.common.meshServerRightsArrayToNumber(obj.config.domains[i].newaccountsrights);
- if (typeof (obj.config.domains[i].newaccountsrights) != 'number') { delete obj.config.domains[i].newaccountsrights; }
-
- // Check if there is a web views path and/or web public path for this domain
- if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) {
- if ((obj.config.domains[i].webviewspath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web-' + i + '/views')))) { obj.config.domains[i].webviewspath = obj.path.join(__dirname, '../../meshcentral-web-' + i + '/views'); }
- if ((obj.config.domains[i].webpublicpath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web-' + i + '/public')))) { obj.config.domains[i].webpublicpath = obj.path.join(__dirname, '../../meshcentral-web-' + i + '/public'); }
- if ((obj.config.domains[i].webemailspath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web-' + i + '/emails')))) { obj.config.domains[i].webemailspath = obj.path.join(__dirname, '../../meshcentral-web-' + i + '/emails'); }
- } else {
- if ((obj.config.domains[i].webviewspath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web-' + i + '/views')))) { obj.config.domains[i].webviewspath = obj.path.join(__dirname, '../meshcentral-web-' + i + '/views'); }
- if ((obj.config.domains[i].webpublicpath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web-' + i + '/public')))) { obj.config.domains[i].webpublicpath = obj.path.join(__dirname, '../meshcentral-web-' + i + '/public'); }
- if ((obj.config.domains[i].webemailspath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web-' + i + '/emails')))) { obj.config.domains[i].webemailspath = obj.path.join(__dirname, '../meshcentral-web-' + i + '/emails'); }
- }
-
- // Check agent customization if any
- if (typeof obj.config.domains[i].agentcustomization == 'object') {
- if (typeof obj.config.domains[i].agentcustomization.displayname != 'string') { delete obj.config.domains[i].agentcustomization.displayname; } else { obj.config.domains[i].agentcustomization.displayname = obj.config.domains[i].agentcustomization.displayname.split('\r').join('').split('\n').join(''); }
- if (typeof obj.config.domains[i].agentcustomization.description != 'string') { delete obj.config.domains[i].agentcustomization.description; } else { obj.config.domains[i].agentcustomization.description = obj.config.domains[i].agentcustomization.description.split('\r').join('').split('\n').join(''); }
- if (typeof obj.config.domains[i].agentcustomization.companyname != 'string') { delete obj.config.domains[i].agentcustomization.companyname; } else { obj.config.domains[i].agentcustomization.companyname = obj.config.domains[i].agentcustomization.companyname.split('\r').join('').split('\n').join(''); }
- if (typeof obj.config.domains[i].agentcustomization.servicename != 'string') { delete obj.config.domains[i].agentcustomization.servicename; } else { obj.config.domains[i].agentcustomization.servicename = obj.config.domains[i].agentcustomization.servicename.split('\r').join('').split('\n').join('').split(' ').join('').split('"').join('').split('\'').join('').split('>').join('').split('<').join('').split('/').join('').split('\\').join(''); }
- if (typeof obj.config.domains[i].agentcustomization.image != 'string') { delete obj.config.domains[i].agentcustomization.image; } else { try { obj.config.domains[i].agentcustomization.image = 'data:image/png;base64,' + Buffer.from(obj.fs.readFileSync(obj.getConfigFilePath(obj.config.domains[i].agentcustomization.image)), 'binary').toString('base64'); } catch (ex) { console.log(ex); delete obj.config.domains[i].agentcustomization.image; } }
- } else {
- delete obj.config.domains[i].agentcustomization;
- }
-
- // Convert user consent flags
- if (typeof obj.config.domains[i].userconsentflags == 'object') {
- var flags = 0;
- if (obj.config.domains[i].userconsentflags.desktopnotify == true) { flags |= 1; }
- if (obj.config.domains[i].userconsentflags.terminalnotify == true) { flags |= 2; }
- if (obj.config.domains[i].userconsentflags.filenotify == true) { flags |= 4; }
- if (obj.config.domains[i].userconsentflags.desktopprompt == true) { flags |= 8; }
- if (obj.config.domains[i].userconsentflags.terminalprompt == true) { flags |= 16; }
- if (obj.config.domains[i].userconsentflags.fileprompt == true) { flags |= 32; }
- if (obj.config.domains[i].userconsentflags.desktopprivacybar == true) { flags |= 64; }
- obj.config.domains[i].userconsentflags = flags;
- }
-
- // If we have Intel AMT manager settings, take a look at them here.
- if (typeof obj.config.domains[i].amtmanager == 'object') {
- if (typeof obj.config.domains[i].amtmanager.tlsrootcert == 'object') {
- obj.config.domains[i].amtmanager.tlsrootcert2 = obj.certificateOperations.loadGenericCertAndKey(obj.config.domains[i].amtmanager.tlsrootcert);
- if (obj.config.domains[i].amtmanager.tlsrootcert2 == null) { // Show an error message if needed
- if (i == '') {
- addServerWarning("Unable to load Intel AMT TLS root certificate for default domain.", 5);
- } else {
- addServerWarning("Unable to load Intel AMT TLS root certificate for domain " + i + ".", 6, [i]);
- }
- }
- }
- }
-
- // Check agentfileinfo
- if (typeof obj.config.domains[i].agentfileinfo == 'object') {
- if ((obj.config.domains[i].agentfileinfo.fileversionnumber != null) && (obj.common.parseVersion(obj.config.domains[i].agentfileinfo.fileversionnumber) == null)) { delete obj.config.domains[i].agentfileinfo.fileversionnumber; }
- if ((obj.config.domains[i].agentfileinfo.productversionnumber != null) && (obj.common.parseVersion(obj.config.domains[i].agentfileinfo.productversionnumber) == null)) { delete obj.config.domains[i].agentfileinfo.productversionnumber; }
- if ((obj.config.domains[i].agentfileinfo.fileversionnumber == null) && (typeof obj.config.domains[i].agentfileinfo.fileversion == 'string') && (obj.common.parseVersion(obj.config.domains[i].agentfileinfo.fileversion) != null)) { obj.config.domains[i].agentfileinfo.fileversionnumber = obj.config.domains[i].agentfileinfo.fileversion; }
- if (typeof obj.config.domains[i].agentfileinfo.icon == 'string') {
- // Load the agent .ico file
- var icon = null;
- try { icon = require('./authenticode.js').loadIcon(obj.path.join(obj.datapath, obj.config.domains[i].agentfileinfo.icon)); } catch (ex) { }
- if (icon != null) {
- // The icon file was correctly loaded
- obj.config.domains[i].agentfileinfo.icon = icon;
- } else {
- // Failed to load the icon file, display a server warning
- addServerWarning("Unable to load agent icon file: " + obj.config.domains[i].agentfileinfo.icon + ".", 23, [obj.config.domains[i].agentfileinfo.icon]);
- delete obj.config.domains[i].agentfileinfo.icon;
- }
- } else {
- // Invalid icon file path
- delete obj.config.domains[i].agentfileinfo.icon;
- }
- if (typeof obj.config.domains[i].agentfileinfo.logo == 'string') {
- // Load the agent .bmp file
- var logo = null;
- try { logo = require('./authenticode.js').loadBitmap(obj.path.join(obj.datapath, obj.config.domains[i].agentfileinfo.logo)); } catch (ex) { }
- if (logo != null) {
- // The logo file was correctly loaded
- obj.config.domains[i].agentfileinfo.logo = logo;
- } else {
- // Failed to load the icon file, display a server warning
- addServerWarning("Unable to load agent logo file: " + obj.config.domains[i].agentfileinfo.logo + ".", 24, [obj.config.domains[i].agentfileinfo.logo]);
- delete obj.config.domains[i].agentfileinfo.logo;
- }
- } else {
- // Invalid icon file path
- delete obj.config.domains[i].agentfileinfo.logo;
- }
- }
- }
-
- // Log passed arguments into Windows Service Log
- //if (obj.servicelog != null) { var s = ''; for (i in obj.args) { if (i != '_') { if (s.length > 0) { s += ', '; } s += i + "=" + obj.args[i]; } } logInfoEvent('MeshServer started with arguments: ' + s); }
-
- // Look at passed in arguments
- if ((obj.args.user != null) && (typeof obj.args.user != 'string')) { delete obj.args.user; }
- if ((obj.args.ciralocalfqdn != null) && ((obj.args.lanonly == true) || (obj.args.wanonly == true))) { addServerWarning("CIRA local FQDN's ignored when server in LAN-only or WAN-only mode.", 7); }
- if ((obj.args.ciralocalfqdn != null) && (obj.args.ciralocalfqdn.split(',').length > 4)) { addServerWarning("Can't have more than 4 CIRA local FQDN's. Ignoring value.", 8); obj.args.ciralocalfqdn = null; }
- if (obj.args.ignoreagenthashcheck === true) { addServerWarning("Agent hash checking is being skipped, this is unsafe.", 9); }
- if (obj.args.port == null || typeof obj.args.port != 'number') { obj.args.port = 443; }
- if (obj.args.aliasport != null && (typeof obj.args.aliasport != 'number')) obj.args.aliasport = null;
- if (obj.args.mpsport == null || typeof obj.args.mpsport != 'number') obj.args.mpsport = 4433;
- if (obj.args.mpsaliasport != null && (typeof obj.args.mpsaliasport != 'number')) obj.args.mpsaliasport = null;
- if (obj.args.rediraliasport != null && (typeof obj.args.rediraliasport != 'number')) obj.args.rediraliasport = null;
- if (obj.args.redirport == null) obj.args.redirport = 80;
- if (obj.args.minifycore == null) obj.args.minifycore = false;
- if (typeof obj.args.agentidletimeout != 'number') { obj.args.agentidletimeout = 150000; } else { obj.args.agentidletimeout *= 1000 } // Default agent idle timeout is 2m, 30sec.
- if ((obj.args.lanonly != true) && (typeof obj.args.webrtconfig == 'object')) { // fix incase you are using an old mis-spelt webrtconfig
- obj.args.webrtcconfig = obj.args.webrtconfig;
- delete obj.args.webrtconfig;
- }
- if ((obj.args.lanonly != true) && (obj.args.webrtcconfig == null)) { obj.args.webrtcconfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun.cloudflare.com:3478' }] }; } // Setup default WebRTC STUN servers
- else if ((obj.args.lanonly != true) && (typeof obj.args.webrtcconfig == 'object')) {
- if (obj.args.webrtcconfig.iceservers) { // webrtc is case-sensitive, so must rename iceservers to iceServers!
- obj.args.webrtcconfig.iceServers = obj.args.webrtcconfig.iceservers;
- delete obj.args.webrtcconfig.iceservers;
- }
- }
- if (typeof obj.args.ignoreagenthashcheck == 'string') { if (obj.args.ignoreagenthashcheck == '') { delete obj.args.ignoreagenthashcheck; } else { obj.args.ignoreagenthashcheck = obj.args.ignoreagenthashcheck.split(','); } }
-
- // Setup a site administrator
- if ((obj.args.admin) && (typeof obj.args.admin == 'string')) {
- var adminname = obj.args.admin.split('/');
- if (adminname.length == 1) { adminname = 'user//' + adminname[0]; }
- else if (adminname.length == 2) { adminname = 'user/' + adminname[0] + '/' + adminname[1]; }
- else { console.log("Invalid administrator name."); process.exit(); return; }
- obj.db.Get(adminname, function (err, user) {
- if (user.length != 1) { console.log("Invalid user name."); process.exit(); return; }
- user[0].siteadmin = 4294967295; // 0xFFFFFFFF
- obj.db.Set(user[0], function () {
- if (user[0].domain == '') { console.log('User ' + user[0].name + ' set to site administrator.'); } else { console.log("User " + user[0].name + " of domain " + user[0].domain + " set to site administrator."); }
- process.exit();
- return;
- });
- });
- return;
- }
-
- // Remove a site administrator
- if ((obj.args.unadmin) && (typeof obj.args.unadmin == 'string')) {
- var adminname = obj.args.unadmin.split('/');
- if (adminname.length == 1) { adminname = 'user//' + adminname[0]; }
- else if (adminname.length == 2) { adminname = 'user/' + adminname[0] + '/' + adminname[1]; }
- else { console.log("Invalid administrator name."); process.exit(); return; }
- obj.db.Get(adminname, function (err, user) {
- if (user.length != 1) { console.log("Invalid user name."); process.exit(); return; }
- if (user[0].siteadmin) { delete user[0].siteadmin; }
- obj.db.Set(user[0], function () {
- if (user[0].domain == '') { console.log("User " + user[0].name + " is not a site administrator."); } else { console.log("User " + user[0].name + " of domain " + user[0].domain + " is not a site administrator."); }
- process.exit();
- return;
- });
- });
- return;
- }
-
- // Setup agent error log
- if ((obj.config) && (obj.config.settings) && (obj.config.settings.agentlogdump)) {
- obj.fs.open(obj.path.join(obj.datapath, 'agenterrorlogs.txt'), 'a', function (err, fd) { obj.agentErrorLog = fd; })
- }
-
- // Perform other database cleanup
- obj.db.cleanup();
-
- // Set all nodes to power state of unknown (0)
- obj.db.storePowerEvent({ time: new Date(), nodeid: '*', power: 0, s: 1 }, obj.multiServer); // s:1 indicates that the server is starting up.
-
- // Read or setup database configuration values
- obj.db.Get('dbconfig', function (err, dbconfig) {
- if ((dbconfig != null) && (dbconfig.length == 1)) { obj.dbconfig = dbconfig[0]; } else { obj.dbconfig = { _id: 'dbconfig', version: 1 }; }
- if (obj.dbconfig.amtWsEventSecret == null) { obj.crypto.randomBytes(32, function (err, buf) { obj.dbconfig.amtWsEventSecret = buf.toString('hex'); obj.db.Set(obj.dbconfig); }); }
-
- // This is used by the user to create a username/password for a Intel AMT WSMAN event subscription
- if (obj.args.getwspass) {
- if (obj.args.getwspass.length == 64) {
- obj.crypto.randomBytes(6, function (err, buf) {
- while (obj.dbconfig.amtWsEventSecret == null) { process.nextTick(); }
- const username = buf.toString('hex');
- const nodeid = obj.args.getwspass;
- const pass = obj.crypto.createHash('sha384').update(username.toLowerCase() + ':' + nodeid + ':' + obj.dbconfig.amtWsEventSecret).digest('base64').substring(0, 12).split('/').join('x').split('\\').join('x');
- console.log("--- Intel(r) AMT WSMAN eventing credentials ---");
- console.log("Username: " + username);
- console.log("Password: " + pass);
- console.log("Argument: " + nodeid);
- process.exit();
- });
- } else {
- console.log("Invalid NodeID.");
- process.exit();
- }
- return;
- }
-
- // Setup the task manager
- if ((obj.config) && (obj.config.settings) && (obj.config.settings.taskmanager == true)) {
- obj.taskManager = require('./taskmanager').createTaskManager(obj);
- }
-
- // Start plugin manager if configuration allows this.
- if ((obj.config) && (obj.config.settings) && (obj.config.settings.plugins != null) && (obj.config.settings.plugins != false) && ((typeof obj.config.settings.plugins != 'object') || (obj.config.settings.plugins.enabled != false))) {
- obj.pluginHandler = require('./pluginHandler.js').pluginHandler(obj);
- }
-
- // Load the default meshcore and meshcmd
- obj.updateMeshCore();
- obj.updateMeshCmd();
-
- // Setup and start the redirection server if needed. We must start the redirection server before Let's Encrypt.
- if ((obj.args.redirport != null) && (typeof obj.args.redirport == 'number') && (obj.args.redirport != 0)) {
- obj.redirserver = require('./redirserver.js').CreateRedirServer(obj, obj.db, obj.args, obj.StartEx2);
- } else {
- obj.StartEx2(); // If not needed, move on.
- }
- });
- }
-
- // Done starting the redirection server, go on to load the server certificates
- obj.StartEx2 = function () {
- // Load server certificates
- obj.certificateOperations.GetMeshServerCertificate(obj.args, obj.config, function (certs) {
- // Get the current node version
- if ((obj.config.letsencrypt == null) || (obj.redirserver == null)) {
- obj.StartEx3(certs); // Just use the configured certificates
- } else if ((obj.config.letsencrypt != null) && (obj.config.letsencrypt.nochecks == true)) {
- // Use Let's Encrypt with no checking
- obj.letsencrypt = require('./letsencrypt.js').CreateLetsEncrypt(obj);
- obj.letsencrypt.getCertificate(certs, obj.StartEx3); // Use Let's Encrypt with no checking, use at your own risk.
- } else {
- // Check Let's Encrypt settings
- var leok = true;
- if ((typeof obj.config.letsencrypt.names != 'string') && (typeof obj.config.settings.cert == 'string')) { obj.config.letsencrypt.names = obj.config.settings.cert; }
- if (typeof obj.config.letsencrypt.email != 'string') { leok = false; addServerWarning("Missing Let's Encrypt email address.", 10); }
- else if (typeof obj.config.letsencrypt.names != 'string') { leok = false; addServerWarning("Invalid Let's Encrypt host names.", 11); }
- else if (obj.config.letsencrypt.names.indexOf('*') >= 0) { leok = false; addServerWarning("Invalid Let's Encrypt names, can't contain a *.", 12); }
- else if (obj.config.letsencrypt.email.split('@').length != 2) { leok = false; addServerWarning("Invalid Let's Encrypt email address.", 10); }
- else if (obj.config.letsencrypt.email.trim() !== obj.config.letsencrypt.email) { leok = false; addServerWarning("Invalid Let's Encrypt email address.", 10); }
- else {
- const le = require('./letsencrypt.js');
- try { obj.letsencrypt = le.CreateLetsEncrypt(obj); } catch (ex) { console.log(ex); }
- if (obj.letsencrypt == null) { addServerWarning("Unable to setup Let's Encrypt module.", 13); leok = false; }
- }
- if (leok == true) {
- // Check that the email address domain MX resolves.
- require('dns').resolveMx(obj.config.letsencrypt.email.split('@')[1], function (err, addresses) {
- if (err == null) {
- // Check that all names resolve
- checkResolveAll(obj.config.letsencrypt.names.split(','), function (err) {
- if (err == null) {
- obj.letsencrypt.getCertificate(certs, obj.StartEx3); // Use Let's Encrypt
- } else {
- for (var i in err) { addServerWarning("Invalid Let's Encrypt names, unable to resolve: " + err[i], 14, [err[i]]); }
- obj.StartEx3(certs); // Let's Encrypt did not load, just use the configured certificates
- }
- });
- } else {
- addServerWarning("Invalid Let's Encrypt email address, unable to resolve: " + obj.config.letsencrypt.email.split('@')[1], 15, [obj.config.letsencrypt.email.split('@')[1]]);
- obj.StartEx3(certs); // Let's Encrypt did not load, just use the configured certificates
- }
- });
- } else {
- obj.StartEx3(certs); // Let's Encrypt did not load, just use the configured certificates
- }
- }
- });
- };
-
- // Start the server with the given certificates, but check if we have web certificates to load
- obj.StartEx3 = function (certs) {
- obj.certificates = certs;
- obj.certificateOperations.acceleratorStart(certs); // Set the state of the accelerators
-
- // Load any domain web certificates
- for (var i in obj.config.domains) {
- // Load any Intel AMT ACM activation certificates
- if (obj.config.domains[i].amtacmactivation == null) { obj.config.domains[i].amtacmactivation = {}; }
- obj.certificateOperations.loadIntelAmtAcmCerts(obj.config.domains[i].amtacmactivation);
- if (obj.config.domains[i].amtacmactivation.acmCertErrors != null) { for (var j in obj.config.domains[i].amtacmactivation.acmCertErrors) { obj.addServerWarning(obj.config.domains[i].amtacmactivation.acmCertErrors[j]); } }
- if (typeof obj.config.domains[i].certurl == 'string') {
- obj.supportsProxyCertificatesRequest = true; // If a certurl is set, enable proxy cert requests
- // Then, fix the URL and add 'https://' if needed
- if (obj.config.domains[i].certurl.indexOf('://') < 0) { obj.config.domains[i].certurl = 'https://' + obj.config.domains[i].certurl; }
- }
- }
-
- // Load CloudFlare trusted proxies list if needed
- if ((obj.config.settings.trustedproxy != null) && (typeof obj.config.settings.trustedproxy == 'string') && (obj.config.settings.trustedproxy.toLowerCase() == 'cloudflare')) {
- obj.config.settings.extrascriptsrc = 'ajax.cloudflare.com'; // Add CloudFlare as a trusted script source. This allows for CloudFlare's RocketLoader feature.
- delete obj.args.trustedproxy;
- delete obj.config.settings.trustedproxy;
- obj.certificateOperations.loadTextFile('https://www.cloudflare.com/ips-v4', null, function (url, data, tag) {
- if (data != null) {
- if (Array.isArray(obj.args.trustedproxy) == false) { obj.args.trustedproxy = []; }
- const ipranges = data.split('\n');
- for (var i in ipranges) { if (ipranges[i] != '') { obj.args.trustedproxy.push(ipranges[i]); } }
- obj.certificateOperations.loadTextFile('https://www.cloudflare.com/ips-v6', null, function (url, data, tag) {
- if (data != null) {
- var ipranges = data.split('\n');
- for (var i in ipranges) { if (ipranges[i] != '') { obj.args.trustedproxy.push(ipranges[i]); } }
- obj.config.settings.trustedproxy = obj.args.trustedproxy;
- } else {
- addServerWarning("Unable to load CloudFlare trusted proxy IPv6 address list.", 16);
- }
- obj.StartEx4(); // Keep going
- });
- } else {
- addServerWarning("Unable to load CloudFlare trusted proxy IPv4 address list.", 16);
- obj.StartEx4(); // Keep going
- }
- });
- } else {
- obj.StartEx4(); // Keep going
- }
- }
-
- // Start the server with the given certificates
- obj.StartEx4 = function () {
- var i;
-
- // If the certificate is un-configured, force LAN-only mode
- if (obj.certificates.CommonName.indexOf('.') == -1) { /*console.log('Server name not configured, running in LAN-only mode.');*/ obj.args.lanonly = true; }
-
- // Write server version and run mode
- const productionMode = (process.env.NODE_ENV && (process.env.NODE_ENV == 'production'));
- const runmode = (obj.args.lanonly ? 2 : (obj.args.wanonly ? 1 : 0));
- console.log("MeshCentral v" + getCurrentVersion() + ', ' + (["Hybrid (LAN + WAN) mode", "WAN mode", "LAN mode"][runmode]) + (productionMode ? ", Production mode." : '.'));
-
- // Check that no sub-domains have the same DNS as the parent
- for (i in obj.config.domains) {
- if ((obj.config.domains[i].dns != null) && (obj.certificates.CommonName.toLowerCase() === obj.config.domains[i].dns.toLowerCase())) {
- console.log("ERROR: Server sub-domain can't have same DNS name as the parent."); process.exit(0); return;
- }
- }
-
- // Load the list of MeshCentral tools
- obj.updateMeshTools();
-
- // Load MeshAgent translation strings
- try {
- var translationpath = obj.path.join(__dirname, 'agents', 'agent-translations.json');
- const translationpath2 = obj.path.join(obj.datapath, 'agents', 'agent-translations.json');
- if (obj.fs.existsSync(translationpath2)) { translationpath = translationpath2; } // If the agent is present in "meshcentral-data/agents", use that one instead.
- var translations = JSON.parse(obj.fs.readFileSync(translationpath).toString());
- if (translations['zh-chs']) { translations['zh-hans'] = translations['zh-chs']; delete translations['zh-chs']; }
- if (translations['zh-cht']) { translations['zh-hant'] = translations['zh-cht']; delete translations['zh-cht']; }
-
- // If there is domain customizations to the agent strings, do this here.
- for (var i in obj.config.domains) {
- var domainTranslations = translations;
- if ((typeof obj.config.domains[i].agentcustomization == 'object') && (typeof obj.config.domains[i].agentcustomization.installtext == 'string')) {
- domainTranslations = Object.assign({}, domainTranslations); // Shallow clone
- for (var j in domainTranslations) { delete domainTranslations[j].description; }
- domainTranslations.en.description = obj.config.domains[i].agentcustomization.installtext;
- }
- obj.config.domains[i].agentTranslations = JSON.stringify(domainTranslations);
- }
- } catch (ex) { }
-
- // Load any domain specific agents
- for (var i in obj.config.domains) { if ((i != '') && (obj.config.domains[i].share == null)) { obj.updateMeshAgentsTable(obj.config.domains[i], function () { }); } }
-
- // Load the list of mesh agents and install scripts
- if ((obj.args.noagentupdate == 1) || (obj.args.noagentupdate == true)) { for (i in obj.meshAgentsArchitectureNumbers) { obj.meshAgentsArchitectureNumbers[i].update = false; } }
- obj.signMeshAgents(obj.config.domains[''], function () {
- obj.updateMeshAgentsTable(obj.config.domains[''], function () {
- obj.updateMeshAgentInstallScripts();
-
- // Setup and start the web server
- obj.crypto.randomBytes(48, function (err, buf) {
- // Setup Mesh Multi-Server if needed
- obj.multiServer = require('./multiserver.js').CreateMultiServer(obj, obj.args);
- if (obj.multiServer != null) {
- if ((obj.db.databaseType != 3) || (obj.db.changeStream != true)) { console.log("ERROR: Multi-server support requires use of MongoDB with ReplicaSet and ChangeStream enabled."); process.exit(0); return; }
- if (typeof obj.args.sessionkey != 'string') { console.log("ERROR: Multi-server support requires \"SessionKey\" be set in the settings section of config.json, same key for all servers."); process.exit(0); return; }
- obj.serverId = obj.multiServer.serverid;
- for (var serverid in obj.config.peers.servers) { obj.peerConnectivityByNode[serverid] = {}; }
- }
-
- // If the server is set to "nousers", allow only loopback unless IP filter is set
- if ((obj.args.nousers == true) && (obj.args.userallowedip == null)) { obj.args.userallowedip = "::1,127.0.0.1"; }
-
- // Set the session length to 60 minutes if not set and set a random key if needed
- if ((obj.args.sessiontime != null) && ((typeof obj.args.sessiontime != 'number') || (obj.args.sessiontime < 1))) { delete obj.args.sessiontime; }
- if (typeof obj.args.sessionkey != 'string') { obj.args.sessionkey = buf.toString('hex').toUpperCase(); }
-
- // Create MQTT Broker to hook into webserver and mpsserver
- if ((typeof obj.config.settings.mqtt == 'object') && (typeof obj.config.settings.mqtt.auth == 'object') && (typeof obj.config.settings.mqtt.auth.keyid == 'string') && (typeof obj.config.settings.mqtt.auth.key == 'string')) { obj.mqttbroker = require("./mqttbroker.js").CreateMQTTBroker(obj, obj.db, obj.args); }
-
- // Start the web server and if needed, the redirection web server.
- obj.webserver = require('./webserver.js').CreateWebServer(obj, obj.db, obj.args, obj.certificates, obj.StartEx5);
- if (obj.redirserver != null) { obj.redirserver.hookMainWebServer(obj.certificates); }
-
- // Change RelayDNS to a array of strings
- if (typeof obj.args.relaydns == 'string') { obj.args.relaydns = [obj.args.relaydns]; }
- if (obj.common.validateStrArray(obj.args.relaydns, 1) == false) { delete obj.args.relaydns; }
-
- // Start the HTTP relay web server if needed
- if ((obj.args.relaydns == null) && (typeof obj.args.relayport == 'number') && (obj.args.relayport != 0)) {
- obj.webrelayserver = require('./webrelayserver.js').CreateWebRelayServer(obj, obj.db, obj.args, obj.certificates, function () { });
- }
-
- // Update proxy certificates
- if (obj.supportsProxyCertificatesRequest == true) { obj.updateProxyCertificates(true); }
-
- // Setup the Intel AMT event handler
- obj.amtEventHandler = require('./amtevents.js').CreateAmtEventsHandler(obj);
-
- // Setup the Intel AMT local network scanner
- if (obj.args.wanonly != true) {
- if (obj.args.amtscanner != false) { obj.amtScanner = require('./amtscanner.js').CreateAmtScanner(obj).start(); }
- if (obj.args.meshscanner != false) { obj.meshScanner = require('./meshscanner.js').CreateMeshScanner(obj).start(); }
- }
-
- // Setup and start the MPS server
- obj.mpsserver = require('./mpsserver.js').CreateMpsServer(obj, obj.db, obj.args, obj.certificates);
-
- // Setup the Intel AMT manager
- if (obj.args.amtmanager !== false) {
- obj.amtManager = require('./amtmanager.js').CreateAmtManager(obj);
- }
-
- // Setup and start the legacy swarm server
- if ((obj.certificates.swarmserver != null) && (obj.args.swarmport != null) && (obj.args.swarmport !== 0)) {
- obj.swarmserver = require('./swarmserver.js').CreateSwarmServer(obj, obj.db, obj.args, obj.certificates);
- }
-
- // Setup the main email server
- if (obj.config.sendgrid != null) {
- // Sendgrid server
- obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
- obj.mailserver.verify();
- if (obj.args.lanonly == true) { addServerWarning("SendGrid server has limited use in LAN mode.", 17); }
- } else if (obj.config.smtp != null) {
- // SMTP server
- obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
- obj.mailserver.verify();
- if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
- } else if (obj.config.sendmail != null) {
- // Sendmail server
- obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
- obj.mailserver.verify();
- if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
- }
-
- // Setup the email server for each domain
- for (i in obj.config.domains) {
- if (obj.config.domains[i].sendgrid != null) {
- // Sendgrid server
- obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
- obj.config.domains[i].mailserver.verify();
- if (obj.args.lanonly == true) { addServerWarning("SendGrid server has limited use in LAN mode.", 17); }
- } else if ((obj.config.domains[i].smtp != null) && (obj.config.domains[i].smtp.host != null) && (obj.config.domains[i].smtp.from != null)) {
- // SMTP server
- obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
- obj.config.domains[i].mailserver.verify();
- if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
- } else if (obj.config.domains[i].sendmail != null) {
- // Sendmail server
- obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
- obj.config.domains[i].mailserver.verify();
- if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
- } else {
- // Setup the parent mail server for this domain
- if (obj.mailserver != null) { obj.config.domains[i].mailserver = obj.mailserver; }
- }
- }
-
- // Setup SMS gateway
- if (config.sms != null) {
- obj.smsserver = require('./meshsms.js').CreateMeshSMS(obj);
- if ((obj.smsserver != null) && (obj.args.lanonly == true)) { addServerWarning("SMS gateway has limited use in LAN mode.", 19); }
- }
-
- // Setup user messaging
- if (config.messaging != null) {
- obj.msgserver = require('./meshmessaging.js').CreateServer(obj);
- }
-
- // Setup web based push notifications
- if ((typeof config.settings.webpush == 'object') && (typeof config.settings.webpush.email == 'string')) {
- obj.webpush = require('web-push');
- var vapidKeys = null;
- try { vapidKeys = JSON.parse(obj.fs.readFileSync(obj.path.join(obj.datapath, 'vapid.json')).toString()); } catch (ex) { }
- if ((vapidKeys == null) || (typeof vapidKeys.publicKey != 'string') || (typeof vapidKeys.privateKey != 'string')) {
- console.log("Generating web push VAPID keys...");
- vapidKeys = obj.webpush.generateVAPIDKeys();
- obj.common.moveOldFiles([obj.path.join(obj.datapath, 'vapid.json')]);
- obj.fs.writeFileSync(obj.path.join(obj.datapath, 'vapid.json'), JSON.stringify(vapidKeys));
- }
- obj.webpush.vapidPublicKey = vapidKeys.publicKey;
- obj.webpush.setVapidDetails('mailto:' + config.settings.webpush.email, vapidKeys.publicKey, vapidKeys.privateKey);
- if (typeof config.settings.webpush.gcmapi == 'string') { webpush.setGCMAPIKey(config.settings.webpush.gcmapi); }
- }
-
- // Get the current node version
- const verSplit = process.version.substring(1).split('.');
- var nodeVersion = parseInt(verSplit[0]) + (parseInt(verSplit[1]) / 100);
-
- // Setup Firebase
- if ((config.firebase != null) && (typeof config.firebase.senderid == 'string') && (typeof config.firebase.serverkey == 'string')) {
- addServerWarning('Firebase now requires a service account JSON file, Firebase disabled.', 27);
- } else if ((config.firebase != null) && (typeof config.firebase.serviceaccountfile == 'string')) {
- var serviceAccount;
- try { serviceAccount = JSON.parse(obj.fs.readFileSync(obj.path.join(obj.datapath, config.firebase.serviceaccountfile)).toString()); } catch (ex) { console.log(ex); }
- if (serviceAccount != null) { obj.firebase = require('./firebase').CreateFirebase(obj, serviceAccount); }
- } else if ((typeof config.firebaserelay == 'object') && (typeof config.firebaserelay.url == 'string')) {
- // Setup the push messaging relay
- obj.firebase = require('./firebase').CreateFirebaseRelay(obj, config.firebaserelay.url, config.firebaserelay.key);
- } else if (obj.config.settings.publicpushnotifications === true) {
- // Setup the Firebase push messaging relay using https://alt.meshcentral.com, this is the public push notification server.
- obj.firebase = require('./firebase').CreateFirebaseRelay(obj, 'https://alt.meshcentral.com/firebaserelay.aspx');
- }
-
- // Setup monitoring
- obj.monitoring = require('./monitoring.js').CreateMonitoring(obj, obj.args);
-
- // Start periodic maintenance
- obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 60 * 60); // Run this every hour
- //obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 10 * 1); // DEBUG: Run this more often
-
- // Dispatch an event that the server is now running
- obj.DispatchEvent(['*'], obj, { etype: 'server', action: 'started', msg: 'Server started' });
-
- // Plugin hook. Need to run something at server startup? This is the place.
- if (obj.pluginHandler) { obj.pluginHandler.callHook('server_startup'); }
-
- // Setup the login cookie encryption key
- if ((obj.config) && (obj.config.settings) && (typeof obj.config.settings.logincookieencryptionkey == 'string')) {
- // We have a string, hash it and use that as a key
- try { obj.loginCookieEncryptionKey = Buffer.from(obj.config.settings.logincookieencryptionkey, 'hex'); } catch (ex) { }
- if ((obj.loginCookieEncryptionKey == null) || (obj.loginCookieEncryptionKey.length != 80)) { addServerWarning("Invalid \"LoginCookieEncryptionKey\" in config.json.", 20); obj.loginCookieEncryptionKey = null; }
- }
-
- // Login cookie encryption key not set, use one from the database
- if (obj.loginCookieEncryptionKey == null) {
- obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
- if ((docs != null) && (docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 160)) {
- obj.loginCookieEncryptionKey = Buffer.from(docs[0].key, 'hex');
- } else {
- obj.loginCookieEncryptionKey = obj.generateCookieKey(); obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() });
- }
- });
- }
-
- // Load the invitation link encryption key from the database
- obj.db.Get('InvitationLinkEncryptionKey', function (err, docs) {
- if ((docs != null) && (docs.length > 0) && (docs[0].key != null) && (docs[0].key.length >= 160)) {
- obj.invitationLinkEncryptionKey = Buffer.from(docs[0].key, 'hex');
- } else {
- obj.invitationLinkEncryptionKey = obj.generateCookieKey(); obj.db.Set({ _id: 'InvitationLinkEncryptionKey', key: obj.invitationLinkEncryptionKey.toString('hex'), time: Date.now() });
- }
- });
-
- // Setup Intel AMT hello server
- if ((typeof config.settings.amtprovisioningserver == 'object') && (typeof config.settings.amtprovisioningserver.devicegroup == 'string') && (typeof config.settings.amtprovisioningserver.newmebxpassword == 'string') && (typeof config.settings.amtprovisioningserver.trustedfqdn == 'string') && (typeof config.settings.amtprovisioningserver.ip == 'string')) {
- obj.amtProvisioningServer = require('./amtprovisioningserver').CreateAmtProvisioningServer(obj, config.settings.amtprovisioningserver);
- }
-
- // Start collecting server stats every 5 minutes
- obj.trafficStats = obj.webserver.getTrafficStats();
- setInterval(function () {
- obj.serverStatsCounter++;
- var hours = 720; // Start with all events lasting 30 days.
- if (((obj.serverStatsCounter) % 2) == 1) { hours = 3; } // Half of the event get removed after 3 hours.
- else if ((Math.floor(obj.serverStatsCounter / 2) % 2) == 1) { hours = 8; } // Another half of the event get removed after 8 hours.
- else if ((Math.floor(obj.serverStatsCounter / 4) % 2) == 1) { hours = 24; } // Another half of the event get removed after 24 hours.
- else if ((Math.floor(obj.serverStatsCounter / 8) % 2) == 1) { hours = 48; } // Another half of the event get removed after 48 hours.
- else if ((Math.floor(obj.serverStatsCounter / 16) % 2) == 1) { hours = 72; } // Another half of the event get removed after 72 hours.
- const expire = new Date();
- expire.setTime(expire.getTime() + (60 * 60 * 1000 * hours));
-
- // Get traffic data
- var trafficStats = obj.webserver.getTrafficDelta(obj.trafficStats);
- obj.trafficStats = trafficStats.current;
-
- var data = {
- time: new Date(),
- expire: expire,
- mem: process.memoryUsage(),
- conn: {
- ca: Object.keys(obj.webserver.wsagents).length,
- cu: Object.keys(obj.webserver.wssessions).length,
- us: Object.keys(obj.webserver.wssessions2).length,
- rs: obj.webserver.relaySessionCount,
- am: 0
- },
- traffic: trafficStats.delta
- };
- try { data.cpu = require('os').loadavg(); } catch (ex) { }
- if (obj.mpsserver != null) {
- data.conn.amc = 0;
- for (var i in obj.mpsserver.ciraConnections) { data.conn.amc += obj.mpsserver.ciraConnections[i].length; }
- }
- for (var i in obj.connectivityByNode) {
- const node = obj.connectivityByNode[i];
- if (node && typeof node.connectivity !== 'undefined' && node.connectivity === 4) { data.conn.am++; }
- }
- if (obj.firstStats === true) { delete obj.firstStats; data.first = true; }
- if (obj.multiServer != null) { data.s = obj.multiServer.serverid; }
- obj.db.SetServerStats(data); // Save the stats to the database
- obj.DispatchEvent(['*'], obj, { action: 'servertimelinestats', data: data }); // Event the server stats
- }, 300000);
-
- obj.debug('main', "Server started");
- if (obj.args.nousers == true) { obj.updateServerState('nousers', '1'); }
- obj.updateServerState('state', "running");
-
- // Setup auto-backup defaults. Unless autobackup is set to false try to make a backup.
- if (obj.config.settings.autobackup == false || obj.config.settings.autobackup == 'false') { obj.config.settings.autobackup = {backupintervalhours: -1}; } //block all autobackup functions
- else {
- if (typeof obj.config.settings.autobackup != 'object') { obj.config.settings.autobackup = {}; };
- if (typeof obj.config.settings.autobackup.backupintervalhours != 'number') { obj.config.settings.autobackup.backupintervalhours = 24; };
- if (typeof obj.config.settings.autobackup.keeplastdaysbackup != 'number') { obj.config.settings.autobackup.keeplastdaysbackup = 10; };
- if (obj.config.settings.autobackup.backuphour != null ) { obj.config.settings.autobackup.backupintervalhours = 24; if ((typeof obj.config.settings.autobackup.backuphour != 'number') || (obj.config.settings.autobackup.backuphour > 23 || obj.config.settings.autobackup.backuphour < 0 )) { obj.config.settings.autobackup.backuphour = 0; }}
- else {obj.config.settings.autobackup.backuphour = -1 };
- //arrayfi in case of string and remove possible ', ' space. !! If a string instead of an array is passed, it will be split by ',' so *{.txt,.log} won't work in that case !!
- if (!obj.config.settings.autobackup.backupignorefilesglob) {obj.config.settings.autobackup.backupignorefilesglob = []}
- else if (typeof obj.config.settings.autobackup.backupignorefilesglob == 'string') { obj.config.settings.autobackup.backupignorefilesglob = obj.config.settings.autobackup.backupignorefilesglob.replaceAll(', ', ',').split(','); };
- if (!obj.config.settings.autobackup.backupskipfoldersglob) {obj.config.settings.autobackup.backupskipfoldersglob = []}
- else if (typeof obj.config.settings.autobackup.backupskipfoldersglob == 'string') { obj.config.settings.autobackup.backupskipfoldersglob = obj.config.settings.autobackup.backupskipfoldersglob.replaceAll(', ', ',').split(','); };
- if (typeof obj.config.settings.autobackup.backuppath == 'string') { obj.backuppath = (obj.config.settings.autobackup.backuppath = (obj.path.resolve(obj.config.settings.autobackup.backuppath))) } else { obj.config.settings.autobackup.backuppath = obj.backuppath };
- if (typeof obj.config.settings.autobackup.backupname != 'string') { obj.config.settings.autobackup.backupname = 'meshcentral-autobackup-'};
- if (typeof obj.config.settings.autobackup.webdav == 'object') {
- //make webdav compliant: http://www.webdav.org/specs/rfc4918.html#rfc.section.5.2, http://www.webdav.org/specs/rfc2518.html#METHOD_MKCOL
- // So with leading and trailing slash in the foldername, and no double and backslashes
- if (typeof obj.config.settings.autobackup.webdav.foldername != 'string') {obj.config.settings.autobackup.webdav.foldername = '/MeshCentral-Backups/'}
- else {obj.config.settings.autobackup.webdav.foldername = ('/' + obj.config.settings.autobackup.webdav.foldername + '/').replaceAll("\\", "/").replaceAll("//", "/").replaceAll("//", "/")};
- }
- }
-
- // Check if the database is capable of performing a backup
- obj.db.checkBackupCapability(function (err, msg) { if (msg != null) { obj.addServerWarning(msg, true) } });
-
- // Load Intel AMT passwords from the "amtactivation.log" file
- obj.loadAmtActivationLogPasswords(function (amtPasswords) {
- obj.amtPasswords = amtPasswords;
- });
-
- // Setup users that can see all device groups
- if (typeof obj.config.settings.managealldevicegroups == 'string') { obj.config.settings.managealldevicegroups = obj.config.settings.managealldevicegroups.split(','); }
- else if (Array.isArray(obj.config.settings.managealldevicegroups) == false) { obj.config.settings.managealldevicegroups = []; }
- for (i in obj.config.domains) {
- if (Array.isArray(obj.config.domains[i].managealldevicegroups)) {
- for (var j in obj.config.domains[i].managealldevicegroups) {
- if (typeof obj.config.domains[i].managealldevicegroups[j] == 'string') {
- const u = 'user/' + i + '/' + obj.config.domains[i].managealldevicegroups[j];
- if (obj.config.settings.managealldevicegroups.indexOf(u) == -1) { obj.config.settings.managealldevicegroups.push(u); }
- }
- }
- }
- }
- obj.config.settings.managealldevicegroups.sort();
-
- // Start watchdog timer if needed
- // This is used to monitor if NodeJS is servicing IO correctly or getting held up a lot. Add this line to the settings section of config.json
- // "watchDog": { "interval": 100, "timeout": 150 }
- // This will check every 100ms, if the timer is more than 150ms late, it will warn.
- if ((typeof config.settings.watchdog == 'object') && (typeof config.settings.watchdog.interval == 'number') && (typeof config.settings.watchdog.timeout == 'number') && (config.settings.watchdog.interval >= 50) && (config.settings.watchdog.timeout >= 50)) {
- obj.watchdogtime = Date.now();
- obj.watchdogmax = 0;
- obj.watchdogmaxtime = null;
- obj.watchdogtable = [];
- obj.watchdog = setInterval(function () {
- const now = Date.now(), delta = now - obj.watchdogtime - config.settings.watchdog.interval;
- if (delta > obj.watchdogmax) { obj.watchdogmax = delta; obj.watchdogmaxtime = new Date().toLocaleString(); }
- if (delta > config.settings.watchdog.timeout) {
- const msg = obj.common.format("Watchdog timer timeout, {0}ms.", delta);
- obj.watchdogtable.push(new Date().toLocaleString() + ', ' + delta + 'ms');
- while (obj.watchdogtable.length > 10) { obj.watchdogtable.shift(); }
- obj.debug('main', msg);
- try {
- var errlogpath = null;
- if (typeof obj.args.mesherrorlogpath == 'string') { errlogpath = obj.path.join(obj.args.mesherrorlogpath, 'mesherrors.txt'); } else { errlogpath = obj.getConfigFilePath('mesherrors.txt'); }
- obj.fs.appendFileSync(errlogpath, new Date().toLocaleString() + ': ' + msg + '\r\n');
- } catch (ex) { console.log('ERROR: Unable to write to mesherrors.txt.'); }
- }
- obj.watchdogtime = now;
- }, config.settings.watchdog.interval);
- obj.debug('main', "Started watchdog timer.");
- }
-
- });
- });
- });
- };
-
- // Called when the web server finished loading
- obj.StartEx5 = function () {
- // Setup the email server for each domain
- var ipKvmSupport = false;
- for (var i in obj.config.domains) { if (obj.config.domains[i].ipkvm == true) { ipKvmSupport = true; } }
- if (ipKvmSupport) { obj.ipKvmManager = require('./meshipkvm').CreateIPKVMManager(obj); }
-
- // Run the server start script if present
- if (typeof obj.config.settings.runonserverstarted == 'string') {
- const child_process = require('child_process');
- var parentpath = __dirname;
- if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) { parentpath = require('path').join(__dirname, '../..'); }
- child_process.exec(obj.config.settings.runonserverstarted + ' ' + getCurrentVersion(), { maxBuffer: 512000, timeout: 120000, cwd: parentpath }, function (error, stdout, stderr) { });
- }
- }
-
- // Refresh any certificate hashs from the reverse proxy
- obj.pendingProxyCertificatesRequests = 0;
- obj.lastProxyCertificatesRequest = null;
- obj.supportsProxyCertificatesRequest = false;
- obj.updateProxyCertificates = function (force) {
- if (force !== true) {
- if ((obj.pendingProxyCertificatesRequests > 0) || (obj.supportsProxyCertificatesRequest == false)) return;
- if ((obj.lastProxyCertificatesRequest != null) && ((Date.now() - obj.lastProxyCertificatesRequest) < 120000)) return; // Don't allow this call more than every 2 minutes.
- obj.lastProxyCertificatesRequest = Date.now();
- }
-
- // Load any domain web certificates
- for (var i in obj.config.domains) {
- if (obj.config.domains[i].certurl != null) {
- // Load web certs
- obj.pendingProxyCertificatesRequests++;
- var dnsname = obj.config.domains[i].dns;
- if ((dnsname == null) && (obj.config.settings.cert != null)) { dnsname = obj.config.settings.cert; }
- obj.certificateOperations.loadCertificate(obj.config.domains[i].certurl, dnsname, obj.config.domains[i], function (url, cert, xhostname, xdomain) {
- obj.pendingProxyCertificatesRequests--;
- if (cert != null) {
- // Hash the entire cert
- const hash = obj.crypto.createHash('sha384').update(Buffer.from(cert, 'binary')).digest('hex');
- if (xdomain.certhash != hash) { // The certificate has changed.
- xdomain.certkeyhash = hash;
- xdomain.certhash = hash;
-
- try {
- // Decode a RSA certificate and hash the public key, if this is not RSA, skip this.
- const forgeCert = obj.certificateOperations.forge.pki.certificateFromAsn1(obj.certificateOperations.forge.asn1.fromDer(cert));
- xdomain.certkeyhash = obj.certificateOperations.forge.pki.getPublicKeyFingerprint(forgeCert.publicKey, { md: obj.certificateOperations.forge.md.sha384.create(), encoding: 'hex' });
- obj.webserver.webCertificateExpire[xdomain.id] = Date.parse(forgeCert.validity.notAfter); // Update certificate expire time
- //console.log('V1: ' + xdomain.certkeyhash);
- } catch (ex) {
- delete obj.webserver.webCertificateExpire[xdomain.id]; // Remove certificate expire time
- delete xdomain.certkeyhash;
- }
-
- if (obj.webserver) {
- obj.webserver.webCertificateHashs[xdomain.id] = obj.webserver.webCertificateFullHashs[xdomain.id] = Buffer.from(hash, 'hex').toString('binary');
- if (xdomain.certkeyhash != null) { obj.webserver.webCertificateHashs[xdomain.id] = Buffer.from(xdomain.certkeyhash, 'hex').toString('binary'); }
-
- // Disconnect all agents with bad web certificates
- for (var i in obj.webserver.wsagentsWithBadWebCerts) { obj.webserver.wsagentsWithBadWebCerts[i].close(1); }
- }
-
- console.log(obj.common.format("Loaded web certificate from \"{0}\", host: \"{1}\"", url, xhostname));
- console.log(obj.common.format(" SHA384 cert hash: {0}", xdomain.certhash));
- if ((xdomain.certkeyhash != null) && (xdomain.certhash != xdomain.certkeyhash)) { console.log(obj.common.format(" SHA384 key hash: {0}", xdomain.certkeyhash)); }
- }
- } else {
- console.log(obj.common.format("Failed to load web certificate at: \"{0}\", host: \"{1}\"", url, xhostname));
- }
- });
- }
- }
- }
-
- // Perform maintenance operations (called every hour)
- obj.maintenanceActions = function () {
- // Perform database maintenance
- obj.db.maintenance();
-
- // Clean up any temporary files
- const removeTime = new Date(Date.now()).getTime() - (30 * 60 * 1000); // 30 minutes
- const dir = obj.fs.readdir(obj.path.join(obj.filespath, 'tmp'), function (err, files) {
- if (err != null) return;
- for (var i in files) { try { const filepath = obj.path.join(obj.filespath, 'tmp', files[i]); if (obj.fs.statSync(filepath).mtime.getTime() < removeTime) { obj.fs.unlink(filepath, function () { }); } } catch (ex) { } }
- });
-
- // Check for self-update that targets a specific version
- if ((typeof obj.args.selfupdate == 'string') && (getCurrentVersion() === obj.args.selfupdate)) { obj.args.selfupdate = false; }
-
- // Check if we need to perform server self-update
- if ((obj.args.selfupdate) && (obj.serverSelfWriteAllowed == true)) {
- obj.db.getValueOfTheDay('performSelfUpdate', 1, function (performSelfUpdate) {
- if (performSelfUpdate.value > 0) {
- performSelfUpdate.value--;
- obj.db.Set(performSelfUpdate);
- obj.getLatestServerVersion(function (currentVer, latestVer) { if (currentVer != latestVer) { obj.performServerUpdate(); return; } });
- } else {
- checkAutobackup();
- }
- });
- } else {
- checkAutobackup();
- }
- };
-
- // Check if we need to perform an automatic backup
- function checkAutobackup() {
- if (obj.config.settings.autobackup.backupintervalhours >= 1 ) {
- obj.db.Get('LastAutoBackupTime', function (err, docs) {
- if (err != null) { console.error("checkAutobackup: Error getting LastBackupTime from DB"); return}
- var lastBackup = 0;
- const currentdate = new Date();
- let currentHour = currentdate.getHours();
- let now = currentdate.getTime();
- if (docs.length == 1) { lastBackup = docs[0].value; }
- const delta = now - lastBackup;
- //const delta = 9999999999; // DEBUG: backup always
- obj.debug ('backup', 'Entering checkAutobackup, lastAutoBackupTime: ' + new Date(lastBackup).toLocaleString('default', { dateStyle: 'medium', timeStyle: 'short' }) + ', delta: ' + (delta/(1000*60*60)).toFixed(2) + ' hours');
- //start autobackup if interval has passed or at configured hour, whichever comes first. When an hour schedule is missed, it will make a backup immediately.
- if ((delta > (obj.config.settings.autobackup.backupintervalhours * 60 * 60 * 1000)) || ((currentHour == obj.config.settings.autobackup.backuphour) && (delta >= 2 * 60 * 60 * 1000))) {
- // A new auto-backup is required.
- obj.db.Set({ _id: 'LastAutoBackupTime', value: now }); // Save the current time in the database
- obj.db.performBackup(); // Perform the backup
- }
- });
- }
- }
-
- // Stop the Meshcentral server
- obj.Stop = function (restoreFile) {
- // If the database is not setup, exit now.
- if (!obj.db) return;
-
- // Dispatch an event saying the server is now stopping
- obj.DispatchEvent(['*'], obj, { etype: 'server', action: 'stopped', msg: "Server stopped" });
-
- // Set all nodes to power state of unknown (0)
- obj.db.storePowerEvent({ time: new Date(), nodeid: '*', power: 0, s: 2 }, obj.multiServer, function () { // s:2 indicates that the server is shutting down.
- if (restoreFile) {
- obj.debug('main', obj.common.format("Server stopped, updating settings: {0}", restoreFile));
- console.log("Updating settings folder...");
-
- const yauzl = require('yauzl');
- yauzl.open(restoreFile, { lazyEntries: true }, function (err, zipfile) {
- if (err) throw err;
- zipfile.readEntry();
- zipfile.on('entry', function (entry) {
- if (/\/$/.test(entry.fileName)) {
- // Directory file names end with '/'.
- // Note that entires for directories themselves are optional.
- // An entry's fileName implicitly requires its parent directories to exist.
- zipfile.readEntry();
- } else {
- // File entry
- zipfile.openReadStream(entry, function (err, readStream) {
- if (err) throw err;
- readStream.on('end', function () { zipfile.readEntry(); });
- var directory = obj.path.dirname(entry.fileName);
- if (directory != '.') {
- directory = obj.getConfigFilePath(directory)
- if (obj.fs.existsSync(directory) == false) { obj.fs.mkdirSync(directory); }
- }
- //console.log('Extracting:', obj.getConfigFilePath(entry.fileName));
- readStream.pipe(obj.fs.createWriteStream(obj.getConfigFilePath(entry.fileName)));
- });
- }
- });
- zipfile.on('end', function () { setTimeout(function () { obj.fs.unlinkSync(restoreFile); process.exit(123); }); });
- });
- } else {
- obj.debug('main', "Server stopped");
- process.exit(0);
- }
- });
-
- // Update the server state
- obj.updateServerState('state', "stopped");
- };
-
- // Event Dispatch
- obj.AddEventDispatch = function (ids, target) {
- obj.debug('dispatch', 'AddEventDispatch', ids);
- for (var i in ids) { var id = ids[i]; if (!obj.eventsDispatch[id]) { obj.eventsDispatch[id] = [target]; } else { obj.eventsDispatch[id].push(target); } }
- };
- obj.RemoveEventDispatch = function (ids, target) {
- obj.debug('dispatch', 'RemoveEventDispatch', ids);
- for (var i in ids) {
- const id = ids[i];
- if (obj.eventsDispatch[id]) {
- var j = obj.eventsDispatch[id].indexOf(target);
- if (j >= 0) {
- if (obj.eventsDispatch[id].length == 1) {
- delete obj.eventsDispatch[id];
- } else {
- const newList = []; // We create a new list so not to modify the original list. Allows this function to be called during an event dispatch.
- for (var k in obj.eventsDispatch[i]) { if (obj.eventsDispatch[i][k] != target) { newList.push(obj.eventsDispatch[i][k]); } }
- obj.eventsDispatch[i] = newList;
- }
- }
- }
- }
- };
- obj.RemoveEventDispatchId = function (id) {
- obj.debug('dispatch', 'RemoveEventDispatchId', id);
- if (obj.eventsDispatch[id] != null) { delete obj.eventsDispatch[id]; }
- };
- obj.RemoveAllEventDispatch = function (target) {
- obj.debug('dispatch', 'RemoveAllEventDispatch');
- for (var i in obj.eventsDispatch) {
- const j = obj.eventsDispatch[i].indexOf(target);
- if (j >= 0) {
- if (obj.eventsDispatch[i].length == 1) {
- delete obj.eventsDispatch[i];
- } else {
- const newList = []; // We create a new list so not to modify the original list. Allows this function to be called during an event dispatch.
- for (var k in obj.eventsDispatch[i]) { if (obj.eventsDispatch[i][k] != target) { newList.push(obj.eventsDispatch[i][k]); } }
- obj.eventsDispatch[i] = newList;
- }
- }
- }
- };
- obj.DispatchEvent = function (ids, source, event, fromPeerServer) {
- // If the database is not setup, exit now.
- if (!obj.db) return;
-
- // Send event to syslog if needed
- if (obj.syslog && event.msg) { obj.syslog.log(obj.syslog.LOG_INFO, event.msg); }
- if (obj.syslogjson) { obj.syslogjson.log(obj.syslogjson.LOG_INFO, JSON.stringify(event)); }
- if (obj.syslogtcp && event.msg) { obj.syslogtcp.log(event.msg, obj.syslogtcp.LOG_INFO); }
-
- obj.debug('dispatch', 'DispatchEvent', ids);
- if ((typeof event == 'object') && (!event.nolog)) {
- event.time = new Date();
- // The event we store is going to skip some of the fields so we don't store too much stuff in the database.
- const storeEvent = Object.assign({}, event);
- if (storeEvent.node) { delete storeEvent.node; } // Skip the "node" field. May skip more in the future.
- if (storeEvent.links) {
- // Escape "links" names that may have "." and/or "$"
- storeEvent.links = Object.assign({}, storeEvent.links);
- for (var i in storeEvent.links) { var ue = obj.common.escapeFieldName(i); if (ue !== i) { storeEvent.links[ue] = storeEvent.links[i]; delete storeEvent.links[i]; } }
- }
- if (storeEvent.mesh) {
- // Escape "mesh" names that may have "." and/or "$"
- storeEvent.mesh = obj.common.escapeLinksFieldNameEx(storeEvent.mesh);
- }
- storeEvent.ids = ids;
- obj.db.StoreEvent(storeEvent);
- }
- const targets = []; // List of targets we dispatched the event to, we don't want to dispatch to the same target twice.
- for (var j in ids) {
- const id = ids[j];
- const eventsDispatch = obj.eventsDispatch[id];
- if (eventsDispatch) {
- for (var i in eventsDispatch) {
- if (targets.indexOf(eventsDispatch[i]) == -1) { // Check if we already displatched to this target
- targets.push(eventsDispatch[i]);
- try { eventsDispatch[i].HandleEvent(source, event, ids, id); } catch (ex) { console.log(ex, eventsDispatch[i]); }
- }
- }
- }
- }
- if ((fromPeerServer == null) && (obj.multiServer != null) && ((typeof event != 'object') || (event.nopeers != 1))) { obj.multiServer.DispatchEvent(ids, source, event); }
- };
-
- // Get the connection state of a node
- obj.GetConnectivityState = function (nodeid) { return obj.connectivityByNode[nodeid]; };
-
- // Get the routing server id for a given node and connection type, can never be self.
- obj.GetRoutingServerIdNotSelf = function (nodeid, connectType) {
- if (obj.multiServer == null) return null;
- for (var serverid in obj.peerConnectivityByNode) {
- if (serverid == obj.serverId) continue;
- var state = obj.peerConnectivityByNode[serverid][nodeid];
- if ((state != null) && ((state.connectivity & connectType) != 0)) { return { serverid: serverid, meshid: state.meshid }; }
- }
- return null;
- };
-
- // Get the routing server id for a given node and connection type, self first
- obj.GetRoutingServerId = function (nodeid, connectType) {
- if (obj.multiServer == null) return null;
-
- // Look at our own server first
- var connections = obj.peerConnectivityByNode[obj.serverId];
- if (connections != null) {
- var state = connections[nodeid];
- if ((state != null) && ((state.connectivity & connectType) != 0)) { return { serverid: obj.serverId, meshid: state.meshid }; }
- }
-
- // Look at other servers
- for (var serverid in obj.peerConnectivityByNode) {
- if (serverid == obj.serverId) continue;
- var state = obj.peerConnectivityByNode[serverid][nodeid];
- if ((state != null) && ((state.connectivity & connectType) != 0)) { return { serverid: serverid, meshid: state.meshid }; }
- }
- return null;
- };
-
- // Update the connection state of a node when in multi-server mode
- // Update obj.connectivityByNode using obj.peerConnectivityByNode for the list of nodes in argument
- obj.UpdateConnectivityState = function (nodeids) {
- for (var nodeid in nodeids) {
- var meshid = null, state = null, oldConnectivity = 0, oldPowerState = 0, newConnectivity = 0, newPowerState = 0;
- var oldState = obj.connectivityByNode[nodeid];
- if (oldState != null) { meshid = oldState.meshid; oldConnectivity = oldState.connectivity; oldPowerState = oldState.powerState; }
- for (var serverid in obj.peerConnectivityByNode) {
- var peerState = obj.peerConnectivityByNode[serverid][nodeid];
- if (peerState != null) {
- if (state == null) {
- // Copy the state
- state = {};
- newConnectivity = state.connectivity = peerState.connectivity;
- newPowerState = state.powerState = peerState.powerState;
- meshid = state.meshid = peerState.meshid;
- //if (peerState.agentPower) { state.agentPower = peerState.agentPower; }
- //if (peerState.ciraPower) { state.ciraPower = peerState.ciraPower; }
- //if (peerState.amtPower) { state.amtPower = peerState.amtPower; }
- } else {
- // Merge the state
- state.connectivity |= peerState.connectivity;
- newConnectivity = state.connectivity;
- if ((peerState.powerState != 0) && ((state.powerState == 0) || (peerState.powerState < state.powerState))) { newPowerState = state.powerState = peerState.powerState; }
- meshid = state.meshid = peerState.meshid;
- //if (peerState.agentPower) { state.agentPower = peerState.agentPower; }
- //if (peerState.ciraPower) { state.ciraPower = peerState.ciraPower; }
- //if (peerState.amtPower) { state.amtPower = peerState.amtPower; }
- }
- }
- }
- obj.connectivityByNode[nodeid] = state;
-
- //console.log('xx', nodeid, meshid, newConnectivity, oldPowerState, newPowerState, oldPowerState);
-
- // Event any changes on this server only
- if ((newConnectivity != oldPowerState) || (newPowerState != oldPowerState)) {
- obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: newConnectivity, pwr: newPowerState, nolog: 1, nopeers: 1, id: Math.random() });
- }
- }
- };
-
- // See if we need to notifiy any user of device state change
- obj.NotifyUserOfDeviceStateChange = function (meshid, nodeid, connectTime, connectType, powerState, serverid, stateSet, extraInfo) {
- // Check if there is a email server for this domain
- const meshSplit = meshid.split('/');
- if (meshSplit.length != 3) return;
- const domainId = meshSplit[1];
- if (obj.config.domains[domainId] == null) return;
- const mailserver = obj.config.domains[domainId].mailserver;
- if ((mailserver == null) && (obj.msgserver == null)) return;
-
- // Get the device group for this device
- const mesh = obj.webserver.meshes[meshid];
- if ((mesh == null) || (mesh.links == null)) return;
-
- // Get the list of users that have visibility to this device
- // This includes users that are part of user groups
- const users = [];
- for (var i in mesh.links) {
- if (i.startsWith('user/') && (users.indexOf(i) < 0)) { users.push(i); }
- if (i.startsWith('ugrp/')) {
- var usergrp = obj.webserver.userGroups[i];
- if (usergrp.links != null) { for (var j in usergrp.links) { if (j.startsWith('user/') && (users.indexOf(j) < 0)) { users.push(j); } } }
- }
- }
-
- // Check if any user needs email notification
- for (var i in users) {
- const user = obj.webserver.users[users[i]];
- if (user != null) {
- var notify = 0;
-
- // Device group notifications
- const meshLinks = user.links[meshid];
- if ((meshLinks != null) && (meshLinks.notify != null)) { notify |= meshLinks.notify; }
-
- // User notifications
- if (user.notify != null) {
- if (user.notify[meshid] != null) { notify |= user.notify[meshid]; }
- if (user.notify[nodeid] != null) { notify |= user.notify[nodeid]; }
- }
-
- // Email notifications
- if ((user.email != null) && (user.emailVerified == true) && (mailserver != null) && ((notify & 48) != 0)) {
- if (stateSet == true) {
- if ((notify & 16) != 0) {
- mailserver.notifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
- } else {
- mailserver.cancelNotifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
- }
- }
- else if (stateSet == false) {
- if ((notify & 32) != 0) {
- mailserver.notifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
- } else {
- mailserver.cancelNotifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
- }
- }
- }
-
- // Messaging notifications
- if ((obj.msgserver != null) && ((notify & 384) != 0)) {
- if (stateSet == true) {
- if ((notify & 128) != 0) {
- obj.msgserver.notifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
- } else {
- obj.msgserver.cancelNotifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
- }
- }
- else if (stateSet == false) {
- if ((notify & 256) != 0) {
- obj.msgserver.notifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
- } else {
- obj.msgserver.cancelNotifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
- }
- }
- }
- }
- }
- }
-
- // See if we need to notifiy any user of device requested help
- //if (typeof device.name == 'string') { parent.parent.NotifyUserOfDeviceHelpRequest(domain, device._id, device.meshid, device.name, command.msgArgs[0], command.msgArgs[1]); }
-
- obj.NotifyUserOfDeviceHelpRequest = function (domain, meshid, nodeid, devicename, helpusername, helprequest) {
- // Check if there is a email server for this domain
- const meshSplit = meshid.split('/');
- if (meshSplit.length != 3) return;
- const domainId = meshSplit[1];
- if (obj.config.domains[domainId] == null) return;
- const mailserver = obj.config.domains[domainId].mailserver;
- if ((mailserver == null) && (obj.msgserver == null)) return;
-
- // Get the device group for this device
- const mesh = obj.webserver.meshes[meshid];
- if ((mesh == null) || (mesh.links == null)) return;
-
- // Get the list of users that have visibility to this device
- // This includes users that are part of user groups
- const users = [];
- for (var i in mesh.links) {
- if (i.startsWith('user/') && (users.indexOf(i) < 0)) { users.push(i); }
- if (i.startsWith('ugrp/')) {
- var usergrp = obj.webserver.userGroups[i];
- if (usergrp.links != null) { for (var j in usergrp.links) { if (j.startsWith('user/') && (users.indexOf(j) < 0)) { users.push(j); } } }
- }
- }
-
- // Check if any user needs email notification
- for (var i in users) {
- const user = obj.webserver.users[users[i]];
- if (user != null) {
- var notify = 0;
-
- // Device group notifications
- const meshLinks = user.links[meshid];
- if ((meshLinks != null) && (meshLinks.notify != null)) { notify |= meshLinks.notify; }
-
- // User notifications
- if (user.notify != null) {
- if (user.notify[meshid] != null) { notify |= user.notify[meshid]; }
- if (user.notify[nodeid] != null) { notify |= user.notify[nodeid]; }
- }
-
- // Mail help request
- if ((user.email != null) && (user.emailVerified == true) && ((notify & 64) != 0)) { mailserver.sendDeviceHelpMail(domain, user.name, user.email, devicename, nodeid, helpusername, helprequest, user.llang); }
-
- // Message help request
- if ((user.msghandle != null) && ((notify & 512) != 0)) { obj.msgserver.sendDeviceHelpRequest(domain, user.name, user.msghandle, devicename, nodeid, helpusername, helprequest, user.llang); }
- }
- }
- }
-
- // Set the connectivity state of a node and setup the server so that messages can be routed correctly.
- // meshId: mesh identifier of format mesh/domain/meshidhex
- // nodeId: node identifier of format node/domain/nodeidhex
- // connectTime: time of connection, milliseconds elapsed since the UNIX epoch.
- // connectType: Bitmask, 1 = MeshAgent, 2 = Intel AMT CIRA, 4 = Intel AMT local, 8 = Intel AMT Relay, 16 = MQTT
- // powerState: Value, 0 = Unknown, 1 = S0 power on, 2 = S1 Sleep, 3 = S2 Sleep, 4 = S3 Sleep, 5 = S4 Hibernate, 6 = S5 Soft-Off, 7 = Present, 8 = Off
- //var connectTypeStrings = ['', 'MeshAgent', 'Intel AMT CIRA', '', 'Intel AMT local', '', '', '', 'Intel AMT Relay', '', '', '', '', '', '', '', 'MQTT'];
- //var powerStateStrings = ['Unknown', 'Powered', 'Sleep', 'Sleep', 'Deep Sleep', 'Hibernating', 'Soft-Off', 'Present', 'Off'];
- obj.SetConnectivityState = function (meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
- //console.log('SetConnectivity for ' + nodeid.substring(0, 16) + ', Type: ' + connectTypeStrings[connectType] + ', Power: ' + powerStateStrings[powerState] + (serverid == null ? ('') : (', ServerId: ' + serverid)));
- if ((serverid == null) && (obj.multiServer != null)) { obj.multiServer.DispatchMessage({ action: 'SetConnectivityState', meshid: meshid, nodeid: nodeid, connectTime: connectTime, connectType: connectType, powerState: powerState, extraInfo: extraInfo }); }
-
- if (obj.multiServer == null) {
- // Single server mode
-
- // Change the node connection state
- var eventConnectChange = 0;
- var state = obj.connectivityByNode[nodeid];
- if (state) {
- // Change the connection in the node and mesh state lists
- if ((state.connectivity & connectType) == 0) { state.connectivity |= connectType; eventConnectChange = 1; }
- state.meshid = meshid;
- } else {
- // Add the connection to the node and mesh state list
- obj.connectivityByNode[nodeid] = state = { connectivity: connectType, meshid: meshid };
- eventConnectChange = 1;
- }
-
- // Set node power state
- if (connectType == 1) { state.agentPower = powerState; } else if (connectType == 2) { state.ciraPower = powerState; } else if (connectType == 4) { state.amtPower = powerState; }
- var powerState = 0, oldPowerState = state.powerState;
- if ((state.connectivity & 1) != 0) { powerState = state.agentPower; } else if ((state.connectivity & 2) != 0) { powerState = state.ciraPower; } else if ((state.connectivity & 4) != 0) { powerState = state.amtPower; }
- if ((state.powerState == null) || (state.powerState == undefined) || (state.powerState != powerState)) {
- state.powerState = powerState;
- eventConnectChange = 1;
-
- // Set new power state in database
- const record = { time: new Date(connectTime), nodeid: nodeid, power: powerState };
- if (oldPowerState != null) { record.oldPower = oldPowerState; }
- obj.db.storePowerEvent(record, obj.multiServer);
- }
-
- // Event the node connection change
- if (eventConnectChange == 1) {
- obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: state.connectivity, pwr: state.powerState, ct: connectTime, nolog: 1, nopeers: 1, id: Math.random() });
-
- // Save indication of node connection change
- const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 1, connectType: connectType };
- if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
- obj.db.Set(lc);
-
- // Notify any users of device connection
- obj.NotifyUserOfDeviceStateChange(meshid, nodeid, connectTime, connectType, powerState, serverid, true, extraInfo);
- }
- } else {
- // Multi server mode
-
- // Change the node connection state
- if (serverid == null) { serverid = obj.serverId; }
- if (obj.peerConnectivityByNode[serverid] == null) return; // Guard against unknown serverid's
- var eventConnectChange = 0;
- var state = obj.peerConnectivityByNode[serverid][nodeid];
- if (state) {
- // Change the connection in the node and mesh state lists
- if ((state.connectivity & connectType) == 0) { state.connectivity |= connectType; eventConnectChange = 1; }
- state.meshid = meshid;
- } else {
- // Add the connection to the node and mesh state list
- obj.peerConnectivityByNode[serverid][nodeid] = state = { connectivity: connectType, meshid: meshid };
- eventConnectChange = 1;
- }
-
- // Set node power state
- if (connectType == 1) { state.agentPower = powerState; } else if (connectType == 2) { state.ciraPower = powerState; } else if (connectType == 4) { state.amtPower = powerState; }
- var powerState = 0, oldPowerState = state.powerState;
- if ((state.connectivity & 1) != 0) { powerState = state.agentPower; } else if ((state.connectivity & 2) != 0) { powerState = state.ciraPower; } else if ((state.connectivity & 4) != 0) { powerState = state.amtPower; }
- if ((state.powerState == null) || (state.powerState == undefined) || (state.powerState != powerState)) {
- state.powerState = powerState;
- eventConnectChange = 1;
-
- // Set new power state in database
- var record = { time: new Date(connectTime), nodeid: nodeid, power: powerState, server: obj.multiServer.serverid };
- if (oldPowerState != null) { record.oldPower = oldPowerState; }
- obj.db.storePowerEvent(record, obj.multiServer);
- }
-
- if (eventConnectChange == 1) {
- // Update the combined node state
- var x = {}; x[nodeid] = 1;
- obj.UpdateConnectivityState(x);
-
- // Save indication of node connection change
- if (serverid == obj.serverId) {
- const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 1, connectType: connectType, serverid: obj.serverId };
- if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
- obj.db.Set(lc);
- }
-
- // Notify any users of device connection
- obj.NotifyUserOfDeviceStateChange(meshid, nodeid, connectTime, connectType, powerState, serverid, true, extraInfo);
- }
- }
- };
-
- // Clear the connectivity state of a node and setup the server so that messages can be routed correctly.
- // meshId: mesh identifier of format mesh/domain/meshidhex
- // nodeId: node identifier of format node/domain/nodeidhex
- // connectType: Bitmask, 1 = MeshAgent, 2 = Intel AMT CIRA, 4 = Intel AMT local.
- obj.ClearConnectivityState = function (meshid, nodeid, connectType, serverid, extraInfo) {
- //console.log('ClearConnectivity for ' + nodeid.substring(0, 16) + ', Type: ' + connectTypeStrings[connectType] + (serverid == null?(''):(', ServerId: ' + serverid)));
- if ((serverid == null) && (obj.multiServer != null)) { obj.multiServer.DispatchMessage({ action: 'ClearConnectivityState', meshid: meshid, nodeid: nodeid, connectType: connectType, extraInfo: extraInfo }); }
-
- if (obj.multiServer == null) {
- // Single server mode
- var eventConnectChange = 0;
-
- // Remove the agent connection from the nodes connection list
- const state = obj.connectivityByNode[nodeid];
- if (state == null) return;
-
- if ((state.connectivity & connectType) != 0) {
- state.connectivity -= connectType;
-
- // Save indication of node connection change
- const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 0, connectType: connectType };
- if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
- obj.db.Set(lc);
-
- // If the node is completely disconnected, clean it up completely
- if (state.connectivity == 0) { delete obj.connectivityByNode[nodeid]; }
- eventConnectChange = 1;
- }
-
- // Clear node power state
- var powerState = 0;
- const oldPowerState = state.powerState;
- if (connectType == 1) { state.agentPower = 0; } else if (connectType == 2) { state.ciraPower = 0; } else if (connectType == 4) { state.amtPower = 0; }
- if ((state.connectivity & 1) != 0) { powerState = state.agentPower; } else if ((state.connectivity & 2) != 0) { powerState = state.ciraPower; } else if ((state.connectivity & 4) != 0) { powerState = state.amtPower; }
- if ((state.powerState == null) || (state.powerState != powerState)) {
- state.powerState = powerState;
- eventConnectChange = 1;
-
- // Set new power state in database
- obj.db.storePowerEvent({ time: new Date(), nodeid: nodeid, power: powerState, oldPower: oldPowerState }, obj.multiServer);
- }
-
- // Event the node connection change
- if (eventConnectChange == 1) {
- obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: state.connectivity, pwr: state.powerState, nolog: 1, nopeers: 1, id: Math.random() });
-
- // Notify any users of device disconnection
- obj.NotifyUserOfDeviceStateChange(meshid, nodeid, Date.now(), connectType, -1, serverid, false, extraInfo);
- }
- } else {
- // Multi server mode
-
- // Remove the agent connection from the nodes connection list
- if (serverid == null) { serverid = obj.serverId; }
- if (obj.peerConnectivityByNode[serverid] == null) return; // Guard against unknown serverid's
- var state = obj.peerConnectivityByNode[serverid][nodeid];
- if (state == null) return;
-
- // If existing state exist, remove this connection
- if ((state.connectivity & connectType) != 0) {
- state.connectivity -= connectType; // Remove one connectivity mode
-
- // Save indication of node connection change
- if (serverid == obj.serverId) {
- const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 0, connectType: connectType, serverid: obj.serverId };
- if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
- obj.db.Set(lc);
- }
-
- // If the node is completely disconnected, clean it up completely
- if (state.connectivity == 0) { delete obj.peerConnectivityByNode[serverid][nodeid]; state.powerState = 0; }
-
- // Notify any users of device disconnection
- obj.NotifyUserOfDeviceStateChange(meshid, nodeid, Date.now(), connectType, -1, serverid, false, extraInfo);
- }
-
- // Clear node power state
- if (connectType == 1) { state.agentPower = 0; } else if (connectType == 2) { state.ciraPower = 0; } else if (connectType == 4) { state.amtPower = 0; }
- var powerState = 0;
- if ((state.connectivity & 1) != 0) { powerState = state.agentPower; } else if ((state.connectivity & 2) != 0) { powerState = state.ciraPower; } else if ((state.connectivity & 4) != 0) { powerState = state.amtPower; }
- if ((state.powerState == null) || (state.powerState != powerState)) { state.powerState = powerState; }
-
- // Update the combined node state
- var x = {}; x[nodeid] = 1;
- obj.UpdateConnectivityState(x);
- }
- };
-
- // Escape a code string
- obj.escapeCodeString = function (str, keepUtf8) {
- const escapeCodeStringTable = { '\'': '\\\'', '\"': '\\"', '\\': '\\\\', '\b': '\\b', '\f': '\\f', '\n': '\\n', '\r': '\\r', '\t': '\\t' };
- var r = '', c, cr, table;
- for (var i = 0; i < str.length; i++) {
- c = str[i];
- table = escapeCodeStringTable[c];
- if (table != null) {
- r += table;
- } else if (keepUtf8 === true) {
- r += c;
- } else {
- cr = c.charCodeAt(0);
- if ((cr >= 32) && (cr <= 127)) { r += c; }
- }
- }
- return r;
- }
-
- // Update the default mesh core
- obj.updateMeshCore = function (func, dumpToFile) {
- // Figure out where meshcore.js is
- var meshcorePath = obj.datapath;
- if (obj.fs.existsSync(obj.path.join(meshcorePath, 'meshcore.js')) == false) {
- meshcorePath = obj.path.join(__dirname, 'agents');
- if (obj.fs.existsSync(obj.path.join(meshcorePath, 'meshcore.js')) == false) {
- obj.defaultMeshCores = obj.defaultMeshCoresHash = {}; if (func != null) { func(false); } // meshcore.js not found
- }
- }
-
- // Read meshcore.js and all .js files in the modules folder.
- var meshCore = null, modulesDir = null;
- const modulesAdd = {
- 'windows-amt': ['var addedModules = [];\r\n'],
- 'linux-amt': ['var addedModules = [];\r\n'],
- 'linux-noamt': ['var addedModules = [];\r\n']
- };
-
- // Read the recovery core if present
- var meshRecoveryCore = null;
- if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'recoverycore.js')) == true) {
- try { meshRecoveryCore = obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'recoverycore.js')).toString(); } catch (ex) { }
- if (meshRecoveryCore != null) {
- modulesAdd['windows-recovery'] = ['var addedModules = [];\r\n'];
- modulesAdd['linux-recovery'] = ['var addedModules = [];\r\n'];
- }
- }
-
- // Read the agent recovery core if present
- var meshAgentRecoveryCore = null;
- if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'meshcore_diagnostic.js')) == true) {
- try { meshAgentRecoveryCore = obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'meshcore_diagnostic.js')).toString(); } catch (ex) { }
- if (meshAgentRecoveryCore != null) {
- modulesAdd['windows-agentrecovery'] = ['var addedModules = [];\r\n'];
- modulesAdd['linux-agentrecovery'] = ['var addedModules = [];\r\n'];
- }
- }
-
- // Read the tiny core if present
- var meshTinyCore = null;
- if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'tinycore.js')) == true) {
- try { meshTinyCore = obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'tinycore.js')).toString(); } catch (ex) { }
- if (meshTinyCore != null) {
- modulesAdd['windows-tiny'] = ['var addedModules = [];\r\n'];
- modulesAdd['linux-tiny'] = ['var addedModules = [];\r\n'];
- }
- }
-
- if (obj.args.minifycore !== false) { try { meshCore = obj.fs.readFileSync(obj.path.join(meshcorePath, 'meshcore.min.js')).toString(); } catch (ex) { } } // Favor minified meshcore if present.
- if (meshCore == null) { try { meshCore = obj.fs.readFileSync(obj.path.join(meshcorePath, 'meshcore.js')).toString(); } catch (ex) { } } // Use non-minified meshcore.
- if (meshCore != null) {
- var moduleDirPath = null;
- if (obj.args.minifycore !== false) { try { moduleDirPath = obj.path.join(meshcorePath, 'modules_meshcore_min'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Favor minified modules if present.
- if (modulesDir == null) { try { moduleDirPath = obj.path.join(meshcorePath, 'modules_meshcore'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Use non-minified mofules.
- if (modulesDir != null) {
- for (var i in modulesDir) {
- if (modulesDir[i].toLowerCase().endsWith('.json')) {
- // We are adding a JSON file to the meshcores
- var moduleName = modulesDir[i].substring(0, modulesDir[i].length - 5);
- if (moduleName.endsWith('.min')) { moduleName = moduleName.substring(0, moduleName.length - 6); } // Remove the ".min" for ".min.json" files.
- const jsonData = obj.escapeCodeString(obj.fs.readFileSync(obj.path.join(moduleDirPath, modulesDir[i])).toString('utf8'), true);
- const moduleData = ['var ', moduleName, ' = JSON.parse(\'', jsonData, '\');\r\n'];
-
- // Add to all major cores
- modulesAdd['windows-amt'].push(...moduleData);
- modulesAdd['linux-amt'].push(...moduleData);
- modulesAdd['linux-noamt'].push(...moduleData);
- }
- if (modulesDir[i].toLowerCase().endsWith('.js')) {
- // We are adding a JS file to the meshcores
- var moduleName = modulesDir[i].substring(0, modulesDir[i].length - 3);
- if (moduleName.endsWith('.min')) { moduleName = moduleName.substring(0, moduleName.length - 4); } // Remove the ".min" for ".min.js" files.
- const moduleData = ['try { addModule("', moduleName, '", "', obj.escapeCodeString(obj.fs.readFileSync(obj.path.join(moduleDirPath, modulesDir[i])).toString('binary')), '"); addedModules.push("', moduleName, '"); } catch (ex) { }\r\n'];
-
- // Merge this module
- // NOTE: "smbios" module makes some non-AI Linux segfault, only include for IA platforms.
- if (moduleName.startsWith('amt-') || (moduleName == 'smbios')) {
- // Add to IA / Intel AMT cores only
- modulesAdd['windows-amt'].push(...moduleData);
- modulesAdd['linux-amt'].push(...moduleData);
- } else if (moduleName.startsWith('win-')) {
- // Add to Windows cores only
- modulesAdd['windows-amt'].push(...moduleData);
- } else if (moduleName.startsWith('linux-')) {
- // Add to Linux cores only
- modulesAdd['linux-amt'].push(...moduleData);
- modulesAdd['linux-noamt'].push(...moduleData);
- } else {
- // Add to all cores
- modulesAdd['windows-amt'].push(...moduleData);
- modulesAdd['linux-amt'].push(...moduleData);
- modulesAdd['linux-noamt'].push(...moduleData);
- }
-
- // Merge this module to recovery modules if needed
- if (modulesAdd['windows-recovery'] != null) {
- if ((moduleName == 'win-console') || (moduleName == 'win-message-pump') || (moduleName == 'win-terminal') || (moduleName == 'win-virtual-terminal')) {
- modulesAdd['windows-recovery'].push(...moduleData);
- }
- }
-
- // Merge this module to agent recovery modules if needed
- if (modulesAdd['windows-agentrecovery'] != null) {
- if ((moduleName == 'win-console') || (moduleName == 'win-message-pump') || (moduleName == 'win-terminal') || (moduleName == 'win-virtual-terminal')) {
- modulesAdd['windows-agentrecovery'].push(...moduleData);
- }
- }
- }
- }
- }
-
- // Add plugins to cores
- if (obj.pluginHandler) { obj.pluginHandler.addMeshCoreModules(modulesAdd); }
-
- // If we need to dump modules to file, create a meshcores folder
- if (dumpToFile) { try { obj.fs.mkdirSync('meshcores'); } catch (ex) { } }
-
- // Merge the cores and compute the hashes
- for (var i in modulesAdd) {
- if ((i == 'windows-recovery') || (i == 'linux-recovery')) {
- obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshRecoveryCore].join('');
- } else if ((i == 'windows-agentrecovery') || (i == 'linux-agentrecovery')) {
- obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshAgentRecoveryCore].join('');
- } else if ((i == 'windows-tiny') || (i == 'linux-tiny')) {
- obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshTinyCore].join('');
- } else {
- obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshCore].join('');
- }
- obj.defaultMeshCores[i] = Buffer.from(obj.defaultMeshCores[i], 'utf8');
- obj.defaultMeshCoresHash[i] = obj.crypto.createHash('sha384').update(obj.defaultMeshCores[i]).digest('binary');
- obj.debug('main', 'Core module ' + i + ' is ' + obj.defaultMeshCores[i].length + ' bytes.');
-
- // Write all modules to files. Great for debugging.
- if (dumpToFile) {
- console.log('Core module ' + i + ' is ' + obj.defaultMeshCores[i].length + ' bytes, saving to meshcores/' + i + '.js.'); // Print the core size and filename
- obj.fs.writeFile('meshcores/' + i + '.js', obj.defaultMeshCores[i].slice(4), function () { }); // Write the core to file
- }
-
- // Compress the mesh cores with DEFLATE
- const callback = function MeshCoreDeflateCb(err, buffer) { if (err == null) { obj.defaultMeshCoresDeflate[MeshCoreDeflateCb.i] = buffer; } }
- callback.i = i;
- require('zlib').deflate(obj.defaultMeshCores[i], { level: require('zlib').Z_BEST_COMPRESSION }, callback);
- }
- }
-
- // We are done creating all the mesh cores.
- if (func != null) { func(true); }
- };
-
- // Update the default meshcmd
- obj.updateMeshCmdTimer = 'notset';
- obj.updateMeshCmd = function (func) {
- // Figure out where meshcmd.js is and read it.
- var meshCmd = null, meshcmdPath, moduleAdditions = ['var addedModules = [];\r\n'], moduleDirPath, modulesDir = null;
- if ((obj.args.minifycore !== false) && (obj.fs.existsSync(obj.path.join(obj.datapath, 'meshcmd.min.js')))) { meshcmdPath = obj.path.join(obj.datapath, 'meshcmd.min.js'); meshCmd = obj.fs.readFileSync(meshcmdPath).toString(); }
- else if (obj.fs.existsSync(obj.path.join(obj.datapath, 'meshcmd.js'))) { meshcmdPath = obj.path.join(obj.datapath, 'meshcmd.js'); meshCmd = obj.fs.readFileSync(meshcmdPath).toString(); }
- else if ((obj.args.minifycore !== false) && (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'meshcmd.min.js')))) { meshcmdPath = obj.path.join(__dirname, 'agents', 'meshcmd.min.js'); meshCmd = obj.fs.readFileSync(meshcmdPath).toString(); }
- else if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'meshcmd.js'))) { meshcmdPath = obj.path.join(__dirname, 'agents', 'meshcmd.js'); meshCmd = obj.fs.readFileSync(meshcmdPath).toString(); }
- else { obj.defaultMeshCmd = null; if (func != null) { func(false); } return; } // meshcmd.js not found
- meshCmd = meshCmd.replace("'***Mesh*Cmd*Version***'", '\'' + getCurrentVersion() + '\'');
-
- // Figure out where the modules_meshcmd folder is.
- if (obj.args.minifycore !== false) { try { moduleDirPath = obj.path.join(meshcmdPath, 'modules_meshcmd_min'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Favor minified modules if present.
- if (modulesDir == null) { try { moduleDirPath = obj.path.join(meshcmdPath, 'modules_meshcmd'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Use non-minified mofules.
- if (obj.args.minifycore !== false) { if (modulesDir == null) { try { moduleDirPath = obj.path.join(__dirname, 'agents', 'modules_meshcmd_min'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } } // Favor minified modules if present.
- if (modulesDir == null) { try { moduleDirPath = obj.path.join(__dirname, 'agents', 'modules_meshcmd'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Use non-minified mofules.
-
- // Read all .js files in the meshcmd modules folder.
- if (modulesDir != null) {
- for (var i in modulesDir) {
- if (modulesDir[i].toLowerCase().endsWith('.js')) {
- // Merge this module
- var moduleName = modulesDir[i].substring(0, modulesDir[i].length - 3);
- if (moduleName.endsWith('.min')) { moduleName = moduleName.substring(0, moduleName.length - 4); } // Remove the ".min" for ".min.js" files.
- moduleAdditions.push('try { addModule("', moduleName, '", "', obj.escapeCodeString(obj.fs.readFileSync(obj.path.join(moduleDirPath, modulesDir[i])).toString('binary')), '"); addedModules.push("', moduleName, '"); } catch (ex) { }\r\n');
- }
- }
- }
-
- // Set the new default meshcmd.js
- moduleAdditions.push(meshCmd);
- obj.defaultMeshCmd = moduleAdditions.join('');
- //console.log('MeshCmd is ' + obj.defaultMeshCmd.length + ' bytes.'); // DEBUG, Print the merged meshcmd.js size
- //obj.fs.writeFile("C:\\temp\\meshcmd.js", obj.defaultMeshCmd.substring(4)); // DEBUG, Write merged meshcmd.js to file
- if (func != null) { func(true); }
-
- // Monitor for changes in meshcmd.js
- if (obj.updateMeshCmdTimer === 'notset') {
- obj.updateMeshCmdTimer = null;
- obj.fs.watch(meshcmdPath, function (eventType, filename) {
- if (obj.updateMeshCmdTimer != null) { clearTimeout(obj.updateMeshCmdTimer); obj.updateMeshCmdTimer = null; }
- obj.updateMeshCmdTimer = setTimeout(function () { obj.updateMeshCmd(); }, 5000);
- });
- }
- };
-
- // List of possible mesh agent install scripts
- const meshToolsList = {
- 'MeshCentralRouter': { localname: 'MeshCentralRouter.exe', dlname: 'winrouter' },
- 'MeshCentralAssistant': { localname: 'MeshCentralAssistant.exe', dlname: 'winassistant', winhash: true }
- //'MeshCentralRouterMacOS': { localname: 'MeshCentralRouter.dmg', dlname: 'MeshCentralRouter.dmg' }
- };
-
- // Update the list of available mesh agents
- obj.updateMeshTools = function () {
- for (var toolname in meshToolsList) {
- if (meshToolsList[toolname].winhash === true) {
- var toolpath = obj.path.join(__dirname, 'agents', meshToolsList[toolname].localname);
- const toolpath2 = obj.path.join(obj.datapath, 'agents', meshToolsList[toolname].localname);
- if (obj.fs.existsSync(toolpath2)) { toolpath = toolpath2; } // If the tool is present in "meshcentral-data/agents", use that one instead.
-
- var hashStream = obj.crypto.createHash('sha384');
- hashStream.toolname = toolname;
- hashStream.toolpath = toolpath;
- hashStream.dlname = meshToolsList[toolname].dlname;
- hashStream.hashx = 0;
- hashStream.on('data', function (data) {
- obj.meshToolsBinaries[this.toolname] = { hash: data.toString('hex'), hashx: this.hashx, path: this.toolpath, dlname: this.dlname, url: this.url };
- obj.meshToolsBinaries[this.toolname].url = 'https://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?meshaction=' + this.dlname;
- var stats = null;
- try { stats = obj.fs.statSync(this.toolpath); } catch (ex) { }
- if (stats != null) { obj.meshToolsBinaries[this.toolname].size = stats.size; }
- });
- const options = { sourcePath: toolpath, targetStream: hashStream };
- obj.exeHandler.hashExecutableFile(options);
- } else {
- var toolpath = obj.path.join(__dirname, 'agents', meshToolsList[toolname].localname);
- const toolpath2 = obj.path.join(obj.datapath, 'agents', meshToolsList[toolname].localname);
- if (obj.fs.existsSync(toolpath2)) { toolpath = toolpath2; } // If the tool is present in "meshcentral-data/agents", use that one instead.
-
- var stream = null;
- try {
- stream = obj.fs.createReadStream(toolpath);
- stream.on('data', function (data) { this.hash.update(data, 'binary'); this.hashx += data.length; });
- stream.on('error', function (data) {
- // If there is an error reading this file, make sure this agent is not in the agent table
- if (obj.meshToolsBinaries[this.toolname] != null) { delete obj.meshToolsBinaries[this.toolname]; }
- });
- stream.on('end', function () {
- // Add the agent to the agent table with all information and the hash
- obj.meshToolsBinaries[this.toolname] = {};
- obj.meshToolsBinaries[this.toolname].hash = this.hash.digest('hex');
- obj.meshToolsBinaries[this.toolname].hashx = this.hashx;
- obj.meshToolsBinaries[this.toolname].path = this.agentpath;
- obj.meshToolsBinaries[this.toolname].dlname = this.dlname;
- obj.meshToolsBinaries[this.toolname].url = 'https://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?meshaction=' + this.dlname;
- var stats = null;
- try { stats = obj.fs.statSync(this.agentpath); } catch (ex) { }
- if (stats != null) { obj.meshToolsBinaries[this.toolname].size = stats.size; }
- });
- stream.toolname = toolname;
- stream.agentpath = toolpath;
- stream.dlname = meshToolsList[toolname].dlname;
- stream.hash = obj.crypto.createHash('sha384', stream);
- stream.hashx = 0;
- } catch (ex) { }
- }
- }
- };
-
- // List of possible mesh agent install scripts
- const meshAgentsInstallScriptList = {
- 1: { id: 1, localname: 'meshinstall-linux.sh', rname: 'meshinstall.sh', linux: true },
- 2: { id: 2, localname: 'meshinstall-initd.sh', rname: 'meshagent', linux: true },
- 5: { id: 5, localname: 'meshinstall-bsd-rcd.sh', rname: 'meshagent', linux: true },
- 6: { id: 6, localname: 'meshinstall-linux.js', rname: 'meshinstall.js', linux: true }
- };
-
- // Update the list of available mesh agents
- obj.updateMeshAgentInstallScripts = function () {
- for (var scriptid in meshAgentsInstallScriptList) {
- var scriptpath = obj.path.join(__dirname, 'agents', meshAgentsInstallScriptList[scriptid].localname);
- var stream = null;
- try {
- stream = obj.fs.createReadStream(scriptpath);
- stream.xdata = '';
- stream.on('data', function (data) { this.hash.update(data, 'binary'); this.xdata += data; });
- stream.on('error', function (data) {
- // If there is an error reading this file, make sure this agent is not in the agent table
- if (obj.meshAgentInstallScripts[this.info.id] != null) { delete obj.meshAgentInstallScripts[this.info.id]; }
- });
- stream.on('end', function () {
- // Add the agent to the agent table with all information and the hash
- obj.meshAgentInstallScripts[this.info.id] = Object.assign({}, this.info);
- obj.meshAgentInstallScripts[this.info.id].hash = this.hash.digest('hex');
- obj.meshAgentInstallScripts[this.info.id].path = this.agentpath;
- obj.meshAgentInstallScripts[this.info.id].data = this.xdata;
- obj.meshAgentInstallScripts[this.info.id].url = 'https://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?script=' + this.info.id;
- var stats = null;
- try { stats = obj.fs.statSync(this.agentpath); } catch (ex) { }
- if (stats != null) { obj.meshAgentInstallScripts[this.info.id].size = stats.size; }
-
- // Place Unit line breaks on Linux scripts if not already present.
- if (obj.meshAgentInstallScripts[this.info.id].linux === true) { obj.meshAgentInstallScripts[this.info.id].data = obj.meshAgentInstallScripts[this.info.id].data.split('\r\n').join('\n') }
- });
- stream.info = meshAgentsInstallScriptList[scriptid];
- stream.agentpath = scriptpath;
- stream.hash = obj.crypto.createHash('sha384', stream);
- } catch (ex) { }
- }
- };
-
- // List of possible mesh agents
- obj.meshAgentsArchitectureNumbers = {
- 0: { id: 0, localname: 'Unknown', rname: 'meshconsole.exe', desc: 'Unknown agent', update: false, amt: true, platform: 'unknown', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 1: { id: 1, localname: 'MeshConsole.exe', rname: 'meshconsole32.exe', desc: 'Windows x86-32 console', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
- 2: { id: 2, localname: 'MeshConsole64.exe', rname: 'meshconsole64.exe', desc: 'Windows x86-64 console', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
- 3: { id: 3, localname: 'MeshService.exe', rname: 'meshagent32.exe', desc: 'Windows x86-32 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', codesign: true },
- 4: { id: 4, localname: 'MeshService64.exe', rname: 'meshagent64.exe', desc: 'Windows x86-64 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', codesign: true },
- 5: { id: 5, localname: 'meshagent_x86', rname: 'meshagent', desc: 'Linux x86-32', update: true, amt: true, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 6: { id: 6, localname: 'meshagent_x86-64', rname: 'meshagent', desc: 'Linux x86-64', update: true, amt: true, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 7: { id: 7, localname: 'meshagent_mips', rname: 'meshagent', desc: 'Linux MIPS', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 8: { id: 8, localname: 'MeshAgent-Linux-XEN-x86-32', rname: 'meshagent', desc: 'XEN x86-64', update: true, amt: false, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 9: { id: 9, localname: 'meshagent_arm', rname: 'meshagent', desc: 'Linux ARM5', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 10: { id: 10, localname: 'MeshAgent-Linux-ARM-PlugPC', rname: 'meshagent', desc: 'Linux ARM PlugPC', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 11: { id: 11, localname: 'meshagent_osx-x86-32', rname: 'meshosx', desc: 'Apple macOS x86-32', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Apple x86-32 binary, no longer supported.
- 12: { id: 12, localname: 'MeshAgent-Android-x86', rname: 'meshandroid', desc: 'Android x86-32', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 13: { id: 13, localname: 'meshagent_pogo', rname: 'meshagent', desc: 'Linux ARM PogoPlug', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 14: { id: 14, localname: 'meshagent_android.apk', rname: 'meshandroid.apk', desc: 'Android', update: false, amt: false, platform: 'android', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Get this one from Google Play
- 15: { id: 15, localname: 'meshagent_poky', rname: 'meshagent', desc: 'Linux Poky x86-32', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 16: { id: 16, localname: 'meshagent_osx-x86-64', rname: 'meshagent', desc: 'Apple macOS x86-64', update: true, amt: false, platform: 'osx', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Apple x86-64 binary
- 17: { id: 17, localname: 'MeshAgent-ChromeOS', rname: 'meshagent', desc: 'Google ChromeOS', update: false, amt: false, platform: 'chromeos', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Get this one from Chrome store
- 18: { id: 18, localname: 'meshagent_poky64', rname: 'meshagent', desc: 'Linux Poky x86-64', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 19: { id: 19, localname: 'meshagent_x86_nokvm', rname: 'meshagent', desc: 'Linux x86-32 NoKVM', update: true, amt: true, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 20: { id: 20, localname: 'meshagent_x86-64_nokvm', rname: 'meshagent', desc: 'Linux x86-64 NoKVM', update: true, amt: true, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 21: { id: 21, localname: 'MeshAgent-WinMinCore-Console-x86-32.exe', rname: 'meshagent.exe', desc: 'Windows MinCore Console x86-32', update: true, amt: false, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
- 22: { id: 22, localname: 'MeshAgent-WinMinCore-Service-x86-64.exe', rname: 'meshagent.exe', desc: 'Windows MinCore Service x86-32', update: true, amt: false, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
- 23: { id: 23, localname: 'MeshAgent-NodeJS', rname: 'meshagent', desc: 'NodeJS', update: false, amt: false, platform: 'node', core: 'nodejs', rcore: 'nodejs', arcore: 'nodejs', tcore: 'nodejs' }, // NodeJS based agent
- 24: { id: 24, localname: 'meshagent_arm-linaro', rname: 'meshagent', desc: 'Linux ARM Linaro', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 25: { id: 25, localname: 'meshagent_armhf', rname: 'meshagent', desc: 'Linux ARM - HardFloat', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // "armv6l" and "armv7l"
- 26: { id: 26, localname: 'meshagent_aarch64', rname: 'meshagent', desc: 'Linux ARM 64 bit (glibc/2.24)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // This is replaced by ARCHID 32
- 27: { id: 27, localname: 'meshagent_armhf2', rname: 'meshagent', desc: 'Linux ARM - HardFloat', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Raspbian 7 2015-02-02 for old Raspberry Pi.
- 28: { id: 28, localname: 'meshagent_mips24kc', rname: 'meshagent', desc: 'Linux MIPS24KC/MUSL (OpenWRT)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // MIPS Router with OpenWRT
- 29: { id: 29, localname: 'meshagent_osx-arm-64', rname: 'meshagent', desc: 'Apple macOS ARM-64', update: true, amt: false, platform: 'osx', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Apple Silicon ARM 64bit
- 30: { id: 30, localname: 'meshagent_freebsd_x86-64', rname: 'meshagent', desc: 'FreeBSD x86-64', update: true, amt: false, platform: 'freebsd', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // FreeBSD x64
- 32: { id: 32, localname: 'meshagent_aarch64', rname: 'meshagent', desc: 'Linux ARM 64 bit (glibc/2.24)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
- 33: { id: 33, localname: 'meshagent_openwrt_x86_64', rname: 'meshagent', desc: 'OpenWRT x86-64', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // This is replaced with ARCHID 36.
- 34: { id: 34, localname: 'assistant_windows', rname: 'meshassistant', desc: 'MeshCentral Assistant (Windows)', update: false, amt: false, platform: 'assistant', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // MeshCentral Assistant for Windows
- 35: { id: 35, localname: 'meshagent_linux-armada370-hf', rname: 'meshagent', desc: 'Armada370 - ARM32/HF (libc/2.26)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Armada370
- 36: { id: 36, localname: 'meshagent_openwrt_x86_64', rname: 'meshagent', desc: 'OpenWRT x86-64', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // OpenWRT x86-64
- 37: { id: 37, localname: 'meshagent_openbsd_x86-64', rname: 'meshagent', desc: 'OpenBSD x86-64', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // OpenBSD x86-64
- 40: { id: 40, localname: 'meshagent_mipsel24kc', rname: 'meshagent', desc: 'Linux MIPSEL24KC (OpenWRT)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // MIPS Router with OpenWRT
- 41: { id: 41, localname: 'meshagent_aarch64-cortex-a53', rname: 'meshagent', desc: 'ARMADA/CORTEX-A53/MUSL (OpenWRT)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // OpenWRT Routers
- 42: { id: 42, localname: 'MeshConsoleARM64.exe', rname: 'meshconsolearm64.exe', desc: 'Windows ARM-64 console', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
- 43: { id: 43, localname: 'MeshServiceARM64.exe', rname: 'meshagentarm64.exe', desc: 'Windows ARM-64 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', codesign: true },
- // 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)
- 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
- 10003: { id: 10003, localname: 'MeshService.exe', rname: 'meshagent32.exe', desc: 'Windows x86-32 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', unsigned: true },
- 10004: { id: 10004, localname: 'MeshService64.exe', rname: 'meshagent64.exe', desc: 'Windows x86-64 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', unsigned: true },
- 10005: { id: 10005, localname: 'meshagent_osx-universal-64', rname: 'meshagent', desc: 'Apple macOS Universal Binary', update: true, amt: false, platform: 'osx', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Apple Silicon + x86 universal binary
- 10006: { id: 10006, localname: 'MeshCentralAssistant.exe', rname: 'MeshCentralAssistant.exe', desc: 'MeshCentral Assistant for Windows', update: false, amt: false, platform: 'win32' }, // MeshCentral Assistant
- 11000: { id: 11000, localname: 'MeshCmd.exe', rname: 'MeshCmd.exe', desc: 'Windows x86-32 meshcmd', update: false, amt: true, platform: 'win32', codesign: true }, // MeshCMD for Windows x86 32-bit
- 11001: { id: 11001, localname: 'MeshCmd64.exe', rname: 'MeshCmd64.exe', desc: 'Windows x86-64 meshcmd', update: false, amt: true, platform: 'win32', codesign: true }, // MeshCMD for Windows x86 64-bit
- 11002: { id: 11002, localname: 'MeshCmdARM64.exe', rname: 'MeshCmdARM64.exe', desc: 'Windows ARM-64 meshcmd', update: false, amt: true, platform: 'win32', codesign: true } // MeshCMD for Windows ARM 64-bit
- };
-
- // Sign windows agents
- obj.signMeshAgents = function (domain, func) {
- // Setup the domain is specified
- var objx = domain, suffix = '';
- if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
-
- // Check if a custom agent signing certificate is available
- var agentSignCertInfo = require('./authenticode.js').loadCertificates([obj.path.join(obj.datapath, 'agentsigningcert.pem')]);
-
- // If not using a custom signing cert, get agent code signature certificate ready with the full cert chain
- if ((agentSignCertInfo == null) && (obj.certificates.codesign != null)) {
- agentSignCertInfo = {
- cert: obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.codesign.cert),
- key: obj.certificateOperations.forge.pki.privateKeyFromPem(obj.certificates.codesign.key),
- extraCerts: [obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.root.cert)]
- }
- }
- if (agentSignCertInfo == null) { func(); return; } // No code signing certificate, nothing to do.
-
- // Setup the domain is specified
- var objx = domain, suffix = '';
- if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
-
- // Generate the agent signature description and URL
- const serverSignedAgentsPath = obj.path.join(obj.datapath, 'signedagents' + suffix);
- const signDesc = (domain.title ? domain.title : agentSignCertInfo.cert.subject.hash);
- const httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
- var httpsHost = ((domain.dns != null) ? domain.dns : obj.certificates.CommonName);
- if (obj.args.agentaliasdns != null) { httpsHost = obj.args.agentaliasdns; }
- var signUrl = 'https://' + httpsHost;
- if (httpsPort != 443) { signUrl += ':' + httpsPort; }
- var xdomain = (domain.dns == null) ? domain.id : '';
- if (xdomain != '') xdomain += '/';
- signUrl += '/' + xdomain;
-
- // If requested, lock the agent to this server
- if (obj.config.settings.agentsignlock) { signUrl += '?ServerID=' + obj.certificateOperations.getPublicKeyHash(obj.certificates.agent.cert).toUpperCase(); }
-
- // Setup the time server
- var timeStampUrl = 'http://timestamp.comodoca.com/authenticode';
- if (obj.args.agenttimestampserver === false) { timeStampUrl = null; }
- else if (typeof obj.args.agenttimestampserver == 'string') { timeStampUrl = obj.args.agenttimestampserver; }
-
- // Setup the time server proxy
- var timeStampProxy = null;
- if (typeof obj.args.agenttimestampproxy == 'string') { timeStampProxy = obj.args.agenttimestampproxy; }
- else if ((obj.args.agenttimestampproxy !== false) && (typeof obj.args.npmproxy == 'string')) { timeStampProxy = obj.args.npmproxy; }
-
- // Setup the pending operations counter
- var pendingOperations = 1;
-
- for (var archid in obj.meshAgentsArchitectureNumbers) {
- if (obj.meshAgentsArchitectureNumbers[archid].codesign !== true) continue;
-
- var agentpath;
- if (domain.id == '') {
- // Load all agents when processing the default domain
- agentpath = obj.path.join(__dirname, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
- var agentpath2 = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
- if (obj.fs.existsSync(agentpath2)) { agentpath = agentpath2; delete obj.meshAgentsArchitectureNumbers[archid].codesign; } // If the agent is present in "meshcentral-data/agents", use that one instead.
- } else {
- // When processing an extra domain, only load agents that are specific to that domain
- agentpath = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
- if (obj.fs.existsSync(agentpath)) { delete obj.meshAgentsArchitectureNumbers[archid].codesign; } else { continue; } // If the agent is not present in "meshcentral-data/agents" skip.
- }
-
- // Open the original agent with authenticode
- const signeedagentpath = obj.path.join(serverSignedAgentsPath, obj.meshAgentsArchitectureNumbers[archid].localname);
- const originalAgent = require('./authenticode.js').createAuthenticodeHandler(agentpath);
- if (originalAgent != null) {
- // Check if the agent is already signed correctly
- const destinationAgent = require('./authenticode.js').createAuthenticodeHandler(signeedagentpath);
- var destinationAgentOk = (
- (destinationAgent != null) &&
- (destinationAgent.fileHashSigned != null) &&
- (Buffer.compare(destinationAgent.fileHashSigned, destinationAgent.fileHashActual) == 0) &&
- (destinationAgent.signingAttribs.indexOf(signUrl) >= 0) &&
- (destinationAgent.signingAttribs.indexOf(signDesc) >= 0)
- );
-
- if (destinationAgent != null) {
- // If the agent is signed correctly, look to see if the resources in the destination agent are correct
- var orgVersionStrings = originalAgent.getVersionInfo();
- if (destinationAgentOk == true) {
- const versionStrings = destinationAgent.getVersionInfo();
- const versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
- for (var i in versionProperties) {
- const prop = versionProperties[i], propl = prop.toLowerCase();
- if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo[propl] == 'string')) {
- if (domain.agentfileinfo[propl] != versionStrings[prop]) { destinationAgentOk = false; break; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
- } else {
- if (orgVersionStrings[prop] != versionStrings[prop]) { destinationAgentOk = false; break; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
- }
- }
-
- // Check file version number
- if (destinationAgentOk == true) {
- if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo['fileversionnumber'] == 'string')) {
- if (domain.agentfileinfo['fileversionnumber'] != versionStrings['~FileVersion']) { destinationAgentOk = false; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
- } else {
- if (orgVersionStrings['~FileVersion'] != versionStrings['~FileVersion']) { destinationAgentOk = false; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
- }
- }
-
- // Check product version number
- if (destinationAgentOk == true) {
- if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo['productversionnumber'] == 'string')) {
- if (domain.agentfileinfo['productversionnumber'] != versionStrings['~ProductVersion']) { destinationAgentOk = false; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
- } else {
- if (orgVersionStrings['~ProductVersion'] != versionStrings['~ProductVersion']) { destinationAgentOk = false; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
- }
- }
-
- // Check the agent icon
- if (destinationAgentOk == true) {
- if ((domain.agentfileinfo != null) && (domain.agentfileinfo.icon != null)) {
- // Check if the destination agent matches the icon we want
- const agentIconGroups = destinationAgent.getIconInfo();
- if (agentIconGroups != null) {
- const agentIconGroupNames = Object.keys(agentIconGroups);
- if (agentIconGroupNames.length > 0) {
- const agentMainIconGroup = agentIconGroups[agentIconGroupNames[0]];
- if (agentMainIconGroup.resCount != domain.agentfileinfo.icon.resCount) {
- destinationAgentOk = false; // The icon image count is different, don't bother hashing to see if the icons are different.
- } else {
- const agentMainIconGroupHash = require('./authenticode.js').hashObject(agentMainIconGroup);
- const iconHash = require('./authenticode.js').hashObject(domain.agentfileinfo.icon);
- if (agentMainIconGroupHash != iconHash) { destinationAgentOk = false; } // If the existing agent icon does not match the desired icon, we need to re-sign the agent.
- }
- }
- }
- } else {
- // Check if the destination agent has the default icon
- const agentIconGroups1 = destinationAgent.getIconInfo();
- const agentIconGroups2 = originalAgent.getIconInfo();
- if (agentIconGroups1.resCount != agentIconGroups2.resCount) {
- destinationAgentOk = false; // The icon image count is different, don't bother hashing to see if the icons are different.
- } else {
- const iconHash1 = require('./authenticode.js').hashObject(agentIconGroups1);
- const iconHash2 = require('./authenticode.js').hashObject(agentIconGroups2);
- if (iconHash1 != iconHash2) { destinationAgentOk = false; } // If the existing agent icon does not match the desired icon, we need to re-sign the agent.
- }
- }
- }
-
- // Check the agent logo
- if (destinationAgentOk == true) {
- if ((domain.agentfileinfo != null) && (domain.agentfileinfo.logo != null)) {
- // Check if the destination agent matches the logo we want
- const agentBitmaps = destinationAgent.getBitmapInfo();
- if (agentBitmaps != null) {
- const agentBitmapNames = Object.keys(agentBitmaps);
- if (agentBitmapNames.length > 0) {
- const agentMainBitmap = agentBitmaps[agentBitmapNames[0]];
- const agentMainBitmapHash = require('./authenticode.js').hashObject(agentMainBitmap);
- const bitmapHash = require('./authenticode.js').hashObject(domain.agentfileinfo.logo);
- if (agentMainBitmapHash != bitmapHash) { destinationAgentOk = false; } // If the existing agent logo does not match the desired logo, we need to re-sign the agent.
- }
- }
- } else {
- // Check if the destination agent has the default icon
- const agentBitmaps1 = destinationAgent.getBitmapInfo();
- const agentBitmaps2 = originalAgent.getBitmapInfo();
- const agentBitmapNames = Object.keys(agentBitmaps1);
- if (agentBitmapNames.length == 0) {
- destinationAgentOk = false;
- } else {
- const iconHash1 = require('./authenticode.js').hashObject(agentBitmaps1[agentBitmapNames[0]]);
- const iconHash2 = require('./authenticode.js').hashObject(agentBitmaps2[agentBitmapNames[0]]);
- if (iconHash1 != iconHash2) { destinationAgentOk = false; } // If the existing agent icon does not match the desired icon, we need to re-sign the agent.
- }
- }
- }
- }
-
- // If everything looks ok, runs a hash of the original and destination agent .text, .data and .rdata sections. If different, sign the agent again.
- if ((destinationAgentOk == true) && (originalAgent.getHashOfSection('sha384', '.text').compare(destinationAgent.getHashOfSection('sha384', '.text')) != 0)) { destinationAgentOk = false; }
- if ((destinationAgentOk == true) && (originalAgent.getHashOfSection('sha384', '.data').compare(destinationAgent.getHashOfSection('sha384', '.data')) != 0)) { destinationAgentOk = false; }
- if ((destinationAgentOk == true) && (originalAgent.getHashOfSection('sha384', '.rdata').compare(destinationAgent.getHashOfSection('sha384', '.rdata')) != 0)) { destinationAgentOk = false; }
-
- // We are done comparing the destination agent, close it.
- destinationAgent.close();
- }
-
- if (destinationAgentOk == false) {
- // If not signed correctly, sign it. First, create the server signed agent folder if needed
- try { obj.fs.mkdirSync(serverSignedAgentsPath); } catch (ex) { }
- const xagentSignedFunc = function agentSignedFunc(err, size) {
- if (err == null) {
- // Agent was signed succesfuly
- console.log(obj.common.format('Code signed {0}.', agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname));
- } else {
- // Failed to sign agent
- addServerWarning('Failed to sign \"' + agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname + '\": ' + err, 22, [agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname, err]);
- }
- obj.callExternalSignJob(agentSignedFunc.signingArguments); // Call external signing job regardless of success or failure
- if (--pendingOperations === 0) { agentSignedFunc.func(); }
- }
- pendingOperations++;
- xagentSignedFunc.func = func;
- xagentSignedFunc.objx = objx;
- xagentSignedFunc.archid = archid;
- xagentSignedFunc.signeedagentpath = signeedagentpath;
-
- // Parse the resources in the executable and make any required changes
- var resChanges = false, versionStrings = null;
- if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object')) {
- versionStrings = originalAgent.getVersionInfo();
- var versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
- // Change the agent string properties
- for (var i in versionProperties) {
- const prop = versionProperties[i], propl = prop.toLowerCase();
- if (domain.agentfileinfo[propl] && (domain.agentfileinfo[propl] != versionStrings[prop])) { versionStrings[prop] = domain.agentfileinfo[propl]; resChanges = true; }
- }
- // Change the agent file version
- if (domain.agentfileinfo['fileversionnumber'] && (domain.agentfileinfo['fileversionnumber'] != versionStrings['~FileVersion'])) {
- versionStrings['~FileVersion'] = domain.agentfileinfo['fileversionnumber']; resChanges = true;
- }
- // Change the agent product version
- if (domain.agentfileinfo['productversionnumber'] && (domain.agentfileinfo['productversionnumber'] != versionStrings['~ProductVersion'])) {
- versionStrings['~ProductVersion'] = domain.agentfileinfo['productversionnumber']; resChanges = true;
- }
- if (resChanges == true) { originalAgent.setVersionInfo(versionStrings); }
-
- // Change the agent icon
- if (domain.agentfileinfo.icon != null) {
- const agentIconGroups = originalAgent.getIconInfo();
- if (agentIconGroups != null) {
- const agentIconGroupNames = Object.keys(agentIconGroups);
- if (agentIconGroupNames.length > 0) {
- const agentMainIconGroupName = agentIconGroupNames[0];
- agentIconGroups[agentIconGroupNames[0]] = domain.agentfileinfo.icon;
- originalAgent.setIconInfo(agentIconGroups);
- }
- }
- }
-
- // Change the agent logo
- if (domain.agentfileinfo.logo != null) {
- const agentBitmaps = originalAgent.getBitmapInfo();
- if (agentBitmaps != null) {
- const agentBitmapNames = Object.keys(agentBitmaps);
- if (agentBitmapNames.length > 0) {
- agentBitmaps[agentBitmapNames[0]] = domain.agentfileinfo.logo;
- originalAgent.setBitmapInfo(agentBitmaps);
- }
- }
- }
- }
-
- const signingArguments = { out: signeedagentpath, desc: signDesc, url: signUrl, time: timeStampUrl, proxy: timeStampProxy }; // Shallow clone
- signingArguments.resChanges = resChanges;
-
- obj.debug('main', "Code signing with arguments: " + JSON.stringify(signingArguments));
- xagentSignedFunc.signingArguments = signingArguments; // Attach the signing arguments to the callback function
- if (resChanges == false) {
- // Sign the agent the simple way, without changing any resources.
- originalAgent.sign(agentSignCertInfo, signingArguments, xagentSignedFunc);
- } else {
- // Change the agent resources and sign the agent, this is a much more involved process.
- // NOTE: This is experimental and could corupt the agent.
- originalAgent.writeExecutable(signingArguments, agentSignCertInfo, xagentSignedFunc);
- }
-
- } else {
- // Signed agent is already ok, use it.
- originalAgent.close();
- }
-
-
- }
- }
-
- if (--pendingOperations === 0) { func(); }
- }
-
- obj.callExternalSignJob = function (signingArguments) {
- if (obj.config.settings && !obj.config.settings.externalsignjob) {
- return;
- }
- obj.debug('main', "External signing job called for file: " + signingArguments.out);
-
- const { spawnSync } = require('child_process');
-
- const signResult = spawnSync('"' + obj.config.settings.externalsignjob + '"', ['"' + signingArguments.out + '"'], {
- encoding: 'utf-8',
- shell: true,
- stdio: 'inherit'
- });
-
- if (signResult.error || signResult.status !== 0) {
- obj.debug('main', "External signing failed for file: " + signingArguments.out);
- console.error("External signing failed for file: " + signingArguments.out);
- return;
- }
- }
-
- // Update the list of available mesh agents
- obj.updateMeshAgentsTable = function (domain, func) {
- // Check if a custom agent signing certificate is available
- var agentSignCertInfo = require('./authenticode.js').loadCertificates([obj.path.join(obj.datapath, 'agentsigningcert.pem')]);
-
- // If not using a custom signing cert, get agent code signature certificate ready with the full cert chain
- if ((agentSignCertInfo == null) && (obj.certificates.codesign != null)) {
- agentSignCertInfo = {
- cert: obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.codesign.cert),
- key: obj.certificateOperations.forge.pki.privateKeyFromPem(obj.certificates.codesign.key),
- extraCerts: [obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.root.cert)]
- }
- }
-
- // Setup the domain is specified
- var objx = domain, suffix = '';
- if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
-
- // Load agent information file. This includes the data & time of the agent.
- const agentInfo = [];
- try { agentInfo = JSON.parse(obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'hashagents.json'), 'utf8')); } catch (ex) { }
-
- var archcount = 0;
- for (var archid in obj.meshAgentsArchitectureNumbers) {
- var agentpath;
- if (domain.id == '') {
- // Load all agents when processing the default domain
- agentpath = obj.path.join(__dirname, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
- if (obj.meshAgentsArchitectureNumbers[archid].unsigned !== true) {
- const agentpath2 = obj.path.join(obj.datapath, 'signedagents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
- if (obj.fs.existsSync(agentpath2)) { agentpath = agentpath2; } // If the agent is present in "meshcentral-data/signedagents", use that one instead.
- const agentpath3 = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
- if (obj.fs.existsSync(agentpath3)) { agentpath = agentpath3; } // If the agent is present in "meshcentral-data/agents", use that one instead.
- }
- } else {
- // When processing an extra domain, only load agents that are specific to that domain
- agentpath = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
- if (obj.fs.existsSync(agentpath)) { delete obj.meshAgentsArchitectureNumbers[archid].codesign; } else { continue; } // If the agent is not present in "meshcentral-data/agents" skip.
- }
-
- // Fetch agent binary information
- var stats = null;
- try { stats = obj.fs.statSync(agentpath); } catch (ex) { }
- if ((stats == null)) continue; // If this agent does not exist, skip it.
-
- // Setup agent information
- archcount++;
- objx.meshAgentBinaries[archid] = Object.assign({}, obj.meshAgentsArchitectureNumbers[archid]);
- objx.meshAgentBinaries[archid].path = agentpath;
- objx.meshAgentBinaries[archid].url = 'http://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?id=' + archid;
- objx.meshAgentBinaries[archid].size = stats.size;
- if ((agentInfo[archid] != null) && (agentInfo[archid].mtime != null)) { objx.meshAgentBinaries[archid].mtime = new Date(agentInfo[archid].mtime); } // Set agent time if available
-
- // If this is a windows binary, pull binary information
- if (obj.meshAgentsArchitectureNumbers[archid].platform == 'win32') {
- try { objx.meshAgentBinaries[archid].pe = obj.exeHandler.parseWindowsExecutable(agentpath); } catch (ex) { }
- }
-
- // If agents must be stored in RAM or if this is a Windows 32/64 agent, load the agent in RAM.
- if ((obj.args.agentsinram === true) || (((archid == 3) || (archid == 4)) && (obj.args.agentsinram !== false))) {
- if ((archid == 3) || (archid == 4)) {
- // Load the agent with a random msh added to it.
- const outStream = new require('stream').Duplex();
- outStream.meshAgentBinary = objx.meshAgentBinaries[archid];
- if (agentSignCertInfo) { outStream.meshAgentBinary.randomMsh = agentSignCertInfo.cert.subject.hash; } else { outStream.meshAgentBinary.randomMsh = obj.crypto.randomBytes(16).toString('hex'); }
- outStream.bufferList = [];
- outStream._write = function (chunk, encoding, callback) { this.bufferList.push(chunk); if (callback) callback(); }; // Append the chuck.
- outStream._read = function (size) { }; // Do nothing, this is not going to be called.
- outStream.on('finish', function () {
- // Merge all chunks
- this.meshAgentBinary.data = Buffer.concat(this.bufferList);
- this.meshAgentBinary.size = this.meshAgentBinary.data.length;
- delete this.bufferList;
-
- // Hash the uncompressed binary
- const hash = obj.crypto.createHash('sha384').update(this.meshAgentBinary.data);
- this.meshAgentBinary.fileHash = hash.digest('binary');
- this.meshAgentBinary.fileHashHex = Buffer.from(this.meshAgentBinary.fileHash, 'binary').toString('hex');
-
- // Compress the agent using ZIP
- const archive = require('archiver')('zip', { level: 9 }); // Sets the compression method.
- const onZipData = function onZipData(buffer) { onZipData.x.zacc.push(buffer); }
- const onZipEnd = function onZipEnd() {
- // Concat all the buffer for create compressed zip agent
- const concatData = Buffer.concat(onZipData.x.zacc);
- delete onZipData.x.zacc;
-
- // Hash the compressed binary
- const hash = obj.crypto.createHash('sha384').update(concatData);
- onZipData.x.zhash = hash.digest('binary');
- onZipData.x.zhashhex = Buffer.from(onZipData.x.zhash, 'binary').toString('hex');
-
- // Set the agent
- onZipData.x.zdata = concatData;
- onZipData.x.zsize = concatData.length;
- }
- const onZipError = function onZipError() { delete onZipData.x.zacc; }
- this.meshAgentBinary.zacc = [];
- onZipData.x = this.meshAgentBinary;
- onZipEnd.x = this.meshAgentBinary;
- onZipError.x = this.meshAgentBinary;
- archive.on('data', onZipData);
- archive.on('end', onZipEnd);
- archive.on('error', onZipError);
-
- // Starting with NodeJS v16, passing in a buffer at archive.append() will result a compressed file with zero byte length. To fix this, we pass in the buffer as a stream.
- // archive.append(this.meshAgentBinary.data, { name: 'meshagent' }); // This is the version that does not work on NodeJS v16.
- const ReadableStream = require('stream').Readable;
- const zipInputStream = new ReadableStream();
- zipInputStream.push(this.meshAgentBinary.data);
- zipInputStream.push(null);
- archive.append(zipInputStream, { name: 'meshagent' });
-
- archive.finalize();
- })
- obj.exeHandler.streamExeWithMeshPolicy(
- {
- platform: 'win32',
- sourceFileName: agentpath,
- destinationStream: outStream,
- randomPolicy: true, // Indicates that the msh policy is random data.
- msh: outStream.meshAgentBinary.randomMsh,
- peinfo: objx.meshAgentBinaries[archid].pe
- });
- } else {
- // Load the agent as-is
- objx.meshAgentBinaries[archid].data = obj.fs.readFileSync(agentpath);
-
- // Compress the agent using ZIP
- const archive = require('archiver')('zip', { level: 9 }); // Sets the compression method.
-
- const onZipData = function onZipData(buffer) { onZipData.x.zacc.push(buffer); }
- const onZipEnd = function onZipEnd() {
- // Concat all the buffer for create compressed zip agent
- const concatData = Buffer.concat(onZipData.x.zacc);
- delete onZipData.x.zacc;
-
- // Hash the compressed binary
- const hash = obj.crypto.createHash('sha384').update(concatData);
- onZipData.x.zhash = hash.digest('binary');
- onZipData.x.zhashhex = Buffer.from(onZipData.x.zhash, 'binary').toString('hex');
-
- // Set the agent
- onZipData.x.zdata = concatData;
- onZipData.x.zsize = concatData.length;
-
- //console.log('Packed', onZipData.x.size, onZipData.x.zsize);
- }
- const onZipError = function onZipError() { delete onZipData.x.zacc; }
- objx.meshAgentBinaries[archid].zacc = [];
- onZipData.x = objx.meshAgentBinaries[archid];
- onZipEnd.x = objx.meshAgentBinaries[archid];
- onZipError.x = objx.meshAgentBinaries[archid];
- archive.on('data', onZipData);
- archive.on('end', onZipEnd);
- archive.on('error', onZipError);
- archive.append(objx.meshAgentBinaries[archid].data, { name: 'meshagent' });
- archive.finalize();
- }
- }
-
- // Hash the binary
- const hashStream = obj.crypto.createHash('sha384');
- hashStream.archid = archid;
- hashStream.on('data', function (data) {
- objx.meshAgentBinaries[this.archid].hash = data.toString('binary');
- objx.meshAgentBinaries[this.archid].hashhex = data.toString('hex');
- if ((--archcount == 0) && (func != null)) { func(); }
- });
- const options = { sourcePath: agentpath, targetStream: hashStream, platform: obj.meshAgentsArchitectureNumbers[archid].platform };
- if (objx.meshAgentBinaries[archid].pe != null) { options.peinfo = objx.meshAgentBinaries[archid].pe; }
- obj.exeHandler.hashExecutableFile(options);
-
- // If we are not loading Windows binaries to RAM, compute the RAW file hash of the signed binaries here.
- if ((obj.args.agentsinram === false) && ((archid == 3) || (archid == 4))) {
- const hash = obj.crypto.createHash('sha384').update(obj.fs.readFileSync(agentpath));
- objx.meshAgentBinaries[archid].fileHash = hash.digest('binary');
- objx.meshAgentBinaries[archid].fileHashHex = Buffer.from(objx.meshAgentBinaries[archid].fileHash, 'binary').toString('hex');
- }
- }
- };
-
- // Generate a time limited user login token
- obj.getLoginToken = function (userid, func) {
- if ((userid == null) || (typeof userid != 'string')) { func('Invalid userid.'); return; }
- const x = userid.split('/');
- if (x == null || x.length != 3 || x[0] != 'user') { func('Invalid userid.'); return; }
- obj.db.Get(userid, function (err, docs) {
- if (err != null || docs == null || docs.length == 0) {
- func('User ' + userid + ' not found.'); return;
- } else {
- // Load the login cookie encryption key from the database
- obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
- if ((docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 160)) {
- // Key is present, use it.
- obj.loginCookieEncryptionKey = Buffer.from(docs[0].key, 'hex');
- func(obj.encodeCookie({ u: userid, a: 3 }, obj.loginCookieEncryptionKey));
- } else {
- // Key is not present, generate one.
- obj.loginCookieEncryptionKey = obj.generateCookieKey();
- obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() }, function () { func(obj.encodeCookie({ u: userid, a: 3 }, obj.loginCookieEncryptionKey)); });
- }
- });
- }
- });
- };
-
- // Show the user login token generation key
- obj.showLoginTokenKey = function (func) {
- // Load the login cookie encryption key from the database
- obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
- if ((docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 160)) {
- // Key is present, use it.
- func(docs[0].key);
- } else {
- // Key is not present, generate one.
- obj.loginCookieEncryptionKey = obj.generateCookieKey();
- obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() }, function () { func(obj.loginCookieEncryptionKey.toString('hex')); });
- }
- });
- };
-
- // Load the list of Intel AMT UUID and passwords from "amtactivation.log"
- obj.loadAmtActivationLogPasswords = function (func) {
- const amtlogfilename = obj.path.join(obj.datapath, 'amtactivation.log');
- obj.fs.readFile(amtlogfilename, 'utf8', function (err, data) {
- const amtPasswords = {}; // UUID --> [Passwords]
- if ((err == null) && (data != null)) {
- const lines = data.split('\n');
- for (var i in lines) {
- const line = lines[i];
- if (line.startsWith('{')) {
- var j = null;
- try { j = JSON.parse(line); } catch (ex) { }
- if ((j != null) && (typeof j == 'object')) {
- if ((typeof j.amtUuid == 'string') && (typeof j.password == 'string')) {
- if (amtPasswords[j.amtUuid] == null) {
- amtPasswords[j.amtUuid] = [j.password]; // Add password to array
- } else {
- amtPasswords[j.amtUuid].unshift(j.password); // Add password at the start of the array
- }
- }
- }
- }
- }
- // Remove all duplicates and only keep the 3 last passwords for any given device
- for (var i in amtPasswords) {
- amtPasswords[i] = [...new Set(amtPasswords[i])];
- while (amtPasswords[i].length > 3) { amtPasswords[i].pop(); }
- }
- }
- func(obj.common.sortObj(amtPasswords)); // Sort by UUID
- });
- }
-
- // Encrypt session data
- obj.encryptSessionData = function (data, key) {
- if (data == null) return null;
- if (key == null) { key = obj.loginCookieEncryptionKey; }
- try {
- const iv = Buffer.from(obj.crypto.randomBytes(12), 'binary'), cipher = obj.crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv);
- const crypted = Buffer.concat([cipher.update(JSON.stringify(data), 'utf8'), cipher.final()]);
- return Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString(obj.args.cookieencoding ? obj.args.cookieencoding : 'base64').replace(/\+/g, '@').replace(/\//g, '$');
- } catch (ex) { return null; }
- }
-
- // Decrypt the session data
- obj.decryptSessionData = function (data, key) {
- if ((typeof data != 'string') || (data.length < 13)) return {};
- if (key == null) { key = obj.loginCookieEncryptionKey; }
- try {
- const buf = Buffer.from(data.replace(/\@/g, '+').replace(/\$/g, '/'), obj.args.cookieencoding ? obj.args.cookieencoding : 'base64');
- const decipher = obj.crypto.createDecipheriv('aes-256-gcm', key.slice(0, 32), buf.slice(0, 12));
- decipher.setAuthTag(buf.slice(12, 28));
- return JSON.parse(decipher.update(buf.slice(28), 'binary', 'utf8') + decipher.final('utf8'));
- } catch (ex) { return {}; }
- }
-
- // Generate a cryptographic key used to encode and decode cookies
- obj.generateCookieKey = function () {
- return Buffer.from(obj.crypto.randomBytes(80), 'binary');
- //return Buffer.alloc(80, 0); // Sets the key to zeros, debug only.
- };
-
- // Encode an object as a cookie using a key using AES-GCM. (key must be 32 bytes or more)
- obj.encodeCookie = function (o, key) {
- try {
- if (key == null) { key = obj.serverKey; }
- o.time = Math.floor(Date.now() / 1000); // Add the cookie creation time
- const iv = Buffer.from(obj.crypto.randomBytes(12), 'binary'), cipher = obj.crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv);
- const crypted = Buffer.concat([cipher.update(JSON.stringify(o), 'utf8'), cipher.final()]);
- const r = Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString(obj.args.cookieencoding ? obj.args.cookieencoding : 'base64').replace(/\+/g, '@').replace(/\//g, '$');
- obj.debug('cookie', 'Encoded AESGCM cookie: ' + JSON.stringify(o));
- return r;
- } catch (ex) { obj.debug('cookie', 'ERR: Failed to encode AESGCM cookie due to exception: ' + ex); return null; }
- };
-
- // Decode a cookie back into an object using a key using AES256-GCM or AES128-CBC/HMAC-SHA384. Return null if it's not a valid cookie. (key must be 32 bytes or more)
- obj.decodeCookie = function (cookie, key, timeout) {
- if (cookie == null) return null;
- var r = obj.decodeCookieAESGCM(cookie, key, timeout);
- if (r === -1) { r = obj.decodeCookieAESSHA(cookie, key, timeout); } // If decodeCookieAESGCM() failed to decode, try decodeCookieAESSHA()
- if ((r == null) && (obj.args.cookieencoding == null) && (cookie.length != 64) && ((cookie == cookie.toLowerCase()) || (cookie == cookie.toUpperCase()))) {
- obj.debug('cookie', 'Upper/Lowercase cookie, try "CookieEncoding":"hex" in settings section of config.json.');
- console.log('Upper/Lowercase cookie, try "CookieEncoding":"hex" in settings section of config.json.');
- }
- if ((r != null) && (typeof r.once == 'string') && (r.once.length > 0)) {
- // This cookie must only be used once.
- if (timeout == null) { timeout = 2; }
- if (obj.cookieUseOnceTable[r.once] == null) {
- const ctimeout = (((r.expire) == null || (typeof r.expire != 'number')) ? (r.time + ((timeout + 3) * 60000)) : (r.time + ((r.expire + 3) * 60000)));
-
- // Store the used cookie in RAM
- obj.cookieUseOnceTable[r.once] = ctimeout;
-
- // Store the used cookie in the database
- // TODO
-
- // Send the used cookie to peer servers
- // TODO
-
- // Clean up the used table
- if (++obj.cookieUseOnceTableCleanCounter > 20) {
- const now = Date.now();
- for (var i in obj.cookieUseOnceTable) { if (obj.cookieUseOnceTable[i] < now) { delete obj.cookieUseOnceTable[i]; } }
- obj.cookieUseOnceTableCleanCounter = 0;
- }
- } else { return null; }
- }
- return r;
- }
-
- // Decode a cookie back into an object using a key using AES256-GCM. Return null if it's not a valid cookie. (key must be 32 bytes or more)
- obj.decodeCookieAESGCM = function (cookie, key, timeout) {
- try {
- if (key == null) { key = obj.serverKey; }
- cookie = Buffer.from(cookie.replace(/\@/g, '+').replace(/\$/g, '/'), obj.args.cookieencoding ? obj.args.cookieencoding : 'base64');
- const decipher = obj.crypto.createDecipheriv('aes-256-gcm', key.slice(0, 32), cookie.slice(0, 12));
- decipher.setAuthTag(cookie.slice(12, 28));
- const o = JSON.parse(decipher.update(cookie.slice(28), 'binary', 'utf8') + decipher.final('utf8'));
- if ((o.time == null) || (o.time == null) || (typeof o.time != 'number')) { obj.debug('cookie', 'ERR: Bad cookie due to invalid time'); return null; }
- o.time = o.time * 1000; // Decode the cookie creation time
- o.dtime = Date.now() - o.time; // Decode how long ago the cookie was created (in milliseconds)
- if ((o.expire) == null || (typeof o.expire != 'number')) {
- // Use a fixed cookie expire time
- if (timeout == null) { timeout = 2; }
- if ((o.dtime > (timeout * 60000)) || (o.dtime < -30000)) { obj.debug('cookie', 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right)
- } else {
- // An expire time is included in the cookie (in minutes), use this.
- if ((o.expire !== 0) && ((o.dtime > (o.expire * 60000)) || (o.dtime < -30000))) { obj.debug('cookie', 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right)
- }
- obj.debug('cookie', 'Decoded AESGCM cookie: ' + JSON.stringify(o));
- return o;
- } catch (ex) { obj.debug('cookie', 'ERR: Bad AESGCM cookie due to exception: ' + ex); return -1; }
- };
-
- // Decode a cookie back into an object using a key using AES256 / HMAC-SHA384. Return null if it's not a valid cookie. (key must be 80 bytes or more)
- // We do this because poor .NET does not support AES256-GCM.
- obj.decodeCookieAESSHA = function (cookie, key, timeout) {
- try {
- if (key == null) { key = obj.serverKey; }
- if (key.length < 80) { return null; }
- cookie = Buffer.from(cookie.replace(/\@/g, '+').replace(/\$/g, '/'), obj.args.cookieencoding ? obj.args.cookieencoding : 'base64');
- const decipher = obj.crypto.createDecipheriv('aes-256-cbc', key.slice(48, 80), cookie.slice(0, 16));
- const rawmsg = decipher.update(cookie.slice(16), 'binary', 'binary') + decipher.final('binary');
- const hmac = obj.crypto.createHmac('sha384', key.slice(0, 48));
- hmac.update(rawmsg.slice(48));
- if (Buffer.compare(hmac.digest(), Buffer.from(rawmsg.slice(0, 48))) == false) { return null; }
- const o = JSON.parse(rawmsg.slice(48).toString('utf8'));
- if ((o.time == null) || (o.time == null) || (typeof o.time != 'number')) { obj.debug('cookie', 'ERR: Bad cookie due to invalid time'); return null; }
- o.time = o.time * 1000; // Decode the cookie creation time
- o.dtime = Date.now() - o.time; // Decode how long ago the cookie was created (in milliseconds)
- if ((o.expire) == null || (typeof o.expire != 'number')) {
- // Use a fixed cookie expire time
- if (timeout == null) { timeout = 2; }
- if ((o.dtime > (timeout * 60000)) || (o.dtime < -30000)) { obj.debug('cookie', 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right)
- } else {
- // An expire time is included in the cookie (in minutes), use this.
- if ((o.expire !== 0) && ((o.dtime > (o.expire * 60000)) || (o.dtime < -30000))) { obj.debug('cookie', 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right)
- }
- obj.debug('cookie', 'Decoded AESSHA cookie: ' + JSON.stringify(o));
- return o;
- } catch (ex) { obj.debug('cookie', 'ERR: Bad AESSHA cookie due to exception: ' + ex); return null; }
- };
-
- // Debug
- obj.debug = function (source, ...args) {
- // Send event to console
- if ((obj.debugSources != null) && ((obj.debugSources == '*') || (obj.debugSources.indexOf(source) >= 0))) { console.log(source.toUpperCase() + ':', ...args); }
-
- // Send event to log file
- if (obj.config.settings && obj.config.settings.log) {
- if (typeof obj.args.log == 'string') { obj.args.log = obj.args.log.split(','); }
- if ((obj.args.log.indexOf(source) >= 0) || (obj.args.log[0] == '*')) {
- const d = new Date();
- if (obj.xxLogFile == null) {
- try {
- obj.xxLogFile = obj.fs.openSync(obj.getConfigFilePath('log.txt'), 'a+', 0o666);
- obj.fs.writeSync(obj.xxLogFile, '---- Log start at ' + new Date().toLocaleString() + ' ----\r\n');
- obj.xxLogDateStr = d.toLocaleDateString();
- } catch (ex) { }
- }
- if (obj.xxLogFile != null) {
- try {
- if (obj.xxLogDateStr != d.toLocaleDateString()) { obj.xxLogDateStr = d.toLocaleDateString(); obj.fs.writeSync(obj.xxLogFile, '---- ' + d.toLocaleDateString() + ' ----\r\n'); }
- const formattedArgs = args.map(function (arg) { return (typeof arg === 'object' && arg !== null) ? JSON.stringify(arg) : arg; });
- obj.fs.writeSync(obj.xxLogFile, new Date().toLocaleTimeString() + ' - ' + source + ': ' + formattedArgs.join(', ') + '\r\n');
- } catch (ex) { }
- }
- }
- }
-
- // Send the event to logged in administrators
- if ((obj.debugRemoteSources != null) && ((obj.debugRemoteSources == '*') || (obj.debugRemoteSources.indexOf(source) >= 0))) {
- var sendcount = 0;
- for (var sessionid in obj.webserver.wssessions2) {
- const ws = obj.webserver.wssessions2[sessionid];
- if ((ws != null) && (ws.userid != null)) {
- const user = obj.webserver.users[ws.userid];
- if ((user != null) && (user.siteadmin == 4294967295)) {
- try { ws.send(JSON.stringify({ action: 'trace', source: source, args: args, time: Date.now() })); sendcount++; } catch (ex) { }
- }
- }
- }
- if (sendcount == 0) { obj.debugRemoteSources = null; } // If there are no listeners, remove debug sources.
- }
- };
-
- // Update server state. Writes a server state file.
- const meshServerState = {};
- obj.updateServerState = function (name, val) {
- //console.log('updateServerState', name, val);
- try {
- if ((name != null) && (val != null)) {
- var changed = false;
- if ((name != null) && (meshServerState[name] != val)) { if ((val == null) && (meshServerState[name] != null)) { delete meshServerState[name]; changed = true; } else { if (meshServerState[name] != val) { meshServerState[name] = val; changed = true; } } }
- if (changed == false) return;
- }
- var r = 'time=' + Date.now() + '\r\n';
- for (var i in meshServerState) { r += (i + '=' + meshServerState[i] + '\r\n'); }
- try {
- obj.fs.writeFileSync(obj.getConfigFilePath('serverstate.txt'), r); // Try to write the server state, this may fail if we don't have permission.
- } catch (ex) { obj.serverSelfWriteAllowed = false; }
- } catch (ex) { } // Do nothing since this is not a critical feature.
- };
-
- // Read a list of IP addresses from a file
- function readIpListFromFile(arg) {
- if ((typeof arg != 'string') || (!arg.startsWith('file:'))) return arg;
- var lines = null;
- try { lines = obj.fs.readFileSync(obj.path.join(obj.datapath, arg.substring(5))).toString().split(/\r?\n/).join('\r').split('\r'); } catch (ex) { }
- if (lines == null) return null;
- const validLines = [];
- for (var i in lines) { if ((lines[i].length > 0) && (((lines[i].charAt(0) > '0') && (lines[i].charAt(0) < '9')) || (lines[i].charAt(0) == ':'))) validLines.push(lines[i]); }
- return validLines;
- }
-
- // Logging funtions
- function logException(e) { e += ''; logErrorEvent(e); }
- function logInfoEvent(msg) { if (obj.servicelog != null) { obj.servicelog.info(msg); } console.log(msg); }
- function logWarnEvent(msg) { if (obj.servicelog != null) { obj.servicelog.warn(msg); } console.log(msg); }
- function logErrorEvent(msg) { if (obj.servicelog != null) { obj.servicelog.error(msg); } console.error(msg); }
- obj.getServerWarnings = function () { return serverWarnings; }
- // TODO: migrate from other addServerWarning function and add timestamp
- obj.addServerWarning = function (msg, id, args, print) { serverWarnings.push({ msg: msg, id: id, args: args }); if (print !== false) { console.log("WARNING: " + msg); } }
-
- // auth.log functions
- obj.authLog = function (server, msg, args) {
- if (typeof msg != 'string') return;
- var str = msg;
- if (args != null) {
- if (typeof args.sessionid == 'string') { str += ', SessionID: ' + args.sessionid; }
- if (typeof args.useragent == 'string') { const userAgentInfo = obj.webserver.getUserAgentInfo(args.useragent); str += ', Browser: ' + userAgentInfo.browserStr + ', OS: ' + userAgentInfo.osStr; }
- }
- obj.debug('authlog', str);
- if (obj.syslogauth != null) { try { obj.syslogauth.log(obj.syslogauth.LOG_INFO, str); } catch (ex) { } }
- if (obj.authlogfile != null) { // Write authlog to file
- try {
- const d = new Date(), month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][d.getMonth()];
- str = month + ' ' + d.getDate() + ' ' + obj.common.zeroPad(d.getHours(), 2) + ':' + obj.common.zeroPad(d.getMinutes(), 2) + ':' + d.getSeconds() + ' meshcentral ' + server + '[' + process.pid + ']: ' + msg + ((obj.platform == 'win32') ? '\r\n' : '\n');
- obj.fs.write(obj.authlogfile, str, function (err, written, string) { if (err) { console.error(err); } });
- } catch (ex) { console.error(ex); }
- }
- }
-
- // Return the path of a file into the meshcentral-data path
- obj.getConfigFilePath = function (filename) {
- if ((obj.config != null) && (obj.config.configfiles != null) && (obj.config.configfiles[filename] != null) && (typeof obj.config.configfiles[filename] == 'string')) {
- //console.log('getConfigFilePath(\"' + filename + '\") = ' + obj.config.configfiles[filename]);
- return obj.config.configfiles[filename];
- }
- //console.log('getConfigFilePath(\"' + filename + '\") = ' + obj.path.join(obj.datapath, filename));
- return obj.path.join(obj.datapath, filename);
- };
-
- return obj;
-}
-
-// Resolve a list of names, call back with list of failed resolves.
-function checkResolveAll(names, func) {
- const dns = require('dns'), state = { func: func, count: names.length, err: null };
- for (var i in names) {
- dns.lookup(names[i], { all: true }, function (err, records) {
- if (err != null) { if (this.state.err == null) { this.state.err = [this.name]; } else { this.state.err.push(this.name); } }
- if (--this.state.count == 0) { this.state.func(this.state.err); }
- }.bind({ name: names[i], state: state }))
- }
-}
-
-// Resolve a list of domains to IP addresses, return a flat array of IPs.
-async function resolveDomainsToIps(originalArray) {
- if (!Array.isArray(originalArray)) { return undefined; }
- const flatResult = [];
- for (const item of originalArray) {
- if (new require('ipcheck')(item).valid) {
- flatResult.push(item);
- continue;
- }
- try {
- const results = await require('dns').promises.lookup(item, { all: true });
- flatResult.push(...results.map(r => r.address));
- } catch (err) {
- console.log(`Could not resolve ${item}`);
- }
- }
- if (flatResult.length == 0) { return undefined; }
- return flatResult;
-}
-
-// Return the server configuration
-function getConfig(createSampleConfig) {
- // Figure out the datapath location
- var i, datapath = null;
- const fs = require('fs'), path = require('path'), args = require('minimist')(process.argv.slice(2));
- if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) {
- datapath = path.join(__dirname, '../../meshcentral-data');
- } else {
- datapath = path.join(__dirname, '../meshcentral-data');
- }
- if (args.datapath) { datapath = args.datapath; }
- try { fs.mkdirSync(datapath); } catch (ex) { }
-
- // Read configuration file if present and change arguments.
- var config = {}, configFilePath = path.join(datapath, 'config.json');
- if (args.configfile) { configFilePath = common.joinPath(datapath, args.configfile); }
- if (fs.existsSync(configFilePath)) {
- // Load and validate the configuration file
- try { config = require(configFilePath); } catch (ex) { console.log('ERROR: Unable to parse ' + configFilePath + '.'); return null; }
- if (config.domains == null) { config.domains = {}; }
- for (i in config.domains) { if ((i.split('/').length > 1) || (i.split(' ').length > 1)) { console.log("ERROR: Error in config.json, domain names can't have spaces or /."); return null; } }
- } else {
- if (createSampleConfig === true) {
- // Copy the "sample-config.json" to give users a starting point
- const sampleConfigPath = path.join(__dirname, 'sample-config.json');
- if (fs.existsSync(sampleConfigPath)) { fs.createReadStream(sampleConfigPath).pipe(fs.createWriteStream(configFilePath)); }
- }
- }
-
- // Set the command line arguments to the config file if they are not present
- if (!config.settings) { config.settings = {}; }
- for (i in args) { config.settings[i] = args[i]; }
-
- // Lower case all keys in the config file
- try {
- require('./common.js').objKeysToLower(config, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'telegram/proxy']);
- } catch (ex) {
- console.log('CRITICAL ERROR: Unable to access the file \"./common.js\".\r\nCheck folder & file permissions.');
- process.exit();
- }
-
- return config;
-}
-
-// Check if a list of modules are present and install any missing ones
-function InstallModules(modules, args, func) {
- var missingModules = [];
- if (modules.length > 0) {
- const dependencies = require('./package.json').dependencies;
- for (var i in modules) {
- // Modules may contain a version tag (foobar@1.0.0), remove it so the module can be found using require
- const moduleNameAndVersion = modules[i];
- const moduleInfo = moduleNameAndVersion.split('@', 3);
- var moduleName = null;
- var moduleVersion = null;
- if(moduleInfo.length == 1){ // normal package without version
- moduleName = moduleInfo[0];
- } else if (moduleInfo.length == 2) { // normal package with a version OR custom repo package with no version
- moduleName = moduleInfo[0] === '' ? moduleNameAndVersion : moduleInfo[0];
- moduleVersion = moduleInfo[0] === '' ? null : moduleInfo[1];
- } else if (moduleInfo.length == 3) { // custom repo package and package with a version
- moduleName = "@" + moduleInfo[1];
- moduleVersion = moduleInfo[2];
- }
- try {
- // Does the module need a specific version?
- if (moduleVersion) {
- var versionMatch = false;
- var modulePath = null;
- // This is the first way to test if a module is already installed.
- try { versionMatch = (require(`${moduleName}/package.json`).version == moduleVersion) } catch (ex) {
- if (ex.code == "ERR_PACKAGE_PATH_NOT_EXPORTED") { modulePath = ("" + ex).split(' ').at(-1); } else { throw new Error(); }
- }
- // If the module is not installed, but we get the ERR_PACKAGE_PATH_NOT_EXPORTED error, try a second way.
- if ((versionMatch == false) && (modulePath != null)) {
- if (JSON.parse(require('fs').readFileSync(modulePath, 'utf8')).version != moduleVersion) { throw new Error(); }
- } else if (versionMatch == false) {
- throw new Error();
- }
- } else {
- // For all other modules, do the check here.
- // Is the module in package.json? Install exact version.
- if (typeof dependencies[moduleName] != null) { moduleVersion = dependencies[moduleName]; }
- require(moduleName);
- }
- } catch (ex) {
- missingModules.push(moduleNameAndVersion);
- }
- }
-
- if (missingModules.length > 0) { if (args.debug) { console.log('Missing Modules: ' + missingModules.join(', ')); } InstallModuleEx(missingModules, args, func); } else { func(); }
- }
-}
-
-// Install all missing modules at once. We will be running "npm install" once, with a full list of all modules we need, no matter if they area already installed or not,
-// this is to make sure NPM gives us exactly what we need. Also, we install the meshcentral with current version, so that NPM does not update it - which it will do if obmitted.
-function InstallModuleEx(modulenames, args, func) {
- var names = modulenames.join(' ');
- console.log('Installing modules', modulenames);
- const child_process = require('child_process');
- var parentpath = __dirname;
- function getCurrentVersion() { try { return JSON.parse(require('fs').readFileSync(require('path').join(__dirname, 'package.json'), 'utf8')).version; } catch (ex) { } return null; } // Fetch server version
- //const meshCentralVersion = getCurrentVersion();
- //if ((meshCentralVersion != null) && (args.dev == null)) { names = 'meshcentral@' + getCurrentVersion() + ' ' + names; }
-
- // Get the working directory
- if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) { parentpath = require('path').join(__dirname, '../..'); }
-
- if (args.debug) { console.log('NPM Command Line: ' + npmpath + ` install --save-exact --no-audit --omit=optional --no-fund ${names}`); }
- // always use --save-exact - https://stackoverflow.com/a/64507176/1210734
- child_process.exec(npmpath + ` install --save-exact --no-audit --no-optional --omit=optional ${names}`, { maxBuffer: 512000, timeout: 300000, cwd: parentpath }, function (error, stdout, stderr) {
- if ((error != null) && (error != '')) {
- var mcpath = __dirname;
- if (mcpath.endsWith('\\node_modules\\meshcentral') || mcpath.endsWith('/node_modules/meshcentral')) { mcpath = require('path').join(mcpath, '..', '..'); }
- console.log('ERROR: Unable to install required modules. MeshCentral may not have access to npm, or npm may not have suffisent rights to load the new module. To manualy install this module try:\r\n\r\n cd "' + mcpath + '"\r\n npm install --no-audit --no-optional --omit=optional ' + names + '\r\n node node_modules' + ((require('os').platform() == 'win32') ? '\\' : '/') + 'meshcentral');
- process.exit();
- return;
- }
- func();
- return;
- });
-}
-
-// Detect CTRL-C on Linux and stop nicely
-process.on('SIGINT', function () { if (meshserver != null) { meshserver.Stop(); meshserver = null; } console.log('Server Ctrl-C exit...'); process.exit(); });
-
-// Add a server warning, warnings will be shown to the administrator on the web application
-// TODO: migrate to obj.addServerWarning?
-const serverWarnings = [];
-function addServerWarning(msg, id, args, print) { serverWarnings.push({ msg: msg, id: id, args: args }); if (print !== false) { console.log("WARNING: " + msg); } }
-
-/*
-var ServerWarnings = {
- 1: "",
- 2: "Missing WebDAV parameters.",
- 3: "Unrecognized configuration option \"{0}\".",
- 4: "WebSocket compression is disabled, this feature is broken in NodeJS v11.11 to v12.15 and v13.2",
- 5: "Unable to load Intel AMT TLS root certificate for default domain.",
- 6: "Unable to load Intel AMT TLS root certificate for domain {0}.",
- 7: "CIRA local FQDN's ignored when server in LAN-only or WAN-only mode.",
- 8: "Can't have more than 4 CIRA local FQDN's. Ignoring value.",
- 9: "Agent hash checking is being skipped, this is unsafe.",
- 10: "Missing Let's Encrypt email address.",
- 11: "Invalid Let's Encrypt host names.",
- 12: "Invalid Let's Encrypt names, can't contain a *.",
- 13: "Unable to setup Let's Encrypt module.",
- 14: "Invalid Let's Encrypt names, unable to resolve: {0}",
- 15: "Invalid Let's Encrypt email address, unable to resolve: {0}",
- 16: "Unable to load CloudFlare trusted proxy IPv6 address list.",
- 17: "SendGrid server has limited use in LAN mode.",
- 18: "SMTP server has limited use in LAN mode.",
- 19: "SMS gateway has limited use in LAN mode.",
- 20: "Invalid \"LoginCookieEncryptionKey\" in config.json.",
- 21: "Backup path can't be set within meshcentral-data folder, backup settings ignored.",
- 22: "Failed to sign agent {0}: {1}",
- 23: "Unable to load agent icon file: {0}.",
- 24: "Unable to load agent logo file: {0}.",
- 25: "This NodeJS version does not support OpenID.",
- 26: "This NodeJS version does not support Discord.js.",
- 27: "Firebase now requires a service account JSON file, Firebase disabled."
-};
-*/
-
-// Load the really basic modules
-var npmpath = 'npm';
-var meshserver = null;
-var childProcess = null;
-var previouslyInstalledModules = {};
-function mainStart() {
- // Check the NodeJS is version 16 or better.
- if (Number(process.version.match(/^v(\d+\.\d+)/)[1]) < 16) { console.log("MeshCentral requires Node v16 or above, current version is " + process.version + "."); return; }
-
- // If running within the node_modules folder, move working directory to the parent of the node_modules folder.
- if (__dirname.endsWith('\\node_modules\\meshcentral') || __dirname.endsWith('/node_modules/meshcentral')) { process.chdir(require('path').join(__dirname, '..', '..')); }
-
- // Check for any missing modules.
- InstallModules(['minimist'], {}, function () {
- // Parse inbound arguments
- const args = require('minimist')(process.argv.slice(2));
-
- // Setup the NPM path
- if (args.npmpath == null) {
- try {
- var xnodepath = process.argv[0];
- var xnpmpath = require('path').join(require('path').dirname(process.argv[0]), 'npm');
- if (require('fs').existsSync(xnodepath) && require('fs').existsSync(xnpmpath)) {
- if (xnodepath.indexOf(' ') >= 0) { xnodepath = '"' + xnodepath + '"'; }
- if (xnpmpath.indexOf(' ') >= 0) { xnpmpath = '"' + xnpmpath + '"'; }
- if (require('os').platform() == 'win32') { npmpath = xnpmpath; } else { npmpath = (xnodepath + ' ' + xnpmpath); }
- }
- } catch (ex) { console.log(ex); }
- } else {
- npmpath = args.npmpath;
- }
-
- // Get the server configuration
- var config = getConfig(false);
- if (config == null) { process.exit(); }
-
- // Lowercase the auth value if present
- for (var i in config.domains) { if (typeof config.domains[i].auth == 'string') { config.domains[i].auth = config.domains[i].auth.toLowerCase(); } }
-
- // Get the current node version
- const verSplit = process.version.substring(1).split('.');
- var nodeVersion = parseInt(verSplit[0]) + (parseInt(verSplit[1]) / 100);
-
- // Check if RDP support if present
- var mstsc = true;
- try { require('./rdp') } catch (ex) { mstsc = false; }
-
- // Check if Windows SSPI, LDAP, Passport and YubiKey OTP will be used
- var sspi = false;
- var ldap = false;
- var passport = [];
- var allsspi = true;
- var yubikey = false;
- var ssh = false;
- var sessionRecording = false;
- var domainCount = 0;
- var wildleek = false;
- var nodemailer = false;
- var sendgrid = false;
- var captcha = false;
- if (require('os').platform() == 'win32') { for (var i in config.domains) { domainCount++; if (config.domains[i].auth == 'sspi') { sspi = true; } else { allsspi = false; } } } else { allsspi = false; }
- if (domainCount == 0) { allsspi = false; }
- for (var i in config.domains) {
- if (i.startsWith('_')) continue;
- if (((config.domains[i].smtp != null) && (config.domains[i].smtp.name != 'console')) || (config.domains[i].sendmail != null)) { nodemailer = true; }
- if (config.domains[i].sendgrid != null) { sendgrid = true; }
- if (config.domains[i].yubikey != null) { yubikey = true; }
- if (config.domains[i].auth == 'ldap') { ldap = true; }
- if (mstsc == false) { config.domains[i].mstsc = false; }
- if (config.domains[i].ssh == true) { ssh = true; }
- if ((typeof config.domains[i].authstrategies == 'object')) {
- if (passport.indexOf('passport') == -1) { passport.push('passport','connect-flash'); } // Passport v0.6.0 requires a patch, see https://github.com/jaredhanson/passport/issues/904 and include connect-flash here to display errors
- if ((typeof config.domains[i].authstrategies.twitter == 'object') && (typeof config.domains[i].authstrategies.twitter.clientid == 'string') && (typeof config.domains[i].authstrategies.twitter.clientsecret == 'string') && (passport.indexOf('passport-twitter') == -1)) { passport.push('passport-twitter'); }
- if ((typeof config.domains[i].authstrategies.google == 'object') && (typeof config.domains[i].authstrategies.google.clientid == 'string') && (typeof config.domains[i].authstrategies.google.clientsecret == 'string') && (passport.indexOf('passport-google-oauth20') == -1)) { passport.push('passport-google-oauth20'); }
- if ((typeof config.domains[i].authstrategies.github == 'object') && (typeof config.domains[i].authstrategies.github.clientid == 'string') && (typeof config.domains[i].authstrategies.github.clientsecret == 'string') && (passport.indexOf('passport-github2') == -1)) { passport.push('passport-github2'); }
- if ((typeof config.domains[i].authstrategies.azure == 'object') && (typeof config.domains[i].authstrategies.azure.clientid == 'string') && (typeof config.domains[i].authstrategies.azure.clientsecret == 'string') && (typeof config.domains[i].authstrategies.azure.tenantid == 'string') && (passport.indexOf('passport-azure-oauth2') == -1)) { passport.push('passport-azure-oauth2'); passport.push('jwt-simple'); }
- if ((typeof config.domains[i].authstrategies.oidc == 'object') && (passport.indexOf('openid-client@5.7.1') == -1)) {
- if ((nodeVersion >= 17)
- || ((Math.floor(nodeVersion) == 16) && (nodeVersion >= 16.13))
- || ((Math.floor(nodeVersion) == 14) && (nodeVersion >= 14.15))
- || ((Math.floor(nodeVersion) == 12) && (nodeVersion >= 12.19))) {
- passport.push('openid-client@5.7.1');
- } else {
- addServerWarning('This NodeJS version does not support OpenID Connect on MeshCentral.', 25);
- delete config.domains[i].authstrategies.oidc;
- }
- }
- if ((typeof config.domains[i].authstrategies.saml == 'object') || (typeof config.domains[i].authstrategies.jumpcloud == 'object')) { passport.push('passport-saml'); }
- }
- if (config.domains[i].sessionrecording != null) { sessionRecording = true; }
- if ((config.domains[i].passwordrequirements != null) && (config.domains[i].passwordrequirements.bancommonpasswords == true)) { wildleek = true; }
- if ((config.domains[i].newaccountscaptcha != null) && (config.domains[i].newaccountscaptcha !== false)) { captcha = true; }
- if ((typeof config.domains[i].duo2factor == 'object') && (passport.indexOf('@duosecurity/duo_universal') == -1)) { passport.push('@duosecurity/duo_universal@2.1.0'); }
- }
-
- // Build the list of required modules
- // NOTE: ALL MODULES MUST HAVE A VERSION NUMBER AND THE VERSION MUST MATCH THAT USED IN Dockerfile
- var modules = ['archiver@7.0.1', 'body-parser@1.20.3', 'cbor@5.2.0', 'compression@1.8.1', 'cookie-session@2.1.1', 'express@4.21.2', 'express-handlebars@7.1.3', 'express-ws@5.0.2', 'ipcheck@0.1.0', 'minimist@1.2.8', 'multiparty@4.2.3', '@seald-io/nedb', 'node-forge@1.3.1', 'ua-parser-js@1.0.40', 'ua-client-hints-js@0.1.2', 'ws@8.18.0', 'yauzl@2.10.0'];
- if (require('os').platform() == 'win32') { modules.push('node-windows@0.1.14'); modules.push('loadavg-windows@1.1.1'); if (sspi == true) { modules.push('node-sspi@0.2.10'); } } // Add Windows modules
- if (ldap == true) { modules.push('ldapauth-fork@5.0.5'); }
- if (ssh == true) { modules.push('ssh2@1.16.0'); }
- if (passport != null) { modules.push(...passport); }
- if (captcha == true) { modules.push('svg-captcha@1.4.0'); }
-
- if (sessionRecording == true) { modules.push('image-size@2.0.2'); } // Need to get the remote desktop JPEG sizes to index the recording file.
- if (config.letsencrypt != null) { modules.push('acme-client@4.2.5'); } // Add acme-client module. We need to force v4.2.4 or higher since olver versions using SHA-1 which is no longer supported by Let's Encrypt.
- if (config.settings.mqtt != null) { modules.push('aedes@0.39.0'); } // Add MQTT Modules
- if (config.settings.mysql != null) { modules.push('mysql2@3.11.4'); } // Add MySQL.
- //if (config.settings.mysql != null) { modules.push('@mysql/xdevapi@8.0.33'); } // Add MySQL, official driver (https://dev.mysql.com/doc/dev/connector-nodejs/8.0/)
- if (config.settings.mongodb != null) { modules.push('mongodb@4.17.2'); } // Add MongoDB, official driver. 4.17.0 and above now includes saslprep by default https://github.com/mongodb/node-mongodb-native/releases/tag/v4.17.0
- if (config.settings.postgres != null) { modules.push('pg@8.14.1') } // Add Postgres, official driver.
- if (config.settings.mariadb != null) { modules.push('mariadb@3.4.0'); } // Add MariaDB, official driver.
- if (config.settings.acebase != null) { modules.push('acebase@1.29.5'); } // Add AceBase, official driver.
- if (config.settings.sqlite3 != null) { modules.push('sqlite3@5.1.7'); } // Add sqlite3, official driver.
- if (config.settings.vault != null) { modules.push('node-vault@0.10.2'); } // Add official HashiCorp's Vault module.
- if ((config.settings.plugins != null) && (config.settings.plugins.proxy != null)) { modules.push('https-proxy-agent@7.0.2'); } // Required for HTTP/HTTPS proxy support
- else if (config.settings.xmongodb != null) { modules.push('mongojs@3.1.0'); } // Add MongoJS, old driver.
- if (nodemailer || ((config.smtp != null) && (config.smtp.name != 'console')) || (config.sendmail != null)) { modules.push('nodemailer@6.10.1'); } // Add SMTP support
- if (sendgrid || (config.sendgrid != null)) { modules.push('@sendgrid/mail'); } // Add SendGrid support
- if ((args.translate || args.dev) && (Number(process.version.match(/^v(\d+\.\d+)/)[1]) >= 16)) { modules.push('jsdom@22.1.0'); modules.push('esprima@4.0.1'); modules.push('html-minifier-terser@7.2.0'); } // Translation support
- if (typeof config.settings.crowdsec == 'object') { modules.push('@crowdsec/express-bouncer@0.1.0'); } // Add CrowdSec bounser module (https://www.npmjs.com/package/@crowdsec/express-bouncer)
- if (config.settings.prometheus != null) { modules.push('prom-client'); } // Add Prometheus Metrics support
-
- if (typeof config.settings.autobackup == 'object') {
- // Setup encrypted zip support if needed
- if (config.settings.autobackup.zippassword) { modules.push('archiver-zip-encrypted@2.0.0'); }
- // Enable Google Drive Support
- if (typeof config.settings.autobackup.googledrive == 'object') { modules.push('googleapis@128.0.0'); }
- // Enable WebDAV Support
- if (typeof config.settings.autobackup.webdav == 'object') {
- if ((typeof config.settings.autobackup.webdav.url != 'string') || (typeof config.settings.autobackup.webdav.username != 'string') || (typeof config.settings.autobackup.webdav.password != 'string')) { addServerWarning("Missing WebDAV parameters.", 2, null, !args.launch); } else { modules.push('webdav@5.8.0'); }
- }
- // Enable S3 Support
- if (typeof config.settings.autobackup.s3 == 'object') { modules.push('minio@8.0.2'); }
- }
-
- // Setup common password blocking
- if (wildleek == true) { modules.push('wildleek@2.0.0'); }
-
- // Setup 2nd factor authentication
- if (config.settings.no2factorauth !== true) {
- // Setup YubiKey OTP if configured
- if (yubikey == true) { modules.push('yub@0.11.1'); } // Add YubiKey OTP support (replaced yubikeyotp due to form-data issues)
- if (allsspi == false) { modules.push('otplib@12.0.1'); } // Google Authenticator support (v10 supports older NodeJS versions).
- }
-
- // Desktop multiplexor support
- if (config.settings.desktopmultiplex === true) { modules.push('image-size@2.0.2'); }
-
- // SMS support
- if (config.sms != null) {
- if (config.sms.provider == 'twilio') { modules.push('twilio@4.19.0'); }
- if (config.sms.provider == 'plivo') { modules.push('plivo@4.58.0'); }
- if (config.sms.provider == 'telnyx') { modules.push('telnyx@1.25.5'); }
- }
-
- // Messaging support
- if (config.messaging != null) {
- if (config.messaging.telegram != null) { modules.push('telegram@2.19.8'); modules.push('input@1.0.1'); }
- if (config.messaging.discord != null) { if (nodeVersion >= 17) { modules.push('discord.js@14.6.0'); } else { delete config.messaging.discord; addServerWarning('This NodeJS version does not support Discord.js.', 26); } }
- if (config.messaging.xmpp != null) { modules.push('@xmpp/client@0.13.1'); }
- if (config.messaging.pushover != null) { modules.push('node-pushover@1.0.0'); }
- if (config.messaging.zulip != null) { modules.push('zulip@0.1.0'); }
- }
-
- // Setup web based push notifications
- if ((typeof config.settings.webpush == 'object') && (typeof config.settings.webpush.email == 'string')) { modules.push('web-push@3.6.6'); }
-
- // Firebase Support
- if ((config.firebase != null) && (typeof config.firebase.serviceaccountfile == 'string')) { modules.push('firebase-admin@12.7.0'); }
-
- // Syslog support
- if ((require('os').platform() != 'win32') && (config.settings.syslog || config.settings.syslogjson)) { modules.push('modern-syslog@1.2.0'); }
- if (config.settings.syslogtcp) { modules.push('syslog@0.1.1-1'); }
-
- // Setup heapdump support if needed, useful for memory leak debugging
- // https://www.arbazsiddiqui.me/a-practical-guide-to-memory-leaks-in-nodejs/
- if (config.settings.heapdump === true) { modules.push('heapdump@0.3.15'); }
-
- // Install any missing modules and launch the server
- InstallModules(modules, args, function () {
- if (require('os').platform() == 'win32') { try { require('node-windows'); } catch (ex) { console.log("Module node-windows can't be loaded. Restart MeshCentral."); process.exit(); return; } }
- meshserver = CreateMeshCentralServer(config, args);
- meshserver.Start();
- });
-
- // On exit, also terminate the child process if applicable
- process.on('exit', function () { if (childProcess) { childProcess.kill(); childProcess = null; } });
-
- // If our parent exits, we also exit
- if (args.launch) {
- process.stderr.on('end', function () { process.exit(); });
- process.stdout.on('end', function () { process.exit(); });
- process.stdin.on('end', function () { process.exit(); });
- process.stdin.on('data', function (data) { });
- }
- });
-}
-
-if (require.main === module) {
- mainStart(); // Called directly, launch normally.
-} else {
- module.exports.mainStart = mainStart; // Required as a module, useful for winservice.js
-}
+/**
+* @description MeshCentral main module
+* @author Ylian Saint-Hilaire
+* @copyright Intel Corporation 2018-2022
+* @license Apache-2.0
+* @version v0.0.1
+*/
+
+/*xjslint node: true */
+/*xjslint plusplus: true */
+/*xjslint maxlen: 256 */
+/*jshint node: true */
+/*jshint strict: false */
+/*jshint esversion: 6 */
+"use strict";
+
+const common = require('./common.js');
+
+// If app metrics is available
+if (process.argv[2] == '--launch') { try { require('appmetrics-dash').monitor({ url: '/', title: 'MeshCentral', port: 88, host: '127.0.0.1' }); } catch (ex) { } }
+
+function CreateMeshCentralServer(config, args) {
+ const obj = {};
+ obj.db = null;
+ obj.webserver = null; // HTTPS main web server, typically on port 443
+ obj.redirserver = null; // HTTP relay web server, typically on port 80
+ obj.mpsserver = null; // Intel AMT CIRA server, typically on port 4433
+ obj.mqttbroker = null; // MQTT server, not is not often used
+ obj.swarmserver = null; // Swarm server, this is used only to update older MeshCentral v1 agents
+ obj.smsserver = null; // SMS server, used to send user SMS messages
+ obj.msgserver = null; // Messaging server, used to sent used messages
+ obj.amtEventHandler = null;
+ obj.pluginHandler = null;
+ obj.amtScanner = null;
+ obj.amtManager = null; // Intel AMT manager, used to oversee all Intel AMT devices, activate them and sync policies
+ obj.meshScanner = null;
+ obj.taskManager = null;
+ obj.letsencrypt = null; // Let's encrypt server, used to get and renew TLS certificates
+ obj.eventsDispatch = {};
+ obj.fs = require('fs');
+ obj.path = require('path');
+ obj.crypto = require('crypto');
+ obj.exeHandler = require('./exeHandler.js');
+ obj.platform = require('os').platform();
+ obj.args = args;
+ obj.common = common;
+ obj.configurationFiles = null;
+ obj.certificates = null;
+ obj.connectivityByNode = {}; // This object keeps a list of all connected CIRA and agents, by nodeid->value (value: 1 = Agent, 2 = CIRA, 4 = AmtDirect)
+ obj.peerConnectivityByNode = {}; // This object keeps a list of all connected CIRA and agents of peers, by serverid->nodeid->value (value: 1 = Agent, 2 = CIRA, 4 = AmtDirect)
+ obj.debugSources = [];
+ obj.debugRemoteSources = null;
+ obj.config = config; // Configuration file
+ obj.dbconfig = {}; // Persistance values, loaded from database
+ obj.certificateOperations = null;
+ obj.defaultMeshCmd = null;
+ obj.defaultMeshCores = {};
+ obj.defaultMeshCoresDeflate = {};
+ obj.defaultMeshCoresHash = {};
+ obj.meshToolsBinaries = {}; // Mesh Tools Binaries, ToolName --> { hash:(sha384 hash), size:(binary size), path:(binary path) }
+ obj.meshAgentBinaries = {}; // Mesh Agent Binaries, Architecture type --> { hash:(sha384 hash), size:(binary size), path:(binary path) }
+ obj.meshAgentInstallScripts = {}; // Mesh Install Scripts, Script ID -- { hash:(sha384 hash), size:(binary size), path:(binary path) }
+ obj.multiServer = null;
+ obj.ipKvmManager = null;
+ obj.maintenanceTimer = null;
+ obj.serverId = null;
+ obj.serverKey = Buffer.from(obj.crypto.randomBytes(48), 'binary');
+ obj.loginCookieEncryptionKey = null;
+ obj.invitationLinkEncryptionKey = null;
+ obj.serverSelfWriteAllowed = true;
+ obj.serverStatsCounter = Math.floor(Math.random() * 1000);
+ obj.taskLimiter = obj.common.createTaskLimiterQueue(50, 20, 60); // (maxTasks, maxTaskTime, cleaningInterval) This is a task limiter queue to smooth out server work.
+ obj.agentUpdateBlockSize = 65531; // MeshAgent update block size
+ obj.serverWarnings = []; // List of warnings that should be shown to administrators
+ obj.cookieUseOnceTable = {}; // List of cookies that are already expired
+ obj.cookieUseOnceTableCleanCounter = 0; // Clean the cookieUseOnceTable each 20 additions
+ obj.firstStats = true; // True until this server saves it's not stats to the database
+
+ // Server version
+ obj.currentVer = null;
+ function getCurrentVersion() { try { obj.currentVer = JSON.parse(obj.fs.readFileSync(obj.path.join(__dirname, 'package.json'), 'utf8')).version; } catch (ex) { } return obj.currentVer; } // Fetch server version
+ getCurrentVersion();
+
+ // Setup the default configuration and files paths
+ if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) {
+ obj.parentpath = obj.path.join(__dirname, '../..');
+ obj.datapath = obj.path.join(__dirname, '../../meshcentral-data');
+ obj.filespath = obj.path.join(__dirname, '../../meshcentral-files');
+ obj.backuppath = obj.path.join(__dirname, '../../meshcentral-backups');
+ obj.recordpath = obj.path.join(__dirname, '../../meshcentral-recordings');
+ obj.webViewsPath = obj.path.join(__dirname, 'views');
+ obj.webPublicPath = obj.path.join(__dirname, 'public');
+ obj.webEmailsPath = obj.path.join(__dirname, 'emails');
+ if (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web/views'))) { obj.webViewsOverridePath = obj.path.join(__dirname, '../../meshcentral-web/views'); }
+ if (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web/public'))) { obj.webPublicOverridePath = obj.path.join(__dirname, '../../meshcentral-web/public'); }
+ if (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web/emails'))) { obj.webEmailsOverridePath = obj.path.join(__dirname, '../../meshcentral-web/emails'); }
+ } else {
+ obj.parentpath = __dirname;
+ obj.datapath = obj.path.join(__dirname, '../meshcentral-data');
+ obj.filespath = obj.path.join(__dirname, '../meshcentral-files');
+ obj.backuppath = obj.path.join(__dirname, '../meshcentral-backups');
+ obj.recordpath = obj.path.join(__dirname, '../meshcentral-recordings');
+ obj.webViewsPath = obj.path.join(__dirname, 'views');
+ obj.webPublicPath = obj.path.join(__dirname, 'public');
+ obj.webEmailsPath = obj.path.join(__dirname, 'emails');
+ if (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web/views'))) { obj.webViewsOverridePath = obj.path.join(__dirname, '../meshcentral-web/views'); }
+ if (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web/public'))) { obj.webPublicOverridePath = obj.path.join(__dirname, '../meshcentral-web/public'); }
+ if (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web/emails'))) { obj.webEmailsOverridePath = obj.path.join(__dirname, '../meshcentral-web/emails'); }
+ }
+
+ // Clean up any temporary files
+ const removeTime = new Date(Date.now()).getTime() - (30 * 60 * 1000); // 30 minutes
+ const dir = obj.fs.readdir(obj.path.join(obj.filespath, 'tmp'), function (err, files) {
+ if (err != null) return;
+ for (var i in files) { try { const filepath = obj.path.join(obj.filespath, 'tmp', files[i]); if (obj.fs.statSync(filepath).mtime.getTime() < removeTime) { obj.fs.unlink(filepath, function () { }); } } catch (ex) { } }
+ });
+
+ // Look to see if data and/or file path is specified
+ if (obj.config.settings && (typeof obj.config.settings.datapath == 'string')) { obj.datapath = obj.config.settings.datapath; }
+ if (obj.config.settings && (typeof obj.config.settings.filespath == 'string')) { obj.filespath = obj.config.settings.filespath; }
+
+ // Create data and files folders if needed
+ try { obj.fs.mkdirSync(obj.datapath); } catch (ex) { }
+ try { obj.fs.mkdirSync(obj.filespath); } catch (ex) { }
+
+ // Windows Specific Code, setup service and event log
+ obj.service = null;
+ obj.servicelog = null;
+ if (obj.platform == 'win32') {
+ const nodewindows = require('node-windows');
+ obj.service = nodewindows.Service;
+ const eventlogger = nodewindows.EventLogger;
+ obj.servicelog = new eventlogger('MeshCentral');
+ }
+
+ // Start the Meshcentral server
+ obj.Start = function () {
+ var i;
+ try { require('./pass').hash('test', function () { }, 0); } catch (ex) { console.log('Old version of node, must upgrade.'); return; } // TODO: Not sure if this test works or not.
+
+ // Check for invalid arguments
+ const validArguments = ['_', 'user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'rediraliasport', 'cert', 'mpscert', 'deletedomain', 'deletedefaultdomain', 'showall', 'showusers', 'showitem', 'listuserids', 'showusergroups', 'shownodes', 'showallmeshes', 'showmeshes', 'showevents', 'showsmbios', 'showpower', 'clearpower', 'showiplocations', 'help', 'exactports', 'xinstall', 'xuninstall', 'install', 'uninstall', 'start', 'stop', 'restart', 'debug', 'filespath', 'datapath', 'noagentupdate', 'launch', 'noserverbackup', 'mongodb', 'mongodbcol', 'wanonly', 'lanonly', 'nousers', 'mpspass', 'ciralocalfqdn', 'dbexport', 'dbexportmin', 'dbimport', 'dbmerge', 'dbfix', 'dbencryptkey', 'selfupdate', 'tlsoffload', 'usenodedefaulttlsciphers', 'tlsciphers', 'userallowedip', 'userblockedip', 'swarmallowedip', 'agentallowedip', 'agentblockedip', 'fastcert', 'swarmport', 'logintoken', 'logintokenkey', 'logintokengen', 'mailtokengen', 'admin', 'unadmin', 'sessionkey', 'sessiontime', 'minify', 'minifycore', 'dblistconfigfiles', 'dbshowconfigfile', 'dbpushconfigfiles', 'oldencrypt', 'dbpullconfigfiles', 'dbdeleteconfigfiles', 'vaultpushconfigfiles', 'vaultpullconfigfiles', 'vaultdeleteconfigfiles', 'configkey', 'loadconfigfromdb', 'npmpath', 'serverid', 'recordencryptionrecode', 'vault', 'token', 'unsealkey', 'name', 'log', 'dbstats', 'translate', 'createaccount', 'setuptelegram', 'resetaccount', 'pass', 'removesubdomain', 'adminaccount', 'domain', 'email', 'configfile', 'maintenancemode', 'nedbtodb', 'removetestagents', 'agentupdatetest', 'hashpassword', 'hashpass', 'indexmcrec', 'mpsdebug', 'dumpcores', 'dev', 'mysql', 'mariadb', 'trustedproxy'];
+ for (var arg in obj.args) { obj.args[arg.toLocaleLowerCase()] = obj.args[arg]; if (validArguments.indexOf(arg.toLocaleLowerCase()) == -1) { console.log('Invalid argument "' + arg + '", use --help.'); return; } }
+ const ENVVAR_PREFIX = "meshcentral_"
+ let envArgs = []
+ for (let [envvar, envval] of Object.entries(process.env)) {
+ if (envvar.toLocaleLowerCase().startsWith(ENVVAR_PREFIX)) {
+ let argname = envvar.slice(ENVVAR_PREFIX.length).toLocaleLowerCase()
+ if (!!argname && !(validArguments.indexOf(argname) == -1)) {
+ envArgs = envArgs.concat([`--${argname}`, envval])
+ }
+ }
+ }
+ envArgs = require('minimist')(envArgs)
+ obj.args = Object.assign(envArgs, obj.args)
+ if (obj.args.mongodb == true) { console.log('Must specify: --mongodb [connectionstring] \r\nSee https://docs.mongodb.com/manual/reference/connection-string/ for MongoDB connection string.'); return; }
+ if (obj.args.mysql == true) { console.log('Must specify: --mysql [connectionstring] \r\nExample mysql://user:password@127.0.0.1:3306/database'); return; }
+ if (obj.args.mariadb == true) { console.log('Must specify: --mariadb [connectionstring] \r\nExample mariadb://user:password@127.0.0.1:3306/database'); return; }
+ for (i in obj.config.settings) { obj.args[i] = obj.config.settings[i]; } // Place all settings into arguments, arguments have already been placed into settings so arguments take precedence.
+
+ if ((obj.args.help == true) || (obj.args['?'] == true)) {
+ console.log('MeshCentral v' + getCurrentVersion() + ', remote computer management web portal.');
+ console.log('This software is open source under Apache 2.0 license.');
+ console.log('Details at: https://www.meshcentral.com\r\n');
+ if ((obj.platform == 'win32') || (obj.platform == 'linux')) {
+ console.log('Run as a background service');
+ console.log(' --install/uninstall Install MeshCentral as a background service.');
+ console.log(' --start/stop/restart Control MeshCentral background service.');
+ console.log('');
+ console.log('Run standalone, console application');
+ }
+ console.log(' --user [username] Always login as [username] if account exists.');
+ console.log(' --port [number] Web server port number.');
+ console.log(' --redirport [number] Creates an additional HTTP server to redirect users to the HTTPS server.');
+ console.log(' --exactports Server must run with correct ports or exit.');
+ console.log(' --noagentupdate Server will not update mesh agent native binaries.');
+ console.log(' --nedbtodb Transfer all NeDB records into current database.');
+ console.log(' --listuserids Show a list of a user identifiers in the database.');
+ console.log(' --cert [name], (country), (org) Create a web server certificate with [name] server name.');
+ console.log(' country and organization can optionally be set.');
+ console.log('');
+ console.log('Server recovery commands, use only when MeshCentral is offline.');
+ console.log(' --createaccount [userid] Create a new user account.');
+ console.log(' --resetaccount [userid] Unlock an account, disable 2FA and set a new account password.');
+ console.log(' --adminaccount [userid] Promote account to site administrator.');
+ return;
+ }
+
+ // Fix a NeDB database
+ if (obj.args.dbfix) {
+ var lines = null, badJsonCount = 0, fieldNames = [], fixedDb = [];
+ try { lines = obj.fs.readFileSync(obj.getConfigFilePath(obj.args.dbfix), { encoding: 'utf8' }).split('\n'); } catch (ex) { console.log('Invalid file: ' + obj.args.dbfix + ': ' + ex); process.exit(); }
+ for (var i = 0; i < lines.length; i++) {
+ var x = null;
+ try { x = JSON.parse(lines[i]); } catch (ex) { badJsonCount++; }
+ if (x != null) { fixedDb.push(lines[i]); for (var j in x) { if (fieldNames.indexOf(j) == -1) { fieldNames.push(j); } } }
+ }
+ console.log('Lines: ' + lines.length + ', badJSON: ' + badJsonCount + ', Feilds: ' + fieldNames);
+ obj.fs.writeFileSync(obj.getConfigFilePath(obj.args.dbfix) + '-fixed', fixedDb.join('\n'), { encoding: 'utf8' });
+ return;
+ }
+
+ // Check for invalid cert name
+ if ((obj.args.cert != null) && ((typeof obj.args.cert != "string") || (obj.args.cert.indexOf('@') >= 0) || (obj.args.cert.indexOf('/') >= 0) || (obj.args.cert.indexOf(':') >= 0))) { console.log("Invalid certificate name"); process.exit(); return; }
+
+ // Perform a password hash
+ if (obj.args.hashpassword) { require('./pass').hash(obj.args.hashpassword, function (err, salt, hash, tag) { console.log(salt + ',' + hash); process.exit(); }); return; }
+
+ // Dump to mesh cores
+ if (obj.args.dumpcores) { obj.updateMeshCore(function () { console.log('Done.'); }, true); return; }
+
+ // Setup Telegram
+ if (obj.args.setuptelegram) { require('./meshmessaging.js').SetupTelegram(obj); return; }
+
+ // Perform web site translations into different languages
+ if (obj.args.translate) {
+ // Check NodeJS version
+ const NodeJSVer = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
+ if (NodeJSVer < 8) { console.log("Translation feature requires Node v8 or above, current version is " + process.version + "."); process.exit(); return; }
+
+ // Check if translate.json is in the "meshcentral-data" folder, if so use that and translate default pages.
+ var translationFile = null, customTranslation = false;
+ if (require('fs').existsSync(obj.path.join(obj.datapath, 'translate.json'))) { translationFile = obj.path.join(obj.datapath, 'translate.json'); console.log("Using translate.json in meshcentral-data."); customTranslation = true; }
+ if (translationFile == null) { if (require('fs').existsSync(obj.path.join(__dirname, 'translate', 'translate.json'))) { translationFile = obj.path.join(__dirname, 'translate', 'translate.json'); console.log("Using default translate.json."); } }
+ if (translationFile == null) { console.log("Unable to find translate.json."); process.exit(); return; }
+
+ // Perform translation operations
+ var didSomething = false;
+ process.chdir(obj.path.join(__dirname, 'translate'));
+ const translateEngine = require('./translate/translate.js')
+ if (customTranslation == true) {
+ // Translate all of the default files using custom translation file
+ translateEngine.startEx(['', '', 'minifyall']);
+ translateEngine.startEx(['', '', 'translateall', translationFile]);
+ translateEngine.startEx(['', '', 'extractall', translationFile]);
+ didSomething = true;
+ } else {
+ // Translate all of the default files
+ translateEngine.startEx(['', '', 'minifyall']);
+ translateEngine.startEx(['', '', 'translateall']);
+ translateEngine.startEx(['', '', 'extractall']);
+ didSomething = true;
+ }
+
+ // Check if "meshcentral-web" exists, if so, translate all pages in that folder.
+ if (obj.webViewsOverridePath != null) {
+ didSomething = true;
+ var files = obj.fs.readdirSync(obj.webViewsOverridePath);
+ for (var i in files) {
+ var file = obj.path.join(obj.webViewsOverridePath, files[i]);
+ if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) {
+ translateEngine.startEx(['', '', 'minify', file]);
+ }
+ }
+ files = obj.fs.readdirSync(obj.webViewsOverridePath);
+ for (var i in files) {
+ var file = obj.path.join(obj.webViewsOverridePath, files[i]);
+ if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) {
+ translateEngine.startEx(['', '', 'translate', '*', translationFile, file, '--subdir:translations']);
+ }
+ }
+ }
+
+ // Check domains and see if "meshcentral-web-DOMAIN" exists, if so, translate all pages in that folder
+ for (i in obj.config.domains) {
+ if (i == "") continue;
+ var path = obj.path.join(obj.datapath, '..', 'meshcentral-web-' + i, 'views');
+ if (require('fs').existsSync(path)) {
+ didSomething = true;
+ var files = obj.fs.readdirSync(path);
+ for (var a in files) {
+ var file = obj.path.join(path, files[a]);
+ if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) {
+ translateEngine.startEx(['', '', 'minify', file]);
+ }
+ }
+ files = obj.fs.readdirSync(path);
+ for (var a in files) {
+ var file = obj.path.join(path, files[a]);
+ if (file.endsWith('.handlebars') && !file.endsWith('-min.handlebars')) {
+ translateEngine.startEx(['', '', 'translate', '*', translationFile, file, '--subdir:translations']);
+ }
+ }
+ }
+ }
+ /*
+ if (obj.webPublicOverridePath != null) {
+ didSomething = true;
+ var files = obj.fs.readdirSync(obj.webPublicOverridePath);
+ for (var i in files) {
+ var file = obj.path.join(obj.webPublicOverridePath, files[i]);
+ if (file.endsWith('.htm') && !file.endsWith('-min.htm')) {
+ translateEngine.startEx(['', '', 'translate', '*', translationFile, file, '--subdir:translations']);
+ }
+ }
+ }
+ */
+
+ if (didSomething == false) { console.log("Nothing to do."); }
+ console.log('Finished Translating.')
+ process.exit();
+ return;
+ }
+
+ // Setup the Node+NPM path if possible, this makes it possible to update the server even if NodeJS and NPM are not in default paths.
+ if (obj.args.npmpath == null) {
+ try {
+ var nodepath = process.argv[0];
+ var npmpath = obj.path.join(obj.path.dirname(process.argv[0]), 'npm');
+ if (obj.fs.existsSync(nodepath) && obj.fs.existsSync(npmpath)) {
+ if (nodepath.indexOf(' ') >= 0) { nodepath = '"' + nodepath + '"'; }
+ if (npmpath.indexOf(' ') >= 0) { npmpath = '"' + npmpath + '"'; }
+ if (obj.platform == 'win32') { obj.args.npmpath = npmpath; } else { obj.args.npmpath = (nodepath + ' ' + npmpath); }
+ }
+ } catch (ex) { }
+ }
+
+ // Linux background service systemd handling
+ if (obj.platform == 'linux') {
+ if (obj.args.install == true) {
+ // Install MeshCentral in Systemd
+ console.log('Installing MeshCentral as background Service...');
+ var systemdConf = null;
+ const userinfo = require('os').userInfo();
+ if (require('fs').existsSync('/etc/systemd/system')) { systemdConf = '/etc/systemd/system/meshcentral.service'; }
+ else if (require('fs').existsSync('/lib/systemd/system')) { systemdConf = '/lib/systemd/system/meshcentral.service'; }
+ else if (require('fs').existsSync('/usr/lib/systemd/system')) { systemdConf = '/usr/lib/systemd/system/meshcentral.service'; }
+ else { console.log('Unable to find systemd configuration folder.'); process.exit(); return; }
+ console.log('Writing config file...');
+ require('child_process').exec('which node', {}, function (error, stdout, stderr) {
+ if ((error != null) || (stdout.indexOf('\n') == -1)) { console.log('ERROR: Unable to get node location: ' + error); process.exit(); return; }
+ const nodePath = stdout.substring(0, stdout.indexOf('\n'));
+ const config = '[Unit]\nDescription=MeshCentral Server\nWants=network-online.target\nAfter=network-online.target\n\n[Service]\nType=simple\nLimitNOFILE=1000000\nExecStart=' + nodePath + ' ' + __dirname + '/meshcentral\nWorkingDirectory=' + userinfo.homedir + '\nEnvironment=NODE_ENV=production\nUser=' + userinfo.username + '\nGroup=' + userinfo.username + '\nRestart=always\n# Restart service after 10 seconds if node service crashes\nRestartSec=10\n# Set port permissions capability\nAmbientCapabilities=cap_net_bind_service\n\n[Install]\nWantedBy=multi-user.target\n';
+ require('child_process').exec('echo \"' + config + '\" | sudo tee ' + systemdConf, {}, function (error, stdout, stderr) {
+ if ((error != null) && (error != '')) { console.log('ERROR: Unable to write config file: ' + error); process.exit(); return; }
+ console.log('Enabling service...');
+ require('child_process').exec('sudo systemctl enable meshcentral.service', {}, function (error, stdout, stderr) {
+ if ((error != null) && (error != '')) { console.log('ERROR: Unable to enable MeshCentral as a service: ' + error); process.exit(); return; }
+ if (stdout.length > 0) { console.log(stdout); }
+ console.log('Starting service...');
+ require('child_process').exec('sudo systemctl start meshcentral.service', {}, function (error, stdout, stderr) {
+ if ((error != null) && (error != '')) { console.log('ERROR: Unable to start MeshCentral as a service: ' + error); process.exit(); return; }
+ if (stdout.length > 0) { console.log(stdout); }
+ console.log('Done.');
+ });
+ });
+ });
+ });
+ return;
+ } else if (obj.args.uninstall == true) {
+ // Uninstall MeshCentral in Systemd
+ console.log('Uninstalling MeshCentral background service...');
+ var systemdConf = null;
+ if (require('fs').existsSync('/etc/systemd/system')) { systemdConf = '/etc/systemd/system/meshcentral.service'; }
+ else if (require('fs').existsSync('/lib/systemd/system')) { systemdConf = '/lib/systemd/system/meshcentral.service'; }
+ else if (require('fs').existsSync('/usr/lib/systemd/system')) { systemdConf = '/usr/lib/systemd/system/meshcentral.service'; }
+ else { console.log('Unable to find systemd configuration folder.'); process.exit(); return; }
+ console.log('Stopping service...');
+ require('child_process').exec('sudo systemctl stop meshcentral.service', {}, function (err, stdout, stderr) {
+ if ((err != null) && (err != '')) { console.log('ERROR: Unable to stop MeshCentral as a service: ' + err); }
+ if (stdout.length > 0) { console.log(stdout); }
+ console.log('Disabling service...');
+ require('child_process').exec('sudo systemctl disable meshcentral.service', {}, function (err, stdout, stderr) {
+ if ((err != null) && (err != '')) { console.log('ERROR: Unable to disable MeshCentral as a service: ' + err); }
+ if (stdout.length > 0) { console.log(stdout); }
+ console.log('Removing config file...');
+ require('child_process').exec('sudo rm ' + systemdConf, {}, function (err, stdout, stderr) {
+ if ((err != null) && (err != '')) { console.log('ERROR: Unable to delete MeshCentral config file: ' + err); }
+ console.log('Done.');
+ });
+ });
+ });
+ return;
+ } else if (obj.args.start == true) {
+ // Start MeshCentral in Systemd
+ require('child_process').exec('sudo systemctl start meshcentral.service', {}, function (err, stdout, stderr) {
+ if ((err != null) && (err != '')) { console.log('ERROR: Unable to start MeshCentral: ' + err); process.exit(); return; }
+ console.log('Done.');
+ });
+ return;
+ } else if (obj.args.stop == true) {
+ // Stop MeshCentral in Systemd
+ require('child_process').exec('sudo systemctl stop meshcentral.service', {}, function (err, stdout, stderr) {
+ if ((err != null) && (err != '')) { console.log('ERROR: Unable to stop MeshCentral: ' + err); process.exit(); return; }
+ console.log('Done.');
+ });
+ return;
+ } else if (obj.args.restart == true) {
+ // Restart MeshCentral in Systemd
+ require('child_process').exec('sudo systemctl restart meshcentral.service', {}, function (err, stdout, stderr) {
+ if ((err != null) && (err != '')) { console.log('ERROR: Unable to restart MeshCentral: ' + err); process.exit(); return; }
+ console.log('Done.');
+ });
+ return;
+ }
+ }
+
+ // FreeBSD background service handling, MUST USE SPAWN FOR SERVICE COMMANDS!
+ if (obj.platform == 'freebsd') {
+ if (obj.args.install == true) {
+ // Install MeshCentral in rc.d
+ console.log('Installing MeshCentral as background Service...');
+ var systemdConf = "/usr/local/etc/rc.d/meshcentral";
+ const userinfo = require('os').userInfo();
+ console.log('Writing config file...');
+ require('child_process').exec('which node', {}, function (error, stdout, stderr) {
+ if ((error != null) || (stdout.indexOf('\n') == -1)) { console.log('ERROR: Unable to get node location: ' + error); process.exit(); return; }
+ const nodePath = stdout.substring(0, stdout.indexOf('\n'));
+ const config = '#!/bin/sh\n# MeshCentral FreeBSD Service Script\n# PROVIDE: meshcentral\n# REQUIRE: NETWORKING\n# KEYWORD: shutdown\n. /etc/rc.subr\nname=meshcentral\nuser=' + userinfo.username + '\nrcvar=meshcentral_enable\n: \\${meshcentral_enable:=\\"NO\\"}\n: \\${meshcentral_args:=\\"\\"}\npidfile=/var/run/meshcentral/meshcentral.pid\ncommand=\\"/usr/sbin/daemon\\"\nmeshcentral_chdir=\\"' + obj.parentpath + '\\"\ncommand_args=\\"-r -u \\${user} -P \\${pidfile} -S -T meshcentral -m 3 ' + nodePath + ' ' + __dirname + ' \\${meshcentral_args}\\"\nload_rc_config \\$name\nrun_rc_command \\"\\$1\\"\n';
+ require('child_process').exec('echo \"' + config + '\" | tee ' + systemdConf + ' && chmod +x ' + systemdConf, {}, function (error, stdout, stderr) {
+ if ((error != null) && (error != '')) { console.log('ERROR: Unable to write config file: ' + error); process.exit(); return; }
+ console.log('Enabling service...');
+ require('child_process').exec('sysrc meshcentral_enable="YES"', {}, function (error, stdout, stderr) {
+ if ((error != null) && (error != '')) { console.log('ERROR: Unable to enable MeshCentral as a service: ' + error); process.exit(); return; }
+ if (stdout.length > 0) { console.log(stdout); }
+ console.log('Starting service...');
+ const service = require('child_process').spawn('service', ['meshcentral', 'start']);
+ service.stdout.on('data', function (data) { console.log(data.toString()); });
+ service.stderr.on('data', function (data) { console.log(data.toString()); });
+ service.on('exit', function (code) {
+ console.log((code === 0) ? 'Done.' : 'ERROR: Unable to start MeshCentral as a service');
+ process.exit(); // Must exit otherwise we just hang
+ });
+ });
+ });
+ });
+ return;
+ } else if (obj.args.uninstall == true) {
+ // Uninstall MeshCentral in rc.d
+ console.log('Uninstalling MeshCentral background service...');
+ var systemdConf = "/usr/local/etc/rc.d/meshcentral";
+ console.log('Stopping service...');
+ const service = require('child_process').spawn('service', ['meshcentral', 'stop']);
+ service.stdout.on('data', function (data) { console.log(data.toString()); });
+ service.stderr.on('data', function (data) { console.log(data.toString()); });
+ service.on('exit', function (code) {
+ if (code !== 0) { console.log('ERROR: Unable to stop MeshCentral as a service'); }
+ console.log('Disabling service...');
+ require('child_process').exec('sysrc -x meshcentral_enable', {}, function (err, stdout, stderr) {
+ if ((err != null) && (err != '')) { console.log('ERROR: Unable to disable MeshCentral as a service: ' + err); }
+ if (stdout.length > 0) { console.log(stdout); }
+ console.log('Removing config file...');
+ require('child_process').exec('rm ' + systemdConf, {}, function (err, stdout, stderr) {
+ if ((err != null) && (err != '')) { console.log('ERROR: Unable to delete MeshCentral config file: ' + err); }
+ console.log('Done.');
+ process.exit(); // Must exit otherwise we just hang
+ });
+ });
+ });
+ return;
+ } else if (obj.args.start == true) {
+ // Start MeshCentral in rc.d
+ const service = require('child_process').spawn('service', ['meshcentral', 'start']);
+ service.stdout.on('data', function (data) { console.log(data.toString()); });
+ service.stderr.on('data', function (data) { console.log(data.toString()); });
+ service.on('exit', function (code) {
+ console.log((code === 0) ? 'Done.' : 'ERROR: Unable to start MeshCentral as a service: ' + error);
+ process.exit(); // Must exit otherwise we just hang
+ });
+ return;
+ } else if (obj.args.stop == true) {
+ // Stop MeshCentral in rc.d
+ const service = require('child_process').spawn('service', ['meshcentral', 'stop']);
+ service.stdout.on('data', function (data) { console.log(data.toString()); });
+ service.stderr.on('data', function (data) { console.log(data.toString()); });
+ service.on('exit', function (code) {
+ console.log((code === 0) ? 'Done.' : 'ERROR: Unable to stop MeshCentral as a service: ' + error);
+ process.exit(); // Must exit otherwise we just hang
+ });
+ return;
+ } else if (obj.args.restart == true) {
+ // Restart MeshCentral in rc.d
+ const service = require('child_process').spawn('service', ['meshcentral', 'restart']);
+ service.stdout.on('data', function (data) { console.log(data.toString()); });
+ service.stderr.on('data', function (data) { console.log(data.toString()); });
+ service.on('exit', function (code) {
+ console.log((code === 0) ? 'Done.' : 'ERROR: Unable to restart MeshCentral as a service: ' + error);
+ process.exit(); // Must exit otherwise we just hang
+ });
+ return;
+ }
+ }
+
+ // Index a recorded file
+ if (obj.args.indexmcrec != null) {
+ if (typeof obj.args.indexmcrec != 'string') {
+ console.log('Usage: --indexmrec [filename.mcrec]');
+ } else if (obj.fs.existsSync(obj.args.indexmcrec)) {
+ console.log('Indexing file: ' + obj.args.indexmcrec);
+ require(require('path').join(__dirname, 'mcrec.js')).indexFile(obj.args.indexmcrec);
+ } else {
+ console.log('Unable to find file: ' + obj.args.indexmcrec);
+ }
+ return;
+ }
+
+ // Windows background service handling
+ if ((obj.platform == 'win32') && (obj.service != null)) {
+ // Build MeshCentral parent path and Windows Service path
+ var mcpath = __dirname;
+ if (mcpath.endsWith('\\node_modules\\meshcentral') || mcpath.endsWith('/node_modules/meshcentral')) { mcpath = require('path').join(mcpath, '..', '..'); }
+ const servicepath = obj.path.join(mcpath, 'WinService');
+
+ // Check if we need to install, start, stop, remove ourself as a background service
+ if (((obj.args.xinstall == true) || (obj.args.xuninstall == true) || (obj.args.start == true) || (obj.args.stop == true) || (obj.args.restart == true))) {
+ var env = [], xenv = ['user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'exactport', 'rediraliasport', 'debug'];
+ for (i in xenv) { if (obj.args[xenv[i]] != null) { env.push({ name: 'mesh' + xenv[i], value: obj.args[xenv[i]] }); } } // Set some args as service environment variables.
+
+ var serviceFilePath = null;
+ if (obj.fs.existsSync(obj.path.join(servicepath, 'winservice.js'))) { serviceFilePath = obj.path.join(servicepath, 'winservice.js'); }
+ else if (obj.fs.existsSync(obj.path.join(__dirname, '../WinService/winservice.js'))) { serviceFilePath = obj.path.join(__dirname, '../WinService/winservice.js'); }
+ else if (obj.fs.existsSync(obj.path.join(__dirname, 'winservice.js'))) { serviceFilePath = obj.path.join(__dirname, 'winservice.js'); }
+ if (serviceFilePath == null) { console.log('Unable to find winservice.js'); return; }
+
+ const svc = new obj.service({ name: 'MeshCentral', description: 'MeshCentral Remote Management Server', script: servicepath, env: env, wait: 2, grow: 0.5 });
+ svc.on('install', function () { console.log('MeshCentral service installed.'); svc.start(); });
+ svc.on('uninstall', function () { console.log('MeshCentral service uninstalled.'); process.exit(); });
+ svc.on('start', function () { console.log('MeshCentral service started.'); process.exit(); });
+ svc.on('stop', function () { console.log('MeshCentral service stopped.'); if (obj.args.stop) { process.exit(); } if (obj.args.restart) { console.log('Holding 5 seconds...'); setTimeout(function () { svc.start(); }, 5000); } });
+ svc.on('alreadyinstalled', function () { console.log('MeshCentral service already installed.'); process.exit(); });
+ svc.on('invalidinstallation', function () { console.log('Invalid MeshCentral service installation.'); process.exit(); });
+
+ if (obj.args.xinstall == true) { try { svc.install(); } catch (ex) { logException(ex); } }
+ if (obj.args.stop == true || obj.args.restart == true) { try { svc.stop(); } catch (ex) { logException(ex); } }
+ if (obj.args.start == true) { try { svc.start(); } catch (ex) { logException(ex); } }
+ if (obj.args.xuninstall == true) { try { svc.uninstall(); } catch (ex) { logException(ex); } }
+ return;
+ }
+
+ // Windows service install using the external winservice.js
+ if (obj.args.install == true) {
+ console.log('Installing MeshCentral as Windows Service...');
+ if (obj.fs.existsSync(servicepath) == false) { try { obj.fs.mkdirSync(servicepath); } catch (ex) { console.log('ERROR: Unable to create WinService folder: ' + ex); process.exit(); return; } }
+ try { obj.fs.createReadStream(obj.path.join(__dirname, 'winservice.js')).pipe(obj.fs.createWriteStream(obj.path.join(servicepath, 'winservice.js'))); } catch (ex) { console.log('ERROR: Unable to copy winservice.js: ' + ex); process.exit(); return; }
+ require('child_process').exec('node winservice.js --install', { maxBuffer: 512000, timeout: 120000, cwd: servicepath }, function (error, stdout, stderr) {
+ if ((error != null) && (error != '')) { console.log('ERROR: Unable to install MeshCentral as a service: ' + error); process.exit(); return; }
+ console.log(stdout);
+ });
+ return;
+ } else if (obj.args.uninstall == true) {
+ console.log('Uninstalling MeshCentral Windows Service...');
+ if (obj.fs.existsSync(servicepath) == true) {
+ require('child_process').exec('node winservice.js --uninstall', { maxBuffer: 512000, timeout: 120000, cwd: servicepath }, function (error, stdout, stderr) {
+ if ((error != null) && (error != '')) { console.log('ERROR: Unable to uninstall MeshCentral service: ' + error); process.exit(); return; }
+ console.log(stdout);
+ try { obj.fs.unlinkSync(obj.path.join(servicepath, 'winservice.js')); } catch (ex) { }
+ try { obj.fs.rmdirSync(servicepath); } catch (ex) { }
+ });
+ } else if (obj.fs.existsSync(obj.path.join(__dirname, '../WinService')) == true) {
+ require('child_process').exec('node winservice.js --uninstall', { maxBuffer: 512000, timeout: 120000, cwd: obj.path.join(__dirname, '../WinService') }, function (error, stdout, stderr) {
+ if ((error != null) && (error != '')) { console.log('ERROR: Unable to uninstall MeshCentral service: ' + error); process.exit(); return; }
+ console.log(stdout);
+ try { obj.fs.unlinkSync(obj.path.join(__dirname, '../WinService/winservice.js')); } catch (ex) { }
+ try { obj.fs.rmdirSync(obj.path.join(__dirname, '../WinService')); } catch (ex) { }
+ });
+ } else {
+ require('child_process').exec('node winservice.js --uninstall', { maxBuffer: 512000, timeout: 120000, cwd: __dirname }, function (error, stdout, stderr) {
+ if ((error != null) && (error != '')) { console.log('ERROR: Unable to uninstall MeshCentral service: ' + error); process.exit(); return; }
+ console.log(stdout);
+ });
+ }
+ return;
+ }
+ }
+
+ // If "--launch" is in the arguments, launch now
+ if (obj.args.launch) {
+ if (obj.args.vault) { obj.StartVault(); } else { obj.StartEx(); }
+ } else {
+ // if "--launch" is not specified, launch the server as a child process.
+ const startArgs = [];
+ for (i in process.argv) {
+ if (i > 0) {
+ const arg = process.argv[i];
+ if ((arg.length > 0) && ((arg.indexOf(' ') >= 0) || (arg.indexOf('&') >= 0))) { startArgs.push(arg); } else { startArgs.push(arg); }
+ }
+ }
+ startArgs.push('--launch', process.pid);
+ obj.launchChildServer(startArgs);
+ }
+ };
+
+ // Launch MeshCentral as a child server and monitor it.
+ obj.launchChildServer = function (startArgs) {
+ const child_process = require('child_process');
+ const isInspectorAttached = (()=> { try { return require('node:inspector').url() !== undefined; } catch (_) { return false; } }).call();
+ const logFromChildProcess = isInspectorAttached ? () => {} : console.log.bind(console);
+ try { if (process.traceDeprecation === true) { startArgs.unshift('--trace-deprecation'); } } catch (ex) { }
+ try { if (process.traceProcessWarnings === true) { startArgs.unshift('--trace-warnings'); } } catch (ex) { }
+ if (startArgs[0] != "--disable-proto=delete") startArgs.unshift("--disable-proto=delete")
+ childProcess = child_process.execFile(process.argv[0], startArgs, { maxBuffer: Infinity, cwd: obj.parentpath }, function (error, stdout, stderr) {
+ if (childProcess.xrestart == 1) {
+ setTimeout(function () { obj.launchChildServer(startArgs); }, 500); // This is an expected restart.
+ } else if (childProcess.xrestart == 2) {
+ console.log('Expected exit...');
+ process.exit(); // User CTRL-C exit.
+ } else if (childProcess.xrestart == 3) {
+ // Server self-update exit
+ var version = '';
+ if (typeof obj.args.selfupdate == 'string') { version = '@' + obj.args.selfupdate; }
+ else if (typeof obj.args.specificupdate == 'string') { version = '@' + obj.args.specificupdate; delete obj.args.specificupdate; }
+ const child_process = require('child_process');
+ const npmpath = ((typeof obj.args.npmpath == 'string') ? obj.args.npmpath : 'npm');
+ const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
+ const env = Object.assign({}, process.env); // Shallow clone
+ if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
+ // always use --save-exact - https://stackoverflow.com/a/64507176/1210734
+ const xxprocess = child_process.exec(npmpath + ' install --save-exact --no-audit meshcentral' + version + npmproxy, { maxBuffer: Infinity, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) {
+ if ((error != null) && (error != '')) { console.log('Update failed: ' + error); }
+ });
+ xxprocess.data = '';
+ xxprocess.stdout.on('data', function (data) { xxprocess.data += data; });
+ xxprocess.stderr.on('data', function (data) { xxprocess.data += data; });
+ xxprocess.on('close', function (code) {
+ if (code == 0) { console.log('Update completed...'); }
+
+ // Run the server updated script if present
+ if (typeof obj.config.settings.runonserverupdated == 'string') {
+ const child_process = require('child_process');
+ var parentpath = __dirname;
+ if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) { parentpath = require('path').join(__dirname, '../..'); }
+ child_process.exec(obj.config.settings.runonserverupdated + ' ' + getCurrentVersion(), { maxBuffer: 512000, timeout: 120000, cwd: parentpath }, function (error, stdout, stderr) { });
+ }
+
+ if (obj.args.cleannpmcacheonupdate === true) {
+ // Perform NPM cache clean
+ console.log('Cleaning NPM cache...');
+ const xxxprocess = child_process.exec(npmpath + ' cache clean --force', { maxBuffer: Infinity, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { });
+ xxxprocess.on('close', function (code) { setTimeout(function () { obj.launchChildServer(startArgs); }, 1000); });
+ } else {
+ // Run the updated server
+ setTimeout(function () { obj.launchChildServer(startArgs); }, 1000);
+ }
+ });
+ } else {
+ if (error != null) {
+ // This is an un-expected restart
+ console.log(error);
+ console.log('ERROR: MeshCentral failed with critical error, check mesherrors.txt. Restarting in 5 seconds...');
+ setTimeout(function () { obj.launchChildServer(startArgs); }, 5000);
+
+ // Run the server error script if present
+ if (typeof obj.config.settings.runonservererror == 'string') {
+ const child_process = require('child_process');
+ var parentpath = __dirname;
+ if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) { parentpath = require('path').join(__dirname, '../..'); }
+ child_process.exec(obj.config.settings.runonservererror + ' ' + getCurrentVersion(), { maxBuffer: 512000, timeout: 120000, cwd: parentpath }, function (error, stdout, stderr) { });
+ }
+ }
+ }
+ });
+ childProcess.stdout.on('data', function (data) {
+ if (data[data.length - 1] == '\n') { data = data.substring(0, data.length - 1); }
+ if (data.indexOf('Updating settings folder...') >= 0) { childProcess.xrestart = 1; }
+ else if (data.indexOf('Updating server certificates...') >= 0) { childProcess.xrestart = 1; }
+ else if (data.indexOf('Server Ctrl-C exit...') >= 0) { childProcess.xrestart = 2; }
+ else if (data.indexOf('Starting self upgrade...') >= 0) { childProcess.xrestart = 3; }
+ else if (data.indexOf('Server restart...') >= 0) { childProcess.xrestart = 1; }
+ else if (data.indexOf('Starting self upgrade to: ') >= 0) { obj.args.specificupdate = data.substring(26).split('\r')[0].split('\n')[0]; childProcess.xrestart = 3; }
+ var datastr = data;
+ while (datastr.endsWith('\r') || datastr.endsWith('\n')) { datastr = datastr.substring(0, datastr.length - 1); }
+ logFromChildProcess(datastr);
+ });
+ childProcess.stderr.on('data', function (data) {
+ var datastr = data;
+ while (datastr.endsWith('\r') || datastr.endsWith('\n')) { datastr = datastr.substring(0, datastr.length - 1); }
+ logFromChildProcess('ERR: ' + datastr);
+ if (data.startsWith('le.challenges[tls-sni-01].loopback')) { return; } // Ignore this error output from GreenLock
+ if (data[data.length - 1] == '\n') { data = data.substring(0, data.length - 1); }
+ obj.logError(data);
+ });
+ childProcess.on('close', function (code) { if ((code != 0) && (code != 123)) { /* console.log("Exited with code " + code); */ } });
+ };
+
+ obj.logError = function (err) {
+ try {
+ var errlogpath = null;
+ if (typeof obj.args.mesherrorlogpath == 'string') { errlogpath = obj.path.join(obj.args.mesherrorlogpath, 'mesherrors.txt'); } else { errlogpath = obj.getConfigFilePath('mesherrors.txt'); }
+ obj.fs.appendFileSync(errlogpath, '-------- ' + new Date().toLocaleString() + ' ---- ' + getCurrentVersion() + ' --------\r\n\r\n' + err + '\r\n\r\n\r\n');
+ } catch (ex) { console.log('ERROR: Unable to write to mesherrors.txt.'); }
+ };
+
+ // Get current and latest MeshCentral server versions using NPM
+ obj.getLatestServerVersion = function (callback) {
+ if (callback == null) return;
+ try {
+ if (typeof obj.args.selfupdate == 'string') { callback(getCurrentVersion(), obj.args.selfupdate); return; } // If we are targetting a specific version, return that one as current.
+ const child_process = require('child_process');
+ const npmpath = ((typeof obj.args.npmpath == 'string') ? obj.args.npmpath : 'npm');
+ const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
+ const env = Object.assign({}, process.env); // Shallow clone
+ if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
+ const xxprocess = child_process.exec(npmpath + npmproxy + ' view meshcentral dist-tags.latest', { maxBuffer: 512000, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { });
+ xxprocess.data = '';
+ xxprocess.stdout.on('data', function (data) { xxprocess.data += data; });
+ xxprocess.stderr.on('data', function (data) { });
+ xxprocess.on('close', function (code) {
+ var latestVer = null;
+ if (code == 0) { try { latestVer = xxprocess.data.split(' ').join('').split('\r').join('').split('\n').join(''); } catch (ex) { } }
+ callback(getCurrentVersion(), latestVer);
+ });
+ } catch (ex) { callback(getCurrentVersion(), null, ex); } // If the system is running out of memory, an exception here can easily happen.
+ };
+
+ // Get current version and all MeshCentral server tags using NPM
+ obj.getServerTags = function (callback) {
+ if (callback == null) return;
+ try {
+ if (typeof obj.args.selfupdate == 'string') { callback({ current: getCurrentVersion(), latest: obj.args.selfupdate }); return; } // If we are targetting a specific version, return that one as current.
+ const child_process = require('child_process');
+ const npmpath = ((typeof obj.args.npmpath == 'string') ? obj.args.npmpath : 'npm');
+ const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
+ const env = Object.assign({}, process.env); // Shallow clone
+ if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
+ const xxprocess = child_process.exec(npmpath + npmproxy + ' dist-tag ls meshcentral', { maxBuffer: 512000, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { });
+ xxprocess.data = '';
+ xxprocess.stdout.on('data', function (data) { xxprocess.data += data; });
+ xxprocess.stderr.on('data', function (data) { });
+ xxprocess.on('close', function (code) {
+ var tags = { current: getCurrentVersion() };
+ if (code == 0) {
+ try {
+ var lines = xxprocess.data.split('\r\n').join('\n').split('\n');
+ for (var i in lines) { var s = lines[i].split(': '); if ((s.length == 2) && (obj.args.npmtag == null) || (obj.args.npmtag == s[0])) { tags[s[0]] = s[1]; } }
+ } catch (ex) { }
+ }
+ callback(tags);
+ });
+ } catch (ex) { callback({ current: getCurrentVersion() }, ex); } // If the system is running out of memory, an exception here can easily happen.
+ };
+
+ // Use NPM to get list of versions
+ obj.getServerVersions = function (callback) {
+ try {
+ const child_process = require('child_process');
+ const npmpath = ((typeof obj.args.npmpath == 'string') ? obj.args.npmpath : 'npm');
+ const npmproxy = ((typeof obj.args.npmproxy == 'string') ? (' --proxy ' + obj.args.npmproxy) : '');
+ const env = Object.assign({}, process.env); // Shallow clone
+ if (typeof obj.args.npmproxy == 'string') { env['HTTP_PROXY'] = env['HTTPS_PROXY'] = env['http_proxy'] = env['https_proxy'] = obj.args.npmproxy; }
+ const xxprocess = child_process.exec(npmpath + npmproxy + ' view meshcentral versions --json', { maxBuffer: 512000, cwd: obj.parentpath, env: env }, function (error, stdout, stderr) { });
+ xxprocess.data = '';
+ xxprocess.stdout.on('data', function (data) { xxprocess.data += data; });
+ xxprocess.stderr.on('data', function (data) { });
+ xxprocess.on('close', function (code) {
+ (code == 0) ? callback(xxprocess.data) : callback('{}');
+ });
+ } catch (ex) { callback('{}'); }
+ };
+
+ // Initiate server self-update
+ obj.performServerUpdate = function (version) {
+ if (obj.serverSelfWriteAllowed != true) return false;
+ if ((version == null) || (version == '') || (typeof version != 'string')) { console.log('Starting self upgrade...'); } else { console.log('Starting self upgrade to: ' + version); }
+ process.exit(200);
+ return true;
+ };
+
+ // Initiate server self-update
+ obj.performServerCertUpdate = function () { console.log('Updating server certificates...'); process.exit(200); };
+
+ // Start by loading configuration from Vault
+ obj.StartVault = function () {
+ // Check that the configuration can only be loaded from one place
+ if ((obj.args.vault != null) && (obj.args.loadconfigfromdb != null)) { console.log("Can't load configuration from both database and Vault."); process.exit(); return; }
+
+ // Fix arguments if needed
+ if (typeof obj.args.vault == 'string') {
+ obj.args.vault = { endpoint: obj.args.vault };
+ if (typeof obj.args.token == 'string') { obj.args.vault.token = obj.args.token; }
+ if (typeof obj.args.unsealkey == 'string') { obj.args.vault.unsealkey = obj.args.unsealkey; }
+ if (typeof obj.args.name == 'string') { obj.args.vault.name = obj.args.name; }
+ }
+
+ // Load configuration for HashiCorp's Vault if needed
+ if (obj.args.vault) {
+ if (obj.args.vault.endpoint == null) { console.log('Missing Vault endpoint.'); process.exit(); return; }
+ if (obj.args.vault.token == null) { console.log('Missing Vault token.'); process.exit(); return; }
+ if (obj.args.vault.unsealkey == null) { console.log('Missing Vault unsealkey.'); process.exit(); return; }
+ if (obj.args.vault.name == null) { obj.args.vault.name = 'meshcentral'; }
+
+ // Get new instance of the client
+ const vault = require("node-vault")({ endpoint: obj.args.vault.endpoint, token: obj.args.vault.token });
+ vault.unseal({ key: obj.args.vault.unsealkey })
+ .then(function () {
+ if (obj.args.vaultdeleteconfigfiles) {
+ vault.delete('secret/data/' + obj.args.vault.name)
+ .then(function (r) { console.log('Done.'); process.exit(); })
+ .catch(function (x) { console.log(x); process.exit(); });
+ } else if (obj.args.vaultpushconfigfiles) {
+ // Push configuration files into Vault
+ if ((obj.args.vaultpushconfigfiles == '*') || (obj.args.vaultpushconfigfiles === true)) { obj.args.vaultpushconfigfiles = obj.datapath; }
+ obj.fs.readdir(obj.args.vaultpushconfigfiles, function (err, files) {
+ if (err != null) { console.log('ERROR: Unable to read from folder ' + obj.args.vaultpushconfigfiles); process.exit(); return; }
+ var configFound = false;
+ for (var i in files) { if (files[i] == 'config.json') { configFound = true; } }
+ if (configFound == false) { console.log('ERROR: No config.json in folder ' + obj.args.vaultpushconfigfiles); process.exit(); return; }
+ var configFiles = {};
+ for (var i in files) {
+ const file = files[i];
+ if ((file == 'config.json') || file.endsWith('.key') || file.endsWith('.crt') || (file == 'terms.txt') || file.endsWith('.jpg') || file.endsWith('.png')) {
+ const path = obj.path.join(obj.args.vaultpushconfigfiles, files[i]), binary = Buffer.from(obj.fs.readFileSync(path, { encoding: 'binary' }), 'binary');
+ console.log('Pushing ' + file + ', ' + binary.length + ' bytes.');
+ if (file.endsWith('.json') || file.endsWith('.key') || file.endsWith('.crt')) { configFiles[file] = binary.toString(); } else { configFiles[file] = binary.toString('base64'); }
+ }
+ }
+ vault.write('secret/data/' + obj.args.vault.name, { "data": configFiles })
+ .then(function (r) { console.log('Done.'); process.exit(); })
+ .catch(function (x) { console.log(x); process.exit(); });
+ });
+ } else {
+ // Read configuration files from Vault
+ vault.read('secret/data/' + obj.args.vault.name)
+ .then(function (r) {
+ if ((r == null) || (r.data == null) || (r.data.data == null)) { console.log('Unable to read configuration from Vault.'); process.exit(); return; }
+ var configFiles = obj.configurationFiles = r.data.data;
+
+ // Decode Base64 when needed
+ for (var file in configFiles) { if (!file.endsWith('.json') && !file.endsWith('.key') && !file.endsWith('.crt')) { configFiles[file] = Buffer.from(configFiles[file], 'base64'); } }
+
+ // Save all of the files
+ if (obj.args.vaultpullconfigfiles) {
+ for (var i in configFiles) {
+ var fullFileName = obj.path.join(obj.args.vaultpullconfigfiles, i);
+ try { obj.fs.writeFileSync(fullFileName, configFiles[i]); } catch (ex) { console.log('Unable to write to ' + fullFileName); process.exit(); return; }
+ console.log('Pulling ' + i + ', ' + configFiles[i].length + ' bytes.');
+ }
+ console.log('Done.');
+ process.exit();
+ }
+
+ // Parse the new configuration file
+ var config2 = null;
+ try { config2 = JSON.parse(configFiles['config.json']); } catch (ex) { console.log('Error, unable to parse config.json from Vault.'); process.exit(); return; }
+
+ // Set the command line arguments to the config file if they are not present
+ if (!config2.settings) { config2.settings = {}; }
+ for (var i in args) { config2.settings[i] = args[i]; }
+ obj.args = args = config2.settings;
+
+ // Lower case all keys in the config file
+ obj.common.objKeysToLower(config2, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'telegram/proxy']);
+
+ // Grad some of the values from the original config.json file if present.
+ if ((config.settings.vault != null) && (config2.settings != null)) { config2.settings.vault = config.settings.vault; }
+
+ // We got a new config.json from the database, let's use it.
+ config = obj.config = config2;
+ obj.StartEx();
+ })
+ .catch(function (x) { console.log(x); process.exit(); });
+ }
+ }).catch(function (x) { console.log(x); process.exit(); });
+ return;
+ }
+ }
+
+ // Look for easy command line instructions and do them here.
+ obj.StartEx = async function () {
+ var i;
+ //var wincmd = require('node-windows');
+ //wincmd.list(function (svc) { console.log(svc); }, true);
+
+ // Setup syslog support. Not supported on Windows.
+ if ((require('os').platform() != 'win32') && ((config.settings.syslog != null) || (config.settings.syslogjson != null) || (config.settings.syslogauth != null))) {
+ if (config.settings.syslog === true) { config.settings.syslog = 'meshcentral'; }
+ if (config.settings.syslogjson === true) { config.settings.syslogjson = 'meshcentral-json'; }
+ if (config.settings.syslogauth === true) { config.settings.syslogauth = 'meshcentral-auth'; }
+ if (typeof config.settings.syslog == 'string') {
+ obj.syslog = require('modern-syslog');
+ console.log('Starting ' + config.settings.syslog + ' syslog.');
+ obj.syslog.init(config.settings.syslog, obj.syslog.LOG_PID | obj.syslog.LOG_ODELAY, obj.syslog.LOG_LOCAL0);
+ obj.syslog.log(obj.syslog.LOG_INFO, "MeshCentral v" + getCurrentVersion() + " Server Start");
+ }
+ if (typeof config.settings.syslogjson == 'string') {
+ obj.syslogjson = require('modern-syslog');
+ console.log('Starting ' + config.settings.syslogjson + ' JSON syslog.');
+ obj.syslogjson.init(config.settings.syslogjson, obj.syslogjson.LOG_PID | obj.syslogjson.LOG_ODELAY, obj.syslogjson.LOG_LOCAL0);
+ obj.syslogjson.log(obj.syslogjson.LOG_INFO, "MeshCentral v" + getCurrentVersion() + " Server Start");
+ }
+ if (typeof config.settings.syslogauth == 'string') {
+ obj.syslogauth = require('modern-syslog');
+ console.log('Starting ' + config.settings.syslogauth + ' auth syslog.');
+ obj.syslogauth.init(config.settings.syslogauth, obj.syslogauth.LOG_PID | obj.syslogauth.LOG_ODELAY, obj.syslogauth.LOG_LOCAL0);
+ obj.syslogauth.log(obj.syslogauth.LOG_INFO, "MeshCentral v" + getCurrentVersion() + " Server Start");
+ }
+ }
+ // Setup TCP syslog support, this works on all OS's.
+ if (config.settings.syslogtcp != null) {
+ const syslog = require('syslog');
+ if (config.settings.syslogtcp === true) {
+ obj.syslogtcp = syslog.createClient(514, 'localhost');
+ } else {
+ const sp = config.settings.syslogtcp.split(':');
+ obj.syslogtcp = syslog.createClient(parseInt(sp[1]), sp[0]);
+ }
+ obj.syslogtcp.log("MeshCentral v" + getCurrentVersion() + " Server Start", obj.syslogtcp.LOG_INFO);
+ }
+
+ // Check top level configuration for any unrecognized values
+ if (config) { for (var i in config) { if ((typeof i == 'string') && (i.length > 0) && (i[0] != '_') && (['settings', 'domaindefaults', 'domains', 'configfiles', 'smtp', 'letsencrypt', 'peers', 'sms', 'messaging', 'sendgrid', 'sendmail', 'firebase', 'firebaserelay', '$schema'].indexOf(i) == -1)) { addServerWarning('Unrecognized configuration option \"' + i + '\".', 3, [i]); } } }
+
+ // Read IP lists from files if applicable
+ config.settings.userallowedip = obj.args.userallowedip = readIpListFromFile(obj.args.userallowedip);
+ config.settings.userblockedip = obj.args.userblockedip = readIpListFromFile(obj.args.userblockedip);
+ config.settings.agentallowedip = obj.args.agentallowedip = readIpListFromFile(obj.args.agentallowedip);
+ config.settings.agentblockedip = obj.args.agentblockedip = readIpListFromFile(obj.args.agentblockedip);
+ config.settings.swarmallowedip = obj.args.swarmallowedip = readIpListFromFile(obj.args.swarmallowedip);
+
+ // Check IP lists and ranges
+ if (typeof obj.args.userallowedip == 'string') { if (obj.args.userallowedip == '') { config.settings.userallowedip = obj.args.userallowedip = null; } else { config.settings.userallowedip = obj.args.userallowedip = obj.args.userallowedip.split(' ').join('').split(','); } }
+ if (typeof obj.args.userblockedip == 'string') { if (obj.args.userblockedip == '') { config.settings.userblockedip = obj.args.userblockedip = null; } else { config.settings.userblockedip = obj.args.userblockedip = obj.args.userblockedip.split(' ').join('').split(','); } }
+ if (typeof obj.args.agentallowedip == 'string') { if (obj.args.agentallowedip == '') { config.settings.agentallowedip = obj.args.agentallowedip = null; } else { config.settings.agentallowedip = obj.args.agentallowedip = obj.args.agentallowedip.split(' ').join('').split(','); } }
+ if (typeof obj.args.agentblockedip == 'string') { if (obj.args.agentblockedip == '') { config.settings.agentblockedip = obj.args.agentblockedip = null; } else { config.settings.agentblockedip = obj.args.agentblockedip = obj.args.agentblockedip.split(' ').join('').split(','); } }
+ if (typeof obj.args.swarmallowedip == 'string') { if (obj.args.swarmallowedip == '') { obj.args.swarmallowedip = null; } else { obj.args.swarmallowedip = obj.args.swarmallowedip.split(' ').join('').split(','); } }
+ if ((typeof obj.args.agentupdateblocksize == 'number') && (obj.args.agentupdateblocksize >= 1024) && (obj.args.agentupdateblocksize <= 65531)) { obj.agentUpdateBlockSize = obj.args.agentupdateblocksize; }
+ if (typeof obj.args.trustedproxy == 'string') { obj.args.trustedproxy = obj.args.trustedproxy.split(' ').join('').split(','); }
+ if (typeof obj.args.tlsoffload == 'string') { obj.args.tlsoffload = obj.args.tlsoffload.split(' ').join('').split(','); }
+
+ // Check IP lists and ranges and if DNS return IP addresses
+ config.settings.userallowedip = await resolveDomainsToIps(config.settings.userallowedip);
+ config.settings.userblockedip = await resolveDomainsToIps(config.settings.userblockedip);
+ config.settings.agentallowedip = await resolveDomainsToIps(config.settings.agentallowedip);
+ config.settings.agentblockedip = await resolveDomainsToIps(config.settings.agentblockedip);
+ config.settings.swarmallowedip = await resolveDomainsToIps(config.settings.swarmallowedip);
+
+ // Check the "cookieIpCheck" value
+ if ((obj.args.cookieipcheck === false) || (obj.args.cookieipcheck == 'none')) { obj.args.cookieipcheck = 'none'; }
+ else if ((typeof obj.args.cookieipcheck != 'string') || (obj.args.cookieipcheck.toLowerCase() != 'strict')) { obj.args.cookieipcheck = 'lax'; }
+ else { obj.args.cookieipcheck = 'strict'; }
+
+ // Check the "cookieSameSite" value
+ if (typeof obj.args.cookiesamesite != 'string') { delete obj.args.cookiesamesite; }
+ else if (['none', 'lax', 'strict'].indexOf(obj.args.cookiesamesite.toLowerCase()) == -1) { delete obj.args.cookiesamesite; } else { obj.args.cookiesamesite = obj.args.cookiesamesite.toLowerCase(); }
+
+ // Check if WebSocket compression is supported. It's known to be broken in NodeJS v11.11 to v12.15, and v13.2
+ const verSplit = process.version.substring(1).split('.');
+ const ver = parseInt(verSplit[0]) + (parseInt(verSplit[1]) / 100);
+ if (((ver >= 11.11) && (ver <= 12.15)) || (ver == 13.2)) {
+ if ((obj.args.wscompression === true) || (obj.args.agentwscompression === true)) { addServerWarning('WebSocket compression is disabled, this feature is broken in NodeJS v11.11 to v12.15 and v13.2', 4); }
+ obj.args.wscompression = obj.args.agentwscompression = false;
+ obj.config.settings.wscompression = obj.config.settings.agentwscompression = false;
+ }
+
+ // Local console tracing
+ if (typeof obj.args.debug == 'string') { obj.debugSources = obj.args.debug.toLowerCase().split(','); }
+ else if (typeof obj.args.debug == 'object') { obj.debugSources = obj.args.debug; }
+ else if (obj.args.debug === true) { obj.debugSources = '*'; }
+
+ require('./db.js').CreateDB(obj,
+ function (db) {
+ obj.db = db;
+ obj.db.SetupDatabase(function (dbversion) {
+ // See if any database operations needs to be completed
+ if (obj.args.deletedomain) { obj.db.DeleteDomain(obj.args.deletedomain, function () { console.log('Deleted domain ' + obj.args.deletedomain + '.'); process.exit(); }); return; }
+ if (obj.args.deletedefaultdomain) { obj.db.DeleteDomain('', function () { console.log('Deleted default domain.'); process.exit(); }); return; }
+ if (obj.args.showall) { obj.db.GetAll(function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
+ if (obj.args.showusers) { obj.db.GetAllType('user', function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
+ if (obj.args.showitem) { obj.db.Get(obj.args.showitem, function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
+ if (obj.args.listuserids) { obj.db.GetAllType('user', function (err, docs) { for (var i in docs) { console.log(docs[i]._id); } process.exit(); }); return; }
+ if (obj.args.showusergroups) { obj.db.GetAllType('ugrp', function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
+ if (obj.args.shownodes) { obj.db.GetAllType('node', function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
+ if (obj.args.showallmeshes) { obj.db.GetAllType('mesh', function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
+ if (obj.args.showmeshes) { obj.db.GetAllType('mesh', function (err, docs) { var x = []; for (var i in docs) { if (docs[i].deleted == null) { x.push(docs[i]); } } console.log(JSON.stringify(x, null, 2)); process.exit(); }); return; }
+ if (obj.args.showevents) { obj.db.GetAllEvents(function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
+ if (obj.args.showsmbios) { obj.db.GetAllSMBIOS(function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
+ if (obj.args.showpower) { obj.db.getAllPower(function (err, docs) { console.log(JSON.stringify(docs, null, 2)); process.exit(); }); return; }
+ if (obj.args.clearpower) { obj.db.removeAllPowerEvents(function () { process.exit(); }); return; }
+ if (obj.args.showiplocations) { obj.db.GetAllType('iploc', function (err, docs) { console.log(docs); process.exit(); }); return; }
+ if (obj.args.logintoken) { obj.getLoginToken(obj.args.logintoken, function (r) { console.log(r); process.exit(); }); return; }
+ if (obj.args.logintokenkey) { obj.showLoginTokenKey(function (r) { console.log(r); process.exit(); }); return; }
+ if (obj.args.recordencryptionrecode) { obj.db.performRecordEncryptionRecode(function (count) { console.log('Re-encoded ' + count + ' record(s).'); process.exit(); }); return; }
+ if (obj.args.dbstats) { obj.db.getDbStats(function (stats) { console.log(stats); process.exit(); }); return; }
+ if (obj.args.createaccount) { // Create a new user account
+ if ((typeof obj.args.createaccount != 'string') || ((obj.args.pass == null) && (obj.args.hashpass == null)) || (obj.args.pass == '') || (obj.args.hashpass == '') || (obj.args.createaccount.indexOf(' ') >= 0)) { console.log("Usage: --createaccount [userid] --pass [password] --domain (domain) --email (email) --name (name)."); process.exit(); return; }
+ var userid = 'user/' + (obj.args.domain ? obj.args.domain : '') + '/' + obj.args.createaccount.toLowerCase(), domainid = obj.args.domain ? obj.args.domain : '';
+ if (obj.args.createaccount.startsWith('user/')) { userid = obj.args.createaccount; domainid = obj.args.createaccount.split('/')[1]; }
+ if (userid.split('/').length != 3) { console.log("Invalid userid."); process.exit(); return; }
+ obj.db.Get(userid, function (err, docs) {
+ if (err != null) { console.log("Database error: " + err); process.exit(); return; }
+ if ((docs != null) && (docs.length != 0)) { console.log('User already exists.'); process.exit(); return; }
+ if ((domainid != '') && ((config.domains == null) || (config.domains[domainid] == null))) { console.log("Invalid domain."); process.exit(); return; }
+ const user = { _id: userid, type: 'user', name: (typeof obj.args.name == 'string') ? obj.args.name : (userid.split('/')[2]), domain: domainid, creation: Math.floor(Date.now() / 1000), links: {} };
+ if (typeof obj.args.email == 'string') { user.email = obj.args.email; user.emailVerified = true; }
+ if (obj.args.hashpass) {
+ // Create an account using a pre-hashed password. Use --hashpassword to pre-hash a password.
+ var hashpasssplit = obj.args.hashpass.split(',');
+ if (hashpasssplit.length != 2) { console.log("Invalid hashed password."); process.exit(); return; }
+ user.salt = hashpasssplit[0];
+ user.hash = hashpasssplit[1];
+ obj.db.Set(user, function () { console.log("Done. This command will only work if MeshCentral is stopped."); process.exit(); return; });
+ } else {
+ // Hash the password and create the account.
+ require('./pass').hash(obj.args.pass, function (err, salt, hash, tag) { if (err) { console.log("Unable create account password: " + err); process.exit(); return; } user.salt = salt; user.hash = hash; obj.db.Set(user, function () { console.log("Done."); process.exit(); return; }); }, 0);
+ }
+ });
+ return;
+ }
+ if (obj.args.resetaccount) { // Unlock a user account, set a new password and remove 2FA
+ if ((typeof obj.args.resetaccount != 'string') || (obj.args.resetaccount.indexOf(' ') >= 0)) { console.log("Usage: --resetaccount [userid] --domain (domain) --pass [password]."); process.exit(); return; }
+ var userid = 'user/' + (obj.args.domain ? obj.args.domain : '') + '/' + obj.args.resetaccount.toLowerCase();
+ if (obj.args.resetaccount.startsWith('user/')) { userid = obj.args.resetaccount; }
+ if (userid.split('/').length != 3) { console.log("Invalid userid."); process.exit(); return; }
+ obj.db.Get(userid, function (err, docs) {
+ if (err != null) { console.log("Database error: " + err); process.exit(); return; }
+ if ((docs == null) || (docs.length == 0)) { console.log("Unknown userid, usage: --resetaccount [userid] --domain (domain) --pass [password]."); process.exit(); return; }
+ const user = docs[0]; if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { user.siteadmin -= 32; } // Unlock the account.
+ delete user.phone; delete user.otpekey; delete user.otpsecret; delete user.otpkeys; delete user.otphkeys; delete user.otpdev; delete user.otpsms; delete user.otpmsg; user.otpduo; // Disable 2FA
+ delete user.msghandle; // Disable users 2fa messaging too
+ var config = getConfig(false);
+ if (config.domains[user.domain].auth || config.domains[user.domain].authstrategies) {
+ console.log('This users domain has external authentication methods enabled so the password will not be changed if you set one')
+ obj.db.Set(user, function () { console.log("Done."); process.exit(); return; });
+ } else {
+ if (obj.args.hashpass && (typeof obj.args.hashpass == 'string')) {
+ // Reset an account using a pre-hashed password. Use --hashpassword to pre-hash a password.
+ var hashpasssplit = obj.args.hashpass.split(',');
+ if (hashpasssplit.length != 2) { console.log("Invalid hashed password."); process.exit(); return; }
+ user.salt = hashpasssplit[0];
+ user.hash = hashpasssplit[1];
+ obj.db.Set(user, function () { console.log("Done. This command will only work if MeshCentral is stopped."); process.exit(); return; });
+ } else if (obj.args.pass && (typeof obj.args.pass == 'string')) {
+ // Hash the password and reset the account.
+ require('./pass').hash(String(obj.args.pass), user.salt, function (err, hash, tag) {
+ if (err) { console.log("Unable to reset password: " + err); process.exit(); return; }
+ user.hash = hash;
+ obj.db.Set(user, function () { console.log("Done."); process.exit(); return; });
+ }, 0);
+ } else {
+ console.log('Not setting a users password');
+ obj.db.Set(user, function () { console.log("Done."); process.exit(); return; });
+ }
+ }
+ });
+ return;
+ }
+ if (obj.args.adminaccount) { // Set a user account to server administrator
+ if ((typeof obj.args.adminaccount != 'string') || (obj.args.adminaccount.indexOf(' ') >= 0)) { console.log("Invalid userid, usage: --adminaccount [username] --domain (domain)"); process.exit(); return; }
+ var userid = 'user/' + (obj.args.domain ? obj.args.domain : '') + '/' + obj.args.adminaccount.toLowerCase();
+ if (obj.args.adminaccount.startsWith('user/')) { userid = obj.args.adminaccount; }
+ if (userid.split('/').length != 3) { console.log("Invalid userid."); process.exit(); return; }
+ obj.db.Get(userid, function (err, docs) {
+ if (err != null) { console.log("Database error: " + err); process.exit(); return; }
+ if ((docs == null) || (docs.length == 0)) { console.log("Unknown userid, usage: --adminaccount [userid] --domain (domain)."); process.exit(); return; }
+ docs[0].siteadmin = 0xFFFFFFFF; // Set user as site administrator
+ obj.db.Set(docs[0], function () { console.log("Done. This command will only work if MeshCentral is stopped."); process.exit(); return; });
+ });
+ return;
+ }
+ if (obj.args.removesubdomain) { // Remove all references to a sub domain from the database
+ if ((typeof obj.args.removesubdomain != 'string') || (obj.args.removesubdomain.indexOf(' ') >= 0)) { console.log("Invalid sub domain, usage: --removesubdomain [domain]"); process.exit(); return; }
+ obj.db.removeDomain(obj.args.removesubdomain, function () { console.log("Done."); process.exit(); return; });
+ return;
+ }
+ if (obj.args.removetestagents) { // Remove all test agents from the database
+ db.GetAllType('node', function (err, docs) {
+ if ((err != null) || (docs.length == 0)) {
+ console.log('Unable to get any nodes from the database');
+ process.exit(0);
+ } else {
+ // Load all users
+ const allusers = {}, removeCount = 0;
+ obj.db.GetAllType('user', function (err, docs) {
+ obj.common.unEscapeAllLinksFieldName(docs);
+ for (i in docs) { allusers[docs[i]._id] = docs[i]; }
+ });
+
+ // Look at all devices
+ for (var i in docs) {
+ if ((docs[i] != null) && (docs[i].agent != null) && (docs[i].agent.id == 23)) {
+ // Remove this test node
+ const node = docs[i];
+
+ // Delete this node including network interface information, events and timeline
+ removeCount++;
+ db.Remove(node._id); // Remove node with that id
+ db.Remove('if' + node._id); // Remove interface information
+ db.Remove('nt' + node._id); // Remove notes
+ db.Remove('lc' + node._id); // Remove last connect time
+ db.Remove('si' + node._id); // Remove system information
+ if (db.RemoveSMBIOS) { db.RemoveSMBIOS(node._id); } // Remove SMBios data
+ db.RemoveAllNodeEvents(node._id); // Remove all events for this node
+ db.removeAllPowerEventsForNode(node._id); // Remove all power events for this node
+ if (typeof node.pmt == 'string') { db.Remove('pmt_' + node.pmt); } // Remove Push Messaging Token
+ db.Get('ra' + node._id, function (err, nodes) {
+ if ((nodes != null) && (nodes.length == 1)) { db.Remove('da' + nodes[0].daid); } // Remove diagnostic agent to real agent link
+ db.Remove('ra' + node._id); // Remove real agent to diagnostic agent link
+ });
+
+ // Remove any user node links
+ if (node.links != null) {
+ for (var i in node.links) {
+ if (i.startsWith('user/')) {
+ var cuser = allusers[i];
+ if ((cuser != null) && (cuser.links != null) && (cuser.links[node._id] != null)) {
+ // Remove the user link & save the user
+ delete cuser.links[node._id];
+ if (Object.keys(cuser.links).length == 0) { delete cuser.links; }
+ db.SetUser(cuser);
+ }
+ }
+ }
+ }
+
+ }
+ }
+ if (removeCount == 0) {
+ console.log("Done, no devices removed.");
+ process.exit(0);
+ } else {
+ console.log("Removed " + removeCount + " device(s), holding 10 seconds...");
+ setTimeout(function () { console.log("Done."); process.exit(0); }, 10000)
+ }
+ }
+ });
+ return;
+ }
+
+ // Import NeDB data into database
+ if (obj.args.nedbtodb) {
+ if (db.databaseType == 1) { console.log("NeDB is current database, can't perform transfer."); process.exit(); return; }
+ console.log("Transfering NeDB data into database...");
+ db.nedbtodb(function (msg) { console.log(msg); process.exit(); })
+ return;
+ }
+
+ // Show a list of all configuration files in the database
+ if (obj.args.dblistconfigfiles) {
+ obj.db.GetAllType('cfile', function (err, docs) {
+ if (err == null) {
+ if (docs.length == 0) {
+ console.log("No files found.");
+ } else {
+ for (var i in docs) {
+ if (typeof obj.args.dblistconfigfiles == 'string') {
+ const data = obj.db.decryptData(obj.args.dblistconfigfiles, docs[i].data);
+ if (data == null) {
+ console.log(docs[i]._id.split('/')[1] + ', ' + Buffer.from(docs[i].data, 'base64').length + ' encrypted bytes - Unable to decrypt.');
+ } else {
+ console.log(docs[i]._id.split('/')[1] + ', ' + data.length + ' bytes, decoded correctly.');
+ }
+ } else {
+ console.log(docs[i]._id.split('/')[1] + ', ' + Buffer.from(docs[i].data, 'base64').length + ' encrypted bytes.');
+ }
+ }
+ }
+ } else { console.log('Unable to read from database.'); } process.exit();
+ });
+ return;
+ }
+
+ // Display the content of a configuration file in the database
+ if (obj.args.dbshowconfigfile) {
+ if (typeof obj.args.configkey != 'string') { console.log("Error, --configkey is required."); process.exit(); return; }
+ obj.db.getConfigFile(obj.args.dbshowconfigfile, function (err, docs) {
+ if (err == null) {
+ if (docs.length == 0) { console.log("File not found."); } else {
+ const data = obj.db.decryptData(obj.args.configkey, docs[0].data);
+ if (data == null) { console.log("Invalid config key."); } else { console.log(data); }
+ }
+ } else { console.log("Unable to read from database."); }
+ process.exit();
+ }); return;
+ }
+
+ // Delete all configuration files from database
+ if (obj.args.dbdeleteconfigfiles) {
+ console.log("Deleting all configuration files from the database..."); obj.db.RemoveAllOfType('cfile', function () { console.log('Done.'); process.exit(); });
+ }
+
+ // Push all relevent files from meshcentral-data into the database
+ if (obj.args.dbpushconfigfiles) {
+ if (typeof obj.args.configkey != 'string') { console.log("Error, --configkey is required."); process.exit(); return; }
+ if ((obj.args.dbpushconfigfiles !== true) && (typeof obj.args.dbpushconfigfiles != 'string')) {
+ console.log("Usage: --dbpulldatafiles (path) This will import files from folder into the database");
+ console.log(" --dbpulldatafiles This will import files from meshcentral-data into the db.");
+ process.exit();
+ } else {
+ if ((obj.args.dbpushconfigfiles == '*') || (obj.args.dbpushconfigfiles === true)) { obj.args.dbpushconfigfiles = obj.datapath; }
+ obj.fs.readdir(obj.args.dbpushconfigfiles, function (err, files) {
+ if (err != null) { console.log('ERROR: Unable to read from folder ' + obj.args.dbpushconfigfiles); process.exit(); return; }
+ var configFound = false;
+ for (var i in files) { if (files[i] == 'config.json') { configFound = true; } }
+ if (configFound == false) { console.log('ERROR: No config.json in folder ' + obj.args.dbpushconfigfiles); process.exit(); return; }
+ obj.db.RemoveAllOfType('cfile', function () {
+ obj.fs.readdir(obj.args.dbpushconfigfiles, function (err, files) {
+ var lockCount = 1
+ for (var i in files) {
+ const file = files[i];
+ if ((file == 'config.json') || file.endsWith('.key') || file.endsWith('.crt') || (file == 'terms.txt') || file.endsWith('.jpg') || file.endsWith('.png')) {
+ const path = obj.path.join(obj.args.dbpushconfigfiles, files[i]), binary = Buffer.from(obj.fs.readFileSync(path, { encoding: 'binary' }), 'binary');
+ console.log('Pushing ' + file + ', ' + binary.length + ' bytes.');
+ lockCount++;
+ if (obj.args.oldencrypt) {
+ obj.db.setConfigFile(file, obj.db.oldEncryptData(obj.args.configkey, binary), function () { if ((--lockCount) == 0) { console.log('Done.'); process.exit(); } });
+ } else {
+ obj.db.setConfigFile(file, obj.db.encryptData(obj.args.configkey, binary), function () { if ((--lockCount) == 0) { console.log('Done.'); process.exit(); } });
+ }
+ }
+ }
+ if (--lockCount == 0) { process.exit(); }
+ });
+ });
+ });
+ }
+ return;
+ }
+
+ // Pull all database files into meshcentral-data
+ if (obj.args.dbpullconfigfiles) {
+ if (typeof obj.args.configkey != 'string') { console.log("Error, --configkey is required."); process.exit(); return; }
+ if (typeof obj.args.dbpullconfigfiles != 'string') {
+ console.log("Usage: --dbpulldatafiles (path)");
+ process.exit();
+ } else {
+ obj.db.GetAllType('cfile', function (err, docs) {
+ if (err == null) {
+ if (docs.length == 0) {
+ console.log("File not found.");
+ } else {
+ for (var i in docs) {
+ const file = docs[i]._id.split('/')[1], binary = obj.db.decryptData(obj.args.configkey, docs[i].data);
+ if (binary == null) {
+ console.log("Invalid config key.");
+ } else {
+ const fullFileName = obj.path.join(obj.args.dbpullconfigfiles, file);
+ try { obj.fs.writeFileSync(fullFileName, binary); } catch (ex) { console.log('Unable to write to ' + fullFileName); process.exit(); return; }
+ console.log('Pulling ' + file + ', ' + binary.length + ' bytes.');
+ }
+ }
+ }
+ } else {
+ console.log("Unable to read from database.");
+ }
+ process.exit();
+ });
+ }
+ return;
+ }
+
+ if (obj.args.dbexport) {
+ // Export the entire database to a JSON file
+ if (obj.args.dbexport == true) { obj.args.dbexport = obj.getConfigFilePath('meshcentral.db.json'); }
+ obj.db.GetAll(function (err, docs) {
+ obj.fs.writeFileSync(obj.args.dbexport, JSON.stringify(docs));
+ console.log('Exported ' + docs.length + ' objects(s) to ' + obj.args.dbexport + '.'); process.exit();
+ });
+ return;
+ }
+ if (obj.args.dbexportmin) {
+ // Export a minimal database to a JSON file. Export only users, meshes and nodes.
+ // This is a useful command to look at the database.
+ if (obj.args.dbexportmin == true) { obj.args.dbexportmin = obj.getConfigFilePath('meshcentral.db.json'); }
+ obj.db.GetAllType({ $in: ['user', 'node', 'mesh'] }, function (err, docs) {
+ obj.fs.writeFileSync(obj.args.dbexportmin, JSON.stringify(docs));
+ console.log('Exported ' + docs.length + ' objects(s) to ' + obj.args.dbexportmin + '.'); process.exit();
+ });
+ return;
+ }
+ if (obj.args.dbimport) {
+ // Import the entire database from a JSON file
+ if (obj.args.dbimport == true) { obj.args.dbimport = obj.getConfigFilePath('meshcentral.db.json'); }
+ var json = null, json2 = '', badCharCount = 0;
+ try { json = obj.fs.readFileSync(obj.args.dbimport, { encoding: 'utf8' }); } catch (ex) { console.log('Invalid JSON file: ' + obj.args.dbimport + ': ' + ex); process.exit(); }
+ for (i = 0; i < json.length; i++) { if (json.charCodeAt(i) >= 32) { json2 += json[i]; } else { var tt = json.charCodeAt(i); if (tt != 10 && tt != 13) { badCharCount++; } } } // Remove all bad chars
+ if (badCharCount > 0) { console.log(badCharCount + ' invalid character(s) where removed.'); }
+ try { json = JSON.parse(json2); } catch (ex) { console.log('Invalid JSON format: ' + obj.args.dbimport + ': ' + e); process.exit(); }
+ if ((json == null) || (typeof json.length != 'number') || (json.length < 1)) { console.log('Invalid JSON format: ' + obj.args.dbimport + '.'); }
+ // Escape MongoDB invalid field chars
+ for (i in json) {
+ const doc = json[i];
+ for (var j in doc) { if (j.indexOf('.') >= 0) { console.log("Invalid field name (" + j + ") in document: " + json[i]); return; } }
+ //if ((json[i].type == 'ifinfo') && (json[i].netif2 != null)) { for (var j in json[i].netif2) { var esc = obj.common.escapeFieldName(j); if (esc !== j) { json[i].netif2[esc] = json[i].netif2[j]; delete json[i].netif2[j]; } } }
+ //if ((json[i].type == 'mesh') && (json[i].links != null)) { for (var j in json[i].links) { var esc = obj.common.escapeFieldName(j); if (esc !== j) { json[i].links[esc] = json[i].links[j]; delete json[i].links[j]; } } }
+ }
+ //for (i in json) { if ((json[i].type == "node") && (json[i].host != null)) { json[i].rname = json[i].host; delete json[i].host; } } // DEBUG: Change host to rname
+ setTimeout(function () { // If the Mongo database is being created for the first time, there is a race condition here. This will get around it.
+ obj.db.RemoveAll(function () {
+ obj.db.InsertMany(json, function (err) {
+ if (err != null) { console.log(err); } else { console.log('Imported ' + json.length + ' objects(s) from ' + obj.args.dbimport + '.'); } process.exit();
+ });
+ });
+ }, 100);
+ return;
+ }
+ /*
+ if (obj.args.dbimport) {
+ // Import the entire database from a very large JSON file
+ obj.db.RemoveAll(function () {
+ if (obj.args.dbimport == true) { obj.args.dbimport = obj.getConfigFilePath('meshcentral.db.json'); }
+ var json = null, json2 = "", badCharCount = 0;
+ const StreamArray = require('stream-json/streamers/StreamArray');
+ const jsonStream = StreamArray.withParser();
+ jsonStream.on('data', function (data) { obj.db.Set(data.value); });
+ jsonStream.on('end', () => { console.log('Done.'); process.exit(); });
+ obj.fs.createReadStream(obj.args.dbimport).pipe(jsonStream.input);
+ });
+ return;
+ }
+ */
+ if (obj.args.dbmerge) {
+ // Import the entire database from a JSON file
+ if (obj.args.dbmerge == true) { obj.args.dbmerge = obj.getConfigFilePath('meshcentral.db.json'); }
+ var json = null, json2 = "", badCharCount = 0;
+ try { json = obj.fs.readFileSync(obj.args.dbmerge, { encoding: 'utf8' }); } catch (ex) { console.log('Invalid JSON file: ' + obj.args.dbmerge + ': ' + ex); process.exit(); }
+ for (i = 0; i < json.length; i++) { if (json.charCodeAt(i) >= 32) { json2 += json[i]; } else { var tt = json.charCodeAt(i); if (tt != 10 && tt != 13) { badCharCount++; } } } // Remove all bad chars
+ if (badCharCount > 0) { console.log(badCharCount + ' invalid character(s) where removed.'); }
+ try { json = JSON.parse(json2); } catch (ex) { console.log('Invalid JSON format: ' + obj.args.dbmerge + ': ' + ex); process.exit(); }
+ if ((json == null) || (typeof json.length != 'number') || (json.length < 1)) { console.log('Invalid JSON format: ' + obj.args.dbimport + '.'); }
+
+ // Get all users from current database
+ obj.db.GetAllType('user', function (err, docs) {
+ const users = {}, usersCount = 0;
+ for (var i in docs) { users[docs[i]._id] = docs[i]; usersCount++; }
+
+ // Fetch all meshes from the database
+ obj.db.GetAllType('mesh', function (err, docs) {
+ obj.common.unEscapeAllLinksFieldName(docs);
+ const meshes = {}, meshesCount = 0;
+ for (var i in docs) { meshes[docs[i]._id] = docs[i]; meshesCount++; }
+ console.log('Loaded ' + usersCount + ' users and ' + meshesCount + ' meshes.');
+ // Look at each object in the import file
+ const objectToAdd = [];
+ for (var i in json) {
+ const newobj = json[i];
+ if (newobj.type == 'user') {
+ // Check if the user already exists
+ var existingUser = users[newobj._id];
+ if (existingUser) {
+ // Merge the links
+ if (typeof newobj.links == 'object') {
+ for (var j in newobj.links) {
+ if ((existingUser.links == null) || (existingUser.links[j] == null)) {
+ if (existingUser.links == null) { existingUser.links = {}; }
+ existingUser.links[j] = newobj.links[j];
+ }
+ }
+ }
+ if (existingUser.name == 'admin') { existingUser.links = {}; }
+ objectToAdd.push(existingUser); // Add this user
+ } else {
+ objectToAdd.push(newobj); // Add this user
+ }
+ } else if (newobj.type == 'mesh') {
+ // Add this object
+ objectToAdd.push(newobj);
+ } // Don't add nodes.
+ }
+ console.log('Importing ' + objectToAdd.length + ' object(s)...');
+ var pendingCalls = 1;
+ for (var i in objectToAdd) {
+ pendingCalls++;
+ obj.db.Set(objectToAdd[i], function (err) { if (err != null) { console.log(err); } else { if (--pendingCalls == 0) { process.exit(); } } });
+ }
+ if (--pendingCalls == 0) { process.exit(); }
+ });
+ });
+ return;
+ }
+
+ // Check if the database is capable of performing a backup
+ // Moved behind autobackup config init in startex4: obj.db.checkBackupCapability(function (err, msg) { if (msg != null) { obj.addServerWarning(msg, true) } });
+
+ // Load configuration for database if needed
+ if (obj.args.loadconfigfromdb) {
+ var key = null;
+ if (typeof obj.args.configkey == 'string') { key = obj.args.configkey; }
+ else if (typeof obj.args.loadconfigfromdb == 'string') { key = obj.args.loadconfigfromdb; }
+ if (key == null) { console.log("Error, --configkey is required."); process.exit(); return; }
+ obj.db.getAllConfigFiles(key, function (configFiles) {
+ if (configFiles == null) { console.log("Error, no configuration files found or invalid configkey."); process.exit(); return; }
+ if (!configFiles['config.json']) { console.log("Error, could not file config.json from database."); process.exit(); return; }
+ if (typeof configFiles['config.json'] == 'object') { configFiles['config.json'] = configFiles['config.json'].toString(); }
+ if (configFiles['config.json'].charCodeAt(0) == 65279) { configFiles['config.json'] = configFiles['config.json'].substring(1); }
+ obj.configurationFiles = configFiles;
+
+ // Parse the new configuration file
+ var config2 = null;
+ try { config2 = JSON.parse(configFiles['config.json']); } catch (ex) { console.log('Error, unable to parse config.json from database.', ex); process.exit(); return; }
+
+ // Set the command line arguments to the config file if they are not present
+ if (!config2.settings) { config2.settings = {}; }
+ for (i in args) { config2.settings[i] = args[i]; }
+
+ // Lower case all keys in the config file
+ common.objKeysToLower(config2, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'telegram/proxy']);
+
+ // Grab some of the values from the original config.json file if present.
+ config2['mysql'] = config['mysql'];
+ config2['mariadb'] = config['mariadb'];
+ config2['mongodb'] = config['mongodb'];
+ config2['mongodbcol'] = config['mongodbcol'];
+ config2['dbencryptkey'] = config['dbencryptkey'];
+ config2['acebase'] = config['acebase'];
+ config2['sqlite3'] = config['sqlite3'];
+
+ // We got a new config.json from the database, let's use it.
+ config = obj.config = config2;
+ obj.StartEx1b();
+ });
+ } else {
+ config = obj.config = getConfig(obj.args.vault == null);
+ obj.StartEx1b();
+ }
+ });
+ }
+ );
+ };
+
+ // Time to start the server of real.
+ obj.StartEx1b = async function () {
+ var i;
+
+ // Add NodeJS version warning if needed
+ if (Number(process.version.match(/^v(\d+\.\d+)/)[1]) < 16) { addServerWarning("MeshCentral will require Node v16 or above in the future, your current version is " + process.version + "."); }
+
+ // Setup certificate operations
+ obj.certificateOperations = require('./certoperations.js').CertificateOperations(obj);
+
+ // Linux format /var/log/auth.log
+ if (obj.config.settings.authlog != null) {
+ obj.fs.open(obj.config.settings.authlog, 'a', function (err, fd) {
+ if (err == null) { obj.authlogfile = fd; } else { console.log('ERROR: Unable to open: ' + obj.config.settings.authlog); }
+ })
+ }
+
+ // Start CrowdSec bouncer if needed: https://www.crowdsec.net/
+ if (typeof obj.args.crowdsec == 'object') { obj.crowdSecBounser = require('./crowdsec.js').CreateCrowdSecBouncer(obj, obj.args.crowdsec); }
+
+ // Check if self update is allowed. If running as a Windows service, self-update is not possible.
+ if (obj.fs.existsSync(obj.path.join(__dirname, 'daemon'))) { obj.serverSelfWriteAllowed = false; }
+
+ // If we are targetting a specific version, update now.
+ if ((obj.serverSelfWriteAllowed == true) && (typeof obj.args.selfupdate == 'string')) {
+ obj.args.selfupdate = obj.args.selfupdate.toLowerCase();
+ if (getCurrentVersion() !== obj.args.selfupdate) { obj.performServerUpdate(); return; } // We are targetting a specific version, run self update now.
+ }
+
+ // Write the server state
+ obj.updateServerState('state', 'starting');
+ if (process.pid) { obj.updateServerState('server-pid', process.pid); }
+ if (process.ppid) { obj.updateServerState('server-parent-pid', process.ppid); }
+
+ // Read environment variables. For a subset of arguments, we allow them to be read from environment variables.
+ const xenv = ['user', 'port', 'mpsport', 'mpsaliasport', 'redirport', 'rediraliasport', 'exactport', 'debug'];
+ for (i in xenv) { if ((obj.args[xenv[i]] == null) && (process.env['mesh' + xenv[i]])) { obj.args[xenv[i]] = obj.common.toNumber(process.env['mesh' + xenv[i]]); } }
+
+ // Validate the domains, this is used for multi-hosting
+ if (obj.config.domains == null) { obj.config.domains = {}; }
+ if (obj.config.domains[''] == null) { obj.config.domains[''] = {}; }
+ if (obj.config.domains[''].dns != null) { console.log("ERROR: Default domain can't have a DNS name."); return; }
+ var xdomains = {}; for (i in obj.config.domains) { xdomains[i.toLowerCase()] = obj.config.domains[i]; } obj.config.domains = xdomains;
+ var bannedDomains = ['public', 'private', 'images', 'scripts', 'styles', 'views']; // List of banned domains
+ for (i in obj.config.domains) { for (var j in bannedDomains) { if (i == bannedDomains[j]) { console.log("ERROR: Domain '" + i + "' is not allowed domain name in config.json."); delete obj.config.domains[i]; } } }
+ for (i in obj.config.domains) { if ((i.length > 64) || (Buffer.from(i).length > 64)) { console.log("ERROR: Domain '" + i + "' is longer that 64 bytes, this is not allowed."); delete obj.config.domains[i]; } }
+ for (i in obj.config.domains) {
+ // Remove any domains that start with underscore
+ if (i.startsWith('_')) { delete obj.config.domains[i]; continue; }
+
+ // Apply default domain settings if present
+ if (typeof obj.config.domaindefaults == 'object') { for (var j in obj.config.domaindefaults) { if (obj.config.domains[i][j] == null) { obj.config.domains[i][j] = obj.config.domaindefaults[j]; } } }
+
+ // Perform domain setup
+ if (typeof obj.config.domains[i] != 'object') { console.log("ERROR: Invalid domain configuration in config.json."); process.exit(); return; }
+ if ((i.length > 0) && (i[0] == '_')) { delete obj.config.domains[i]; continue; } // Remove any domains with names that start with _
+ if (typeof config.domains[i].auth == 'string') { config.domains[i].auth = config.domains[i].auth.toLowerCase(); }
+ if (obj.config.domains[i].limits == null) { obj.config.domains[i].limits = {}; }
+ if (obj.config.domains[i].preventduplicatedevices == null) { obj.config.domains[i].preventduplicatedevices = false; }
+ if (obj.config.domains[i].dns == null) { obj.config.domains[i].url = (i == '') ? '/' : ('/' + i + '/'); } else { obj.config.domains[i].url = '/'; }
+ obj.config.domains[i].id = i;
+ if ((typeof obj.config.domains[i].maxdeviceview != 'number') || (obj.config.domains[i].maxdeviceview < 1)) { delete obj.config.domains[i].maxdeviceview; }
+ if (typeof obj.config.domains[i].loginkey == 'string') { obj.config.domains[i].loginkey = [obj.config.domains[i].loginkey]; }
+ if ((obj.config.domains[i].loginkey != null) && (obj.common.validateAlphaNumericArray(obj.config.domains[i].loginkey, 1, 128) == false)) { console.log("ERROR: Invalid login key, must be alpha-numeric string with no spaces."); process.exit(); return; }
+ if (typeof obj.config.domains[i].agentkey == 'string') { obj.config.domains[i].agentkey = [obj.config.domains[i].agentkey]; }
+ if ((obj.config.domains[i].agentkey != null) && (obj.common.validateAlphaNumericArray(obj.config.domains[i].agentkey, 1, 128) == false)) { console.log("ERROR: Invalid agent key, must be alpha-numeric string with no spaces."); process.exit(); return; }
+ obj.config.domains[i].userallowedip = obj.config.domains[i].userallowedip = readIpListFromFile(obj.config.domains[i].userallowedip);
+ obj.config.domains[i].userblockedip = obj.config.domains[i].userblockedip = readIpListFromFile(obj.config.domains[i].userblockedip);
+ obj.config.domains[i].agentallowedip = obj.config.domains[i].agentallowedip = readIpListFromFile(obj.config.domains[i].agentallowedip);
+ obj.config.domains[i].agentblockedip = obj.config.domains[i].agentblockedip = readIpListFromFile(obj.config.domains[i].agentblockedip);
+ if (typeof obj.config.domains[i].userallowedip == 'string') { if (obj.config.domains[i].userallowedip == '') { delete obj.config.domains[i].userallowedip; } else { obj.config.domains[i].userallowedip = obj.config.domains[i].userallowedip.split(' ').join('').split(','); } }
+ if (typeof obj.config.domains[i].userblockedip == 'string') { if (obj.config.domains[i].userblockedip == '') { delete obj.config.domains[i].userblockedip; } else { obj.config.domains[i].userblockedip = obj.config.domains[i].userblockedip.split(' ').join('').split(','); } }
+ if (typeof obj.config.domains[i].agentallowedip == 'string') { if (obj.config.domains[i].agentallowedip == '') { delete obj.config.domains[i].agentallowedip; } else { obj.config.domains[i].agentallowedip = obj.config.domains[i].agentallowedip.split(' ').join('').split(','); } }
+ if (typeof obj.config.domains[i].agentblockedip == 'string') { if (obj.config.domains[i].agentblockedip == '') { delete obj.config.domains[i].agentblockedip; } else { obj.config.domains[i].agentblockedip = obj.config.domains[i].agentblockedip.split(' ').join('').split(','); } }
+ // Check IP lists and ranges and if DNS return IP addresses
+ obj.config.domains[i].userallowedip = await resolveDomainsToIps(obj.config.domains[i].userallowedip);
+ obj.config.domains[i].userblockedip = await resolveDomainsToIps(obj.config.domains[i].userblockedip);
+ obj.config.domains[i].agentallowedip = await resolveDomainsToIps(obj.config.domains[i].agentallowedip);
+ obj.config.domains[i].agentblockedip = await resolveDomainsToIps(obj.config.domains[i].agentblockedip);
+ if (typeof obj.config.domains[i].ignoreagenthashcheck == 'string') { if (obj.config.domains[i].ignoreagenthashcheck == '') { delete obj.config.domains[i].ignoreagenthashcheck; } else { obj.config.domains[i].ignoreagenthashcheck = obj.config.domains[i].ignoreagenthashcheck.split(','); } }
+ if (typeof obj.config.domains[i].allowedorigin == 'string') { if (obj.config.domains[i].allowedorigin == '') { delete obj.config.domains[i].allowedorigin; } else { obj.config.domains[i].allowedorigin = obj.config.domains[i].allowedorigin.split(','); } }
+ if ((obj.config.domains[i].passwordrequirements != null) && (typeof obj.config.domains[i].passwordrequirements == 'object')) {
+ if (typeof obj.config.domains[i].passwordrequirements.skip2factor == 'string') {
+ obj.config.domains[i].passwordrequirements.skip2factor = obj.config.domains[i].passwordrequirements.skip2factor.split(',');
+ } else {
+ delete obj.config.domains[i].passwordrequirements.skip2factor;
+ }
+ // Fix the list of users to add "user/domain/" if needed
+ if (Array.isArray(obj.config.domains[i].passwordrequirements.logintokens)) {
+ var newValues = [];
+ for (var j in obj.config.domains[i].passwordrequirements.logintokens) {
+ var splitVal = obj.config.domains[i].passwordrequirements.logintokens[j].split('/');;
+ if (splitVal.length == 1) { newValues.push('user/' + i + '/' + splitVal[0]); }
+ if (splitVal.length == 2) { newValues.push('user/' + splitVal[0] + '/' + splitVal[1]); }
+ if (splitVal.length == 3) { newValues.push(splitVal[0] + '/' + splitVal[1] + '/' + splitVal[2]); }
+ }
+ obj.config.domains[i].passwordrequirements.logintokens = newValues;
+ }
+ }
+ if ((obj.config.domains[i].auth == 'ldap') && (typeof obj.config.domains[i].ldapoptions != 'object')) {
+ if (i == '') { console.log("ERROR: Default domain is LDAP, but is missing LDAPOptions."); } else { console.log("ERROR: Domain '" + i + "' is LDAP, but is missing LDAPOptions."); }
+ process.exit();
+ return;
+ }
+ if ((obj.config.domains[i].auth == 'ldap') || (obj.config.domains[i].auth == 'sspi')) { obj.config.domains[i].newaccounts = 0; } // No new accounts allowed in SSPI/LDAP authentication modes.
+ if (obj.config.domains[i].sitestyle == null) { obj.config.domains[i].sitestyle = 2; } // Default to site style #2
+
+ // Convert newAccountsRights from a array of strings to flags number.
+ obj.config.domains[i].newaccountsrights = obj.common.meshServerRightsArrayToNumber(obj.config.domains[i].newaccountsrights);
+ if (typeof (obj.config.domains[i].newaccountsrights) != 'number') { delete obj.config.domains[i].newaccountsrights; }
+
+ // Check if there is a web views path and/or web public path for this domain
+ if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) {
+ if ((obj.config.domains[i].webviewspath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web-' + i + '/views')))) { obj.config.domains[i].webviewspath = obj.path.join(__dirname, '../../meshcentral-web-' + i + '/views'); }
+ if ((obj.config.domains[i].webpublicpath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web-' + i + '/public')))) { obj.config.domains[i].webpublicpath = obj.path.join(__dirname, '../../meshcentral-web-' + i + '/public'); }
+ if ((obj.config.domains[i].webemailspath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web-' + i + '/emails')))) { obj.config.domains[i].webemailspath = obj.path.join(__dirname, '../../meshcentral-web-' + i + '/emails'); }
+ } else {
+ if ((obj.config.domains[i].webviewspath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web-' + i + '/views')))) { obj.config.domains[i].webviewspath = obj.path.join(__dirname, '../meshcentral-web-' + i + '/views'); }
+ if ((obj.config.domains[i].webpublicpath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web-' + i + '/public')))) { obj.config.domains[i].webpublicpath = obj.path.join(__dirname, '../meshcentral-web-' + i + '/public'); }
+ if ((obj.config.domains[i].webemailspath == null) && (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web-' + i + '/emails')))) { obj.config.domains[i].webemailspath = obj.path.join(__dirname, '../meshcentral-web-' + i + '/emails'); }
+ }
+
+ // Check agent customization if any
+ if (typeof obj.config.domains[i].agentcustomization == 'object') {
+ if (typeof obj.config.domains[i].agentcustomization.displayname != 'string') { delete obj.config.domains[i].agentcustomization.displayname; } else { obj.config.domains[i].agentcustomization.displayname = obj.config.domains[i].agentcustomization.displayname.split('\r').join('').split('\n').join(''); }
+ if (typeof obj.config.domains[i].agentcustomization.description != 'string') { delete obj.config.domains[i].agentcustomization.description; } else { obj.config.domains[i].agentcustomization.description = obj.config.domains[i].agentcustomization.description.split('\r').join('').split('\n').join(''); }
+ if (typeof obj.config.domains[i].agentcustomization.companyname != 'string') { delete obj.config.domains[i].agentcustomization.companyname; } else { obj.config.domains[i].agentcustomization.companyname = obj.config.domains[i].agentcustomization.companyname.split('\r').join('').split('\n').join(''); }
+ if (typeof obj.config.domains[i].agentcustomization.servicename != 'string') { delete obj.config.domains[i].agentcustomization.servicename; } else { obj.config.domains[i].agentcustomization.servicename = obj.config.domains[i].agentcustomization.servicename.split('\r').join('').split('\n').join('').split(' ').join('').split('"').join('').split('\'').join('').split('>').join('').split('<').join('').split('/').join('').split('\\').join(''); }
+ if (typeof obj.config.domains[i].agentcustomization.image != 'string') { delete obj.config.domains[i].agentcustomization.image; } else { try { obj.config.domains[i].agentcustomization.image = 'data:image/png;base64,' + Buffer.from(obj.fs.readFileSync(obj.getConfigFilePath(obj.config.domains[i].agentcustomization.image)), 'binary').toString('base64'); } catch (ex) { console.log(ex); delete obj.config.domains[i].agentcustomization.image; } }
+ } else {
+ delete obj.config.domains[i].agentcustomization;
+ }
+
+ // Convert user consent flags
+ if (typeof obj.config.domains[i].userconsentflags == 'object') {
+ var flags = 0;
+ if (obj.config.domains[i].userconsentflags.desktopnotify == true) { flags |= 1; }
+ if (obj.config.domains[i].userconsentflags.terminalnotify == true) { flags |= 2; }
+ if (obj.config.domains[i].userconsentflags.filenotify == true) { flags |= 4; }
+ if (obj.config.domains[i].userconsentflags.desktopprompt == true) { flags |= 8; }
+ if (obj.config.domains[i].userconsentflags.terminalprompt == true) { flags |= 16; }
+ if (obj.config.domains[i].userconsentflags.fileprompt == true) { flags |= 32; }
+ if (obj.config.domains[i].userconsentflags.desktopprivacybar == true) { flags |= 64; }
+ obj.config.domains[i].userconsentflags = flags;
+ }
+
+ // If we have Intel AMT manager settings, take a look at them here.
+ if (typeof obj.config.domains[i].amtmanager == 'object') {
+ if (typeof obj.config.domains[i].amtmanager.tlsrootcert == 'object') {
+ obj.config.domains[i].amtmanager.tlsrootcert2 = obj.certificateOperations.loadGenericCertAndKey(obj.config.domains[i].amtmanager.tlsrootcert);
+ if (obj.config.domains[i].amtmanager.tlsrootcert2 == null) { // Show an error message if needed
+ if (i == '') {
+ addServerWarning("Unable to load Intel AMT TLS root certificate for default domain.", 5);
+ } else {
+ addServerWarning("Unable to load Intel AMT TLS root certificate for domain " + i + ".", 6, [i]);
+ }
+ }
+ }
+ }
+
+ // Check agentfileinfo
+ if (typeof obj.config.domains[i].agentfileinfo == 'object') {
+ if ((obj.config.domains[i].agentfileinfo.fileversionnumber != null) && (obj.common.parseVersion(obj.config.domains[i].agentfileinfo.fileversionnumber) == null)) { delete obj.config.domains[i].agentfileinfo.fileversionnumber; }
+ if ((obj.config.domains[i].agentfileinfo.productversionnumber != null) && (obj.common.parseVersion(obj.config.domains[i].agentfileinfo.productversionnumber) == null)) { delete obj.config.domains[i].agentfileinfo.productversionnumber; }
+ if ((obj.config.domains[i].agentfileinfo.fileversionnumber == null) && (typeof obj.config.domains[i].agentfileinfo.fileversion == 'string') && (obj.common.parseVersion(obj.config.domains[i].agentfileinfo.fileversion) != null)) { obj.config.domains[i].agentfileinfo.fileversionnumber = obj.config.domains[i].agentfileinfo.fileversion; }
+ if (typeof obj.config.domains[i].agentfileinfo.icon == 'string') {
+ // Load the agent .ico file
+ var icon = null;
+ try { icon = require('./authenticode.js').loadIcon(obj.path.join(obj.datapath, obj.config.domains[i].agentfileinfo.icon)); } catch (ex) { }
+ if (icon != null) {
+ // The icon file was correctly loaded
+ obj.config.domains[i].agentfileinfo.icon = icon;
+ } else {
+ // Failed to load the icon file, display a server warning
+ addServerWarning("Unable to load agent icon file: " + obj.config.domains[i].agentfileinfo.icon + ".", 23, [obj.config.domains[i].agentfileinfo.icon]);
+ delete obj.config.domains[i].agentfileinfo.icon;
+ }
+ } else {
+ // Invalid icon file path
+ delete obj.config.domains[i].agentfileinfo.icon;
+ }
+ if (typeof obj.config.domains[i].agentfileinfo.logo == 'string') {
+ // Load the agent .bmp file
+ var logo = null;
+ try { logo = require('./authenticode.js').loadBitmap(obj.path.join(obj.datapath, obj.config.domains[i].agentfileinfo.logo)); } catch (ex) { }
+ if (logo != null) {
+ // The logo file was correctly loaded
+ obj.config.domains[i].agentfileinfo.logo = logo;
+ } else {
+ // Failed to load the icon file, display a server warning
+ addServerWarning("Unable to load agent logo file: " + obj.config.domains[i].agentfileinfo.logo + ".", 24, [obj.config.domains[i].agentfileinfo.logo]);
+ delete obj.config.domains[i].agentfileinfo.logo;
+ }
+ } else {
+ // Invalid icon file path
+ delete obj.config.domains[i].agentfileinfo.logo;
+ }
+ }
+ }
+
+ // Log passed arguments into Windows Service Log
+ //if (obj.servicelog != null) { var s = ''; for (i in obj.args) { if (i != '_') { if (s.length > 0) { s += ', '; } s += i + "=" + obj.args[i]; } } logInfoEvent('MeshServer started with arguments: ' + s); }
+
+ // Look at passed in arguments
+ if ((obj.args.user != null) && (typeof obj.args.user != 'string')) { delete obj.args.user; }
+ if ((obj.args.ciralocalfqdn != null) && ((obj.args.lanonly == true) || (obj.args.wanonly == true))) { addServerWarning("CIRA local FQDN's ignored when server in LAN-only or WAN-only mode.", 7); }
+ if ((obj.args.ciralocalfqdn != null) && (obj.args.ciralocalfqdn.split(',').length > 4)) { addServerWarning("Can't have more than 4 CIRA local FQDN's. Ignoring value.", 8); obj.args.ciralocalfqdn = null; }
+ if (obj.args.ignoreagenthashcheck === true) { addServerWarning("Agent hash checking is being skipped, this is unsafe.", 9); }
+ if (obj.args.port == null || typeof obj.args.port != 'number') { obj.args.port = 443; }
+ if (obj.args.aliasport != null && (typeof obj.args.aliasport != 'number')) obj.args.aliasport = null;
+ if (obj.args.mpsport == null || typeof obj.args.mpsport != 'number') obj.args.mpsport = 4433;
+ if (obj.args.mpsaliasport != null && (typeof obj.args.mpsaliasport != 'number')) obj.args.mpsaliasport = null;
+ if (obj.args.rediraliasport != null && (typeof obj.args.rediraliasport != 'number')) obj.args.rediraliasport = null;
+ if (obj.args.redirport == null) obj.args.redirport = 80;
+ if (obj.args.minifycore == null) obj.args.minifycore = false;
+ if (typeof obj.args.agentidletimeout != 'number') { obj.args.agentidletimeout = 150000; } else { obj.args.agentidletimeout *= 1000 } // Default agent idle timeout is 2m, 30sec.
+ if ((obj.args.lanonly != true) && (typeof obj.args.webrtconfig == 'object')) { // fix incase you are using an old mis-spelt webrtconfig
+ obj.args.webrtcconfig = obj.args.webrtconfig;
+ delete obj.args.webrtconfig;
+ }
+ if ((obj.args.lanonly != true) && (obj.args.webrtcconfig == null)) { obj.args.webrtcconfig = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun.cloudflare.com:3478' }] }; } // Setup default WebRTC STUN servers
+ else if ((obj.args.lanonly != true) && (typeof obj.args.webrtcconfig == 'object')) {
+ if (obj.args.webrtcconfig.iceservers) { // webrtc is case-sensitive, so must rename iceservers to iceServers!
+ obj.args.webrtcconfig.iceServers = obj.args.webrtcconfig.iceservers;
+ delete obj.args.webrtcconfig.iceservers;
+ }
+ }
+ if (typeof obj.args.ignoreagenthashcheck == 'string') { if (obj.args.ignoreagenthashcheck == '') { delete obj.args.ignoreagenthashcheck; } else { obj.args.ignoreagenthashcheck = obj.args.ignoreagenthashcheck.split(','); } }
+
+ // Setup a site administrator
+ if ((obj.args.admin) && (typeof obj.args.admin == 'string')) {
+ var adminname = obj.args.admin.split('/');
+ if (adminname.length == 1) { adminname = 'user//' + adminname[0]; }
+ else if (adminname.length == 2) { adminname = 'user/' + adminname[0] + '/' + adminname[1]; }
+ else { console.log("Invalid administrator name."); process.exit(); return; }
+ obj.db.Get(adminname, function (err, user) {
+ if (user.length != 1) { console.log("Invalid user name."); process.exit(); return; }
+ user[0].siteadmin = 4294967295; // 0xFFFFFFFF
+ obj.db.Set(user[0], function () {
+ if (user[0].domain == '') { console.log('User ' + user[0].name + ' set to site administrator.'); } else { console.log("User " + user[0].name + " of domain " + user[0].domain + " set to site administrator."); }
+ process.exit();
+ return;
+ });
+ });
+ return;
+ }
+
+ // Remove a site administrator
+ if ((obj.args.unadmin) && (typeof obj.args.unadmin == 'string')) {
+ var adminname = obj.args.unadmin.split('/');
+ if (adminname.length == 1) { adminname = 'user//' + adminname[0]; }
+ else if (adminname.length == 2) { adminname = 'user/' + adminname[0] + '/' + adminname[1]; }
+ else { console.log("Invalid administrator name."); process.exit(); return; }
+ obj.db.Get(adminname, function (err, user) {
+ if (user.length != 1) { console.log("Invalid user name."); process.exit(); return; }
+ if (user[0].siteadmin) { delete user[0].siteadmin; }
+ obj.db.Set(user[0], function () {
+ if (user[0].domain == '') { console.log("User " + user[0].name + " is not a site administrator."); } else { console.log("User " + user[0].name + " of domain " + user[0].domain + " is not a site administrator."); }
+ process.exit();
+ return;
+ });
+ });
+ return;
+ }
+
+ // Setup agent error log
+ if ((obj.config) && (obj.config.settings) && (obj.config.settings.agentlogdump)) {
+ obj.fs.open(obj.path.join(obj.datapath, 'agenterrorlogs.txt'), 'a', function (err, fd) { obj.agentErrorLog = fd; })
+ }
+
+ // Perform other database cleanup
+ obj.db.cleanup();
+
+ // Set all nodes to power state of unknown (0)
+ obj.db.storePowerEvent({ time: new Date(), nodeid: '*', power: 0, s: 1 }, obj.multiServer); // s:1 indicates that the server is starting up.
+
+ // Read or setup database configuration values
+ obj.db.Get('dbconfig', function (err, dbconfig) {
+ if ((dbconfig != null) && (dbconfig.length == 1)) { obj.dbconfig = dbconfig[0]; } else { obj.dbconfig = { _id: 'dbconfig', version: 1 }; }
+ if (obj.dbconfig.amtWsEventSecret == null) { obj.crypto.randomBytes(32, function (err, buf) { obj.dbconfig.amtWsEventSecret = buf.toString('hex'); obj.db.Set(obj.dbconfig); }); }
+
+ // This is used by the user to create a username/password for a Intel AMT WSMAN event subscription
+ if (obj.args.getwspass) {
+ if (obj.args.getwspass.length == 64) {
+ obj.crypto.randomBytes(6, function (err, buf) {
+ while (obj.dbconfig.amtWsEventSecret == null) { process.nextTick(); }
+ const username = buf.toString('hex');
+ const nodeid = obj.args.getwspass;
+ const pass = obj.crypto.createHash('sha384').update(username.toLowerCase() + ':' + nodeid + ':' + obj.dbconfig.amtWsEventSecret).digest('base64').substring(0, 12).split('/').join('x').split('\\').join('x');
+ console.log("--- Intel(r) AMT WSMAN eventing credentials ---");
+ console.log("Username: " + username);
+ console.log("Password: " + pass);
+ console.log("Argument: " + nodeid);
+ process.exit();
+ });
+ } else {
+ console.log("Invalid NodeID.");
+ process.exit();
+ }
+ return;
+ }
+
+ // Setup the task manager
+ if ((obj.config) && (obj.config.settings) && (obj.config.settings.taskmanager == true)) {
+ obj.taskManager = require('./taskmanager').createTaskManager(obj);
+ }
+
+ // Start plugin manager if configuration allows this.
+ if ((obj.config) && (obj.config.settings) && (obj.config.settings.plugins != null) && (obj.config.settings.plugins != false) && ((typeof obj.config.settings.plugins != 'object') || (obj.config.settings.plugins.enabled != false))) {
+ obj.pluginHandler = require('./pluginHandler.js').pluginHandler(obj);
+ }
+
+ // Load the default meshcore and meshcmd
+ obj.updateMeshCore();
+ obj.updateMeshCmd();
+
+ // Setup and start the redirection server if needed. We must start the redirection server before Let's Encrypt.
+ if ((obj.args.redirport != null) && (typeof obj.args.redirport == 'number') && (obj.args.redirport != 0)) {
+ obj.redirserver = require('./redirserver.js').CreateRedirServer(obj, obj.db, obj.args, obj.StartEx2);
+ } else {
+ obj.StartEx2(); // If not needed, move on.
+ }
+ });
+ }
+
+ // Done starting the redirection server, go on to load the server certificates
+ obj.StartEx2 = function () {
+ // Load server certificates
+ obj.certificateOperations.GetMeshServerCertificate(obj.args, obj.config, function (certs) {
+ // Get the current node version
+ if ((obj.config.letsencrypt == null) || (obj.redirserver == null)) {
+ obj.StartEx3(certs); // Just use the configured certificates
+ } else if ((obj.config.letsencrypt != null) && (obj.config.letsencrypt.nochecks == true)) {
+ // Use Let's Encrypt with no checking
+ obj.letsencrypt = require('./letsencrypt.js').CreateLetsEncrypt(obj);
+ obj.letsencrypt.getCertificate(certs, obj.StartEx3); // Use Let's Encrypt with no checking, use at your own risk.
+ } else {
+ // Check Let's Encrypt settings
+ var leok = true;
+ if ((typeof obj.config.letsencrypt.names != 'string') && (typeof obj.config.settings.cert == 'string')) { obj.config.letsencrypt.names = obj.config.settings.cert; }
+ if (typeof obj.config.letsencrypt.email != 'string') { leok = false; addServerWarning("Missing Let's Encrypt email address.", 10); }
+ else if (typeof obj.config.letsencrypt.names != 'string') { leok = false; addServerWarning("Invalid Let's Encrypt host names.", 11); }
+ else if (obj.config.letsencrypt.names.indexOf('*') >= 0) { leok = false; addServerWarning("Invalid Let's Encrypt names, can't contain a *.", 12); }
+ else if (obj.config.letsencrypt.email.split('@').length != 2) { leok = false; addServerWarning("Invalid Let's Encrypt email address.", 10); }
+ else if (obj.config.letsencrypt.email.trim() !== obj.config.letsencrypt.email) { leok = false; addServerWarning("Invalid Let's Encrypt email address.", 10); }
+ else {
+ const le = require('./letsencrypt.js');
+ try { obj.letsencrypt = le.CreateLetsEncrypt(obj); } catch (ex) { console.log(ex); }
+ if (obj.letsencrypt == null) { addServerWarning("Unable to setup Let's Encrypt module.", 13); leok = false; }
+ }
+ if (leok == true) {
+ // Check that the email address domain MX resolves.
+ require('dns').resolveMx(obj.config.letsencrypt.email.split('@')[1], function (err, addresses) {
+ if (err == null) {
+ // Check that all names resolve
+ checkResolveAll(obj.config.letsencrypt.names.split(','), function (err) {
+ if (err == null) {
+ obj.letsencrypt.getCertificate(certs, obj.StartEx3); // Use Let's Encrypt
+ } else {
+ for (var i in err) { addServerWarning("Invalid Let's Encrypt names, unable to resolve: " + err[i], 14, [err[i]]); }
+ obj.StartEx3(certs); // Let's Encrypt did not load, just use the configured certificates
+ }
+ });
+ } else {
+ addServerWarning("Invalid Let's Encrypt email address, unable to resolve: " + obj.config.letsencrypt.email.split('@')[1], 15, [obj.config.letsencrypt.email.split('@')[1]]);
+ obj.StartEx3(certs); // Let's Encrypt did not load, just use the configured certificates
+ }
+ });
+ } else {
+ obj.StartEx3(certs); // Let's Encrypt did not load, just use the configured certificates
+ }
+ }
+ });
+ };
+
+ // Start the server with the given certificates, but check if we have web certificates to load
+ obj.StartEx3 = function (certs) {
+ obj.certificates = certs;
+ obj.certificateOperations.acceleratorStart(certs); // Set the state of the accelerators
+
+ // Load any domain web certificates
+ for (var i in obj.config.domains) {
+ // Load any Intel AMT ACM activation certificates
+ if (obj.config.domains[i].amtacmactivation == null) { obj.config.domains[i].amtacmactivation = {}; }
+ obj.certificateOperations.loadIntelAmtAcmCerts(obj.config.domains[i].amtacmactivation);
+ if (obj.config.domains[i].amtacmactivation.acmCertErrors != null) { for (var j in obj.config.domains[i].amtacmactivation.acmCertErrors) { obj.addServerWarning(obj.config.domains[i].amtacmactivation.acmCertErrors[j]); } }
+ if (typeof obj.config.domains[i].certurl == 'string') {
+ obj.supportsProxyCertificatesRequest = true; // If a certurl is set, enable proxy cert requests
+ // Then, fix the URL and add 'https://' if needed
+ if (obj.config.domains[i].certurl.indexOf('://') < 0) { obj.config.domains[i].certurl = 'https://' + obj.config.domains[i].certurl; }
+ }
+ }
+
+ // Load CloudFlare trusted proxies list if needed
+ if ((obj.config.settings.trustedproxy != null) && (typeof obj.config.settings.trustedproxy == 'string') && (obj.config.settings.trustedproxy.toLowerCase() == 'cloudflare')) {
+ obj.config.settings.extrascriptsrc = 'ajax.cloudflare.com'; // Add CloudFlare as a trusted script source. This allows for CloudFlare's RocketLoader feature.
+ delete obj.args.trustedproxy;
+ delete obj.config.settings.trustedproxy;
+ obj.certificateOperations.loadTextFile('https://www.cloudflare.com/ips-v4', null, function (url, data, tag) {
+ if (data != null) {
+ if (Array.isArray(obj.args.trustedproxy) == false) { obj.args.trustedproxy = []; }
+ const ipranges = data.split('\n');
+ for (var i in ipranges) { if (ipranges[i] != '') { obj.args.trustedproxy.push(ipranges[i]); } }
+ obj.certificateOperations.loadTextFile('https://www.cloudflare.com/ips-v6', null, function (url, data, tag) {
+ if (data != null) {
+ var ipranges = data.split('\n');
+ for (var i in ipranges) { if (ipranges[i] != '') { obj.args.trustedproxy.push(ipranges[i]); } }
+ obj.config.settings.trustedproxy = obj.args.trustedproxy;
+ } else {
+ addServerWarning("Unable to load CloudFlare trusted proxy IPv6 address list.", 16);
+ }
+ obj.StartEx4(); // Keep going
+ });
+ } else {
+ addServerWarning("Unable to load CloudFlare trusted proxy IPv4 address list.", 16);
+ obj.StartEx4(); // Keep going
+ }
+ });
+ } else {
+ obj.StartEx4(); // Keep going
+ }
+ }
+
+ // Start the server with the given certificates
+ obj.StartEx4 = function () {
+ var i;
+
+ // If the certificate is un-configured, force LAN-only mode
+ if (obj.certificates.CommonName.indexOf('.') == -1) { /*console.log('Server name not configured, running in LAN-only mode.');*/ obj.args.lanonly = true; }
+
+ // Write server version and run mode
+ const productionMode = (process.env.NODE_ENV && (process.env.NODE_ENV == 'production'));
+ const runmode = (obj.args.lanonly ? 2 : (obj.args.wanonly ? 1 : 0));
+ console.log("MeshCentral v" + getCurrentVersion() + ', ' + (["Hybrid (LAN + WAN) mode", "WAN mode", "LAN mode"][runmode]) + (productionMode ? ", Production mode." : '.'));
+
+ // Check that no sub-domains have the same DNS as the parent
+ for (i in obj.config.domains) {
+ if ((obj.config.domains[i].dns != null) && (obj.certificates.CommonName.toLowerCase() === obj.config.domains[i].dns.toLowerCase())) {
+ console.log("ERROR: Server sub-domain can't have same DNS name as the parent."); process.exit(0); return;
+ }
+ }
+
+ // Load the list of MeshCentral tools
+ obj.updateMeshTools();
+
+ // Load MeshAgent translation strings
+ try {
+ var translationpath = obj.path.join(__dirname, 'agents', 'agent-translations.json');
+ const translationpath2 = obj.path.join(obj.datapath, 'agents', 'agent-translations.json');
+ if (obj.fs.existsSync(translationpath2)) { translationpath = translationpath2; } // If the agent is present in "meshcentral-data/agents", use that one instead.
+ var translations = JSON.parse(obj.fs.readFileSync(translationpath).toString());
+ if (translations['zh-chs']) { translations['zh-hans'] = translations['zh-chs']; delete translations['zh-chs']; }
+ if (translations['zh-cht']) { translations['zh-hant'] = translations['zh-cht']; delete translations['zh-cht']; }
+
+ // If there is domain customizations to the agent strings, do this here.
+ for (var i in obj.config.domains) {
+ var domainTranslations = translations;
+ if ((typeof obj.config.domains[i].agentcustomization == 'object') && (typeof obj.config.domains[i].agentcustomization.installtext == 'string')) {
+ domainTranslations = Object.assign({}, domainTranslations); // Shallow clone
+ for (var j in domainTranslations) { delete domainTranslations[j].description; }
+ domainTranslations.en.description = obj.config.domains[i].agentcustomization.installtext;
+ }
+ obj.config.domains[i].agentTranslations = JSON.stringify(domainTranslations);
+ }
+ } catch (ex) { }
+
+ // Load any domain specific agents
+ for (var i in obj.config.domains) { if ((i != '') && (obj.config.domains[i].share == null)) { obj.updateMeshAgentsTable(obj.config.domains[i], function () { }); } }
+
+ // Load the list of mesh agents and install scripts
+ if ((obj.args.noagentupdate == 1) || (obj.args.noagentupdate == true)) { for (i in obj.meshAgentsArchitectureNumbers) { obj.meshAgentsArchitectureNumbers[i].update = false; } }
+ obj.signMeshAgents(obj.config.domains[''], function () {
+ obj.updateMeshAgentsTable(obj.config.domains[''], function () {
+ obj.updateMeshAgentInstallScripts();
+
+ // Setup and start the web server
+ obj.crypto.randomBytes(48, function (err, buf) {
+ // Setup Mesh Multi-Server if needed
+ obj.multiServer = require('./multiserver.js').CreateMultiServer(obj, obj.args);
+ if (obj.multiServer != null) {
+ if ((obj.db.databaseType != 3) || (obj.db.changeStream != true)) { console.log("ERROR: Multi-server support requires use of MongoDB with ReplicaSet and ChangeStream enabled."); process.exit(0); return; }
+ if (typeof obj.args.sessionkey != 'string') { console.log("ERROR: Multi-server support requires \"SessionKey\" be set in the settings section of config.json, same key for all servers."); process.exit(0); return; }
+ obj.serverId = obj.multiServer.serverid;
+ for (var serverid in obj.config.peers.servers) { obj.peerConnectivityByNode[serverid] = {}; }
+ }
+
+ // If the server is set to "nousers", allow only loopback unless IP filter is set
+ if ((obj.args.nousers == true) && (obj.args.userallowedip == null)) { obj.args.userallowedip = "::1,127.0.0.1"; }
+
+ // Set the session length to 60 minutes if not set and set a random key if needed
+ if ((obj.args.sessiontime != null) && ((typeof obj.args.sessiontime != 'number') || (obj.args.sessiontime < 1))) { delete obj.args.sessiontime; }
+ if (typeof obj.args.sessionkey != 'string') { obj.args.sessionkey = buf.toString('hex').toUpperCase(); }
+
+ // Create MQTT Broker to hook into webserver and mpsserver
+ if ((typeof obj.config.settings.mqtt == 'object') && (typeof obj.config.settings.mqtt.auth == 'object') && (typeof obj.config.settings.mqtt.auth.keyid == 'string') && (typeof obj.config.settings.mqtt.auth.key == 'string')) { obj.mqttbroker = require("./mqttbroker.js").CreateMQTTBroker(obj, obj.db, obj.args); }
+
+ // Start the web server and if needed, the redirection web server.
+ obj.webserver = require('./webserver.js').CreateWebServer(obj, obj.db, obj.args, obj.certificates, obj.StartEx5);
+ if (obj.redirserver != null) { obj.redirserver.hookMainWebServer(obj.certificates); }
+
+ // Change RelayDNS to a array of strings
+ if (typeof obj.args.relaydns == 'string') { obj.args.relaydns = [obj.args.relaydns]; }
+ if (obj.common.validateStrArray(obj.args.relaydns, 1) == false) { delete obj.args.relaydns; }
+
+ // Start the HTTP relay web server if needed
+ if ((obj.args.relaydns == null) && (typeof obj.args.relayport == 'number') && (obj.args.relayport != 0)) {
+ obj.webrelayserver = require('./webrelayserver.js').CreateWebRelayServer(obj, obj.db, obj.args, obj.certificates, function () { });
+ }
+
+ // Update proxy certificates
+ if (obj.supportsProxyCertificatesRequest == true) { obj.updateProxyCertificates(true); }
+
+ // Setup the Intel AMT event handler
+ obj.amtEventHandler = require('./amtevents.js').CreateAmtEventsHandler(obj);
+
+ // Setup the Intel AMT local network scanner
+ if (obj.args.wanonly != true) {
+ if (obj.args.amtscanner != false) { obj.amtScanner = require('./amtscanner.js').CreateAmtScanner(obj).start(); }
+ if (obj.args.meshscanner != false) { obj.meshScanner = require('./meshscanner.js').CreateMeshScanner(obj).start(); }
+ }
+
+ // Setup and start the MPS server
+ obj.mpsserver = require('./mpsserver.js').CreateMpsServer(obj, obj.db, obj.args, obj.certificates);
+
+ // Setup the Intel AMT manager
+ if (obj.args.amtmanager !== false) {
+ obj.amtManager = require('./amtmanager.js').CreateAmtManager(obj);
+ }
+
+ // Setup and start the legacy swarm server
+ if ((obj.certificates.swarmserver != null) && (obj.args.swarmport != null) && (obj.args.swarmport !== 0)) {
+ obj.swarmserver = require('./swarmserver.js').CreateSwarmServer(obj, obj.db, obj.args, obj.certificates);
+ }
+
+ // Setup the main email server
+ if (obj.config.sendgrid != null) {
+ // Sendgrid server
+ obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
+ obj.mailserver.verify();
+ if (obj.args.lanonly == true) { addServerWarning("SendGrid server has limited use in LAN mode.", 17); }
+ } else if (obj.config.smtp != null) {
+ // SMTP server
+ obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
+ obj.mailserver.verify();
+ if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
+ } else if (obj.config.sendmail != null) {
+ // Sendmail server
+ obj.mailserver = require('./meshmail.js').CreateMeshMail(obj);
+ obj.mailserver.verify();
+ if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
+ }
+
+ // Setup the email server for each domain
+ for (i in obj.config.domains) {
+ if (obj.config.domains[i].sendgrid != null) {
+ // Sendgrid server
+ obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
+ obj.config.domains[i].mailserver.verify();
+ if (obj.args.lanonly == true) { addServerWarning("SendGrid server has limited use in LAN mode.", 17); }
+ } else if ((obj.config.domains[i].smtp != null) && (obj.config.domains[i].smtp.host != null) && (obj.config.domains[i].smtp.from != null)) {
+ // SMTP server
+ obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
+ obj.config.domains[i].mailserver.verify();
+ if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
+ } else if (obj.config.domains[i].sendmail != null) {
+ // Sendmail server
+ obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]);
+ obj.config.domains[i].mailserver.verify();
+ if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode.", 18); }
+ } else {
+ // Setup the parent mail server for this domain
+ if (obj.mailserver != null) { obj.config.domains[i].mailserver = obj.mailserver; }
+ }
+ }
+
+ // Setup SMS gateway
+ if (config.sms != null) {
+ obj.smsserver = require('./meshsms.js').CreateMeshSMS(obj);
+ if ((obj.smsserver != null) && (obj.args.lanonly == true)) { addServerWarning("SMS gateway has limited use in LAN mode.", 19); }
+ }
+
+ // Setup user messaging
+ if (config.messaging != null) {
+ obj.msgserver = require('./meshmessaging.js').CreateServer(obj);
+ }
+
+ // Setup web based push notifications
+ if ((typeof config.settings.webpush == 'object') && (typeof config.settings.webpush.email == 'string')) {
+ obj.webpush = require('web-push');
+ var vapidKeys = null;
+ try { vapidKeys = JSON.parse(obj.fs.readFileSync(obj.path.join(obj.datapath, 'vapid.json')).toString()); } catch (ex) { }
+ if ((vapidKeys == null) || (typeof vapidKeys.publicKey != 'string') || (typeof vapidKeys.privateKey != 'string')) {
+ console.log("Generating web push VAPID keys...");
+ vapidKeys = obj.webpush.generateVAPIDKeys();
+ obj.common.moveOldFiles([obj.path.join(obj.datapath, 'vapid.json')]);
+ obj.fs.writeFileSync(obj.path.join(obj.datapath, 'vapid.json'), JSON.stringify(vapidKeys));
+ }
+ obj.webpush.vapidPublicKey = vapidKeys.publicKey;
+ obj.webpush.setVapidDetails('mailto:' + config.settings.webpush.email, vapidKeys.publicKey, vapidKeys.privateKey);
+ if (typeof config.settings.webpush.gcmapi == 'string') { webpush.setGCMAPIKey(config.settings.webpush.gcmapi); }
+ }
+
+ // Get the current node version
+ const verSplit = process.version.substring(1).split('.');
+ var nodeVersion = parseInt(verSplit[0]) + (parseInt(verSplit[1]) / 100);
+
+ // Setup Firebase
+ if ((config.firebase != null) && (typeof config.firebase.senderid == 'string') && (typeof config.firebase.serverkey == 'string')) {
+ addServerWarning('Firebase now requires a service account JSON file, Firebase disabled.', 27);
+ } else if ((config.firebase != null) && (typeof config.firebase.serviceaccountfile == 'string')) {
+ var serviceAccount;
+ try { serviceAccount = JSON.parse(obj.fs.readFileSync(obj.path.join(obj.datapath, config.firebase.serviceaccountfile)).toString()); } catch (ex) { console.log(ex); }
+ if (serviceAccount != null) { obj.firebase = require('./firebase').CreateFirebase(obj, serviceAccount); }
+ } else if ((typeof config.firebaserelay == 'object') && (typeof config.firebaserelay.url == 'string')) {
+ // Setup the push messaging relay
+ obj.firebase = require('./firebase').CreateFirebaseRelay(obj, config.firebaserelay.url, config.firebaserelay.key);
+ } else if (obj.config.settings.publicpushnotifications === true) {
+ // Setup the Firebase push messaging relay using https://alt.meshcentral.com, this is the public push notification server.
+ obj.firebase = require('./firebase').CreateFirebaseRelay(obj, 'https://alt.meshcentral.com/firebaserelay.aspx');
+ }
+
+ // Setup monitoring
+ obj.monitoring = require('./monitoring.js').CreateMonitoring(obj, obj.args);
+
+ // Start periodic maintenance
+ obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 60 * 60); // Run this every hour
+ //obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 10 * 1); // DEBUG: Run this more often
+
+ // Dispatch an event that the server is now running
+ obj.DispatchEvent(['*'], obj, { etype: 'server', action: 'started', msg: 'Server started' });
+
+ // Plugin hook. Need to run something at server startup? This is the place.
+ if (obj.pluginHandler) { obj.pluginHandler.callHook('server_startup'); }
+
+ // Setup the login cookie encryption key
+ if ((obj.config) && (obj.config.settings) && (typeof obj.config.settings.logincookieencryptionkey == 'string')) {
+ // We have a string, hash it and use that as a key
+ try { obj.loginCookieEncryptionKey = Buffer.from(obj.config.settings.logincookieencryptionkey, 'hex'); } catch (ex) { }
+ if ((obj.loginCookieEncryptionKey == null) || (obj.loginCookieEncryptionKey.length != 80)) { addServerWarning("Invalid \"LoginCookieEncryptionKey\" in config.json.", 20); obj.loginCookieEncryptionKey = null; }
+ }
+
+ // Login cookie encryption key not set, use one from the database
+ if (obj.loginCookieEncryptionKey == null) {
+ obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
+ if ((docs != null) && (docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 160)) {
+ obj.loginCookieEncryptionKey = Buffer.from(docs[0].key, 'hex');
+ } else {
+ obj.loginCookieEncryptionKey = obj.generateCookieKey(); obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() });
+ }
+ });
+ }
+
+ // Load the invitation link encryption key from the database
+ obj.db.Get('InvitationLinkEncryptionKey', function (err, docs) {
+ if ((docs != null) && (docs.length > 0) && (docs[0].key != null) && (docs[0].key.length >= 160)) {
+ obj.invitationLinkEncryptionKey = Buffer.from(docs[0].key, 'hex');
+ } else {
+ obj.invitationLinkEncryptionKey = obj.generateCookieKey(); obj.db.Set({ _id: 'InvitationLinkEncryptionKey', key: obj.invitationLinkEncryptionKey.toString('hex'), time: Date.now() });
+ }
+ });
+
+ // Setup Intel AMT hello server
+ if ((typeof config.settings.amtprovisioningserver == 'object') && (typeof config.settings.amtprovisioningserver.devicegroup == 'string') && (typeof config.settings.amtprovisioningserver.newmebxpassword == 'string') && (typeof config.settings.amtprovisioningserver.trustedfqdn == 'string') && (typeof config.settings.amtprovisioningserver.ip == 'string')) {
+ obj.amtProvisioningServer = require('./amtprovisioningserver').CreateAmtProvisioningServer(obj, config.settings.amtprovisioningserver);
+ }
+
+ // Start collecting server stats every 5 minutes
+ obj.trafficStats = obj.webserver.getTrafficStats();
+ setInterval(function () {
+ obj.serverStatsCounter++;
+ var hours = 720; // Start with all events lasting 30 days.
+ if (((obj.serverStatsCounter) % 2) == 1) { hours = 3; } // Half of the event get removed after 3 hours.
+ else if ((Math.floor(obj.serverStatsCounter / 2) % 2) == 1) { hours = 8; } // Another half of the event get removed after 8 hours.
+ else if ((Math.floor(obj.serverStatsCounter / 4) % 2) == 1) { hours = 24; } // Another half of the event get removed after 24 hours.
+ else if ((Math.floor(obj.serverStatsCounter / 8) % 2) == 1) { hours = 48; } // Another half of the event get removed after 48 hours.
+ else if ((Math.floor(obj.serverStatsCounter / 16) % 2) == 1) { hours = 72; } // Another half of the event get removed after 72 hours.
+ const expire = new Date();
+ expire.setTime(expire.getTime() + (60 * 60 * 1000 * hours));
+
+ // Get traffic data
+ var trafficStats = obj.webserver.getTrafficDelta(obj.trafficStats);
+ obj.trafficStats = trafficStats.current;
+
+ var data = {
+ time: new Date(),
+ expire: expire,
+ mem: process.memoryUsage(),
+ conn: {
+ ca: Object.keys(obj.webserver.wsagents).length,
+ cu: Object.keys(obj.webserver.wssessions).length,
+ us: Object.keys(obj.webserver.wssessions2).length,
+ rs: obj.webserver.relaySessionCount,
+ am: 0
+ },
+ traffic: trafficStats.delta
+ };
+ try { data.cpu = require('os').loadavg(); } catch (ex) { }
+ if (obj.mpsserver != null) {
+ data.conn.amc = 0;
+ for (var i in obj.mpsserver.ciraConnections) { data.conn.amc += obj.mpsserver.ciraConnections[i].length; }
+ }
+ for (var i in obj.connectivityByNode) {
+ const node = obj.connectivityByNode[i];
+ if (node && typeof node.connectivity !== 'undefined' && node.connectivity === 4) { data.conn.am++; }
+ }
+ if (obj.firstStats === true) { delete obj.firstStats; data.first = true; }
+ if (obj.multiServer != null) { data.s = obj.multiServer.serverid; }
+ obj.db.SetServerStats(data); // Save the stats to the database
+ obj.DispatchEvent(['*'], obj, { action: 'servertimelinestats', data: data }); // Event the server stats
+ }, 300000);
+
+ obj.debug('main', "Server started");
+ if (obj.args.nousers == true) { obj.updateServerState('nousers', '1'); }
+ obj.updateServerState('state', "running");
+
+ // Setup auto-backup defaults. Unless autobackup is set to false try to make a backup.
+ if (obj.config.settings.autobackup == false || obj.config.settings.autobackup == 'false') { obj.config.settings.autobackup = {backupintervalhours: -1}; } //block all autobackup functions
+ else {
+ if (typeof obj.config.settings.autobackup != 'object') { obj.config.settings.autobackup = {}; };
+ if (typeof obj.config.settings.autobackup.backupintervalhours != 'number') { obj.config.settings.autobackup.backupintervalhours = 24; };
+ if (typeof obj.config.settings.autobackup.keeplastdaysbackup != 'number') { obj.config.settings.autobackup.keeplastdaysbackup = 10; };
+ if (obj.config.settings.autobackup.backuphour != null ) { obj.config.settings.autobackup.backupintervalhours = 24; if ((typeof obj.config.settings.autobackup.backuphour != 'number') || (obj.config.settings.autobackup.backuphour > 23 || obj.config.settings.autobackup.backuphour < 0 )) { obj.config.settings.autobackup.backuphour = 0; }}
+ else {obj.config.settings.autobackup.backuphour = -1 };
+ //arrayfi in case of string and remove possible ', ' space. !! If a string instead of an array is passed, it will be split by ',' so *{.txt,.log} won't work in that case !!
+ if (!obj.config.settings.autobackup.backupignorefilesglob) {obj.config.settings.autobackup.backupignorefilesglob = []}
+ else if (typeof obj.config.settings.autobackup.backupignorefilesglob == 'string') { obj.config.settings.autobackup.backupignorefilesglob = obj.config.settings.autobackup.backupignorefilesglob.replaceAll(', ', ',').split(','); };
+ if (!obj.config.settings.autobackup.backupskipfoldersglob) {obj.config.settings.autobackup.backupskipfoldersglob = []}
+ else if (typeof obj.config.settings.autobackup.backupskipfoldersglob == 'string') { obj.config.settings.autobackup.backupskipfoldersglob = obj.config.settings.autobackup.backupskipfoldersglob.replaceAll(', ', ',').split(','); };
+ if (typeof obj.config.settings.autobackup.backuppath == 'string') { obj.backuppath = (obj.config.settings.autobackup.backuppath = (obj.path.resolve(obj.config.settings.autobackup.backuppath))) } else { obj.config.settings.autobackup.backuppath = obj.backuppath };
+ if (typeof obj.config.settings.autobackup.backupname != 'string') { obj.config.settings.autobackup.backupname = 'meshcentral-autobackup-'};
+ if (typeof obj.config.settings.autobackup.webdav == 'object') {
+ //make webdav compliant: http://www.webdav.org/specs/rfc4918.html#rfc.section.5.2, http://www.webdav.org/specs/rfc2518.html#METHOD_MKCOL
+ // So with leading and trailing slash in the foldername, and no double and backslashes
+ if (typeof obj.config.settings.autobackup.webdav.foldername != 'string') {obj.config.settings.autobackup.webdav.foldername = '/MeshCentral-Backups/'}
+ else {obj.config.settings.autobackup.webdav.foldername = ('/' + obj.config.settings.autobackup.webdav.foldername + '/').replaceAll("\\", "/").replaceAll("//", "/").replaceAll("//", "/")};
+ }
+ }
+
+ // Check if the database is capable of performing a backup
+ obj.db.checkBackupCapability(function (err, msg) { if (msg != null) { obj.addServerWarning(msg, true) } });
+
+ // Load Intel AMT passwords from the "amtactivation.log" file
+ obj.loadAmtActivationLogPasswords(function (amtPasswords) {
+ obj.amtPasswords = amtPasswords;
+ });
+
+ // Setup users that can see all device groups
+ if (typeof obj.config.settings.managealldevicegroups == 'string') { obj.config.settings.managealldevicegroups = obj.config.settings.managealldevicegroups.split(','); }
+ else if (Array.isArray(obj.config.settings.managealldevicegroups) == false) { obj.config.settings.managealldevicegroups = []; }
+ for (i in obj.config.domains) {
+ if (Array.isArray(obj.config.domains[i].managealldevicegroups)) {
+ for (var j in obj.config.domains[i].managealldevicegroups) {
+ if (typeof obj.config.domains[i].managealldevicegroups[j] == 'string') {
+ const u = 'user/' + i + '/' + obj.config.domains[i].managealldevicegroups[j];
+ if (obj.config.settings.managealldevicegroups.indexOf(u) == -1) { obj.config.settings.managealldevicegroups.push(u); }
+ }
+ }
+ }
+ }
+ obj.config.settings.managealldevicegroups.sort();
+
+ // Start watchdog timer if needed
+ // This is used to monitor if NodeJS is servicing IO correctly or getting held up a lot. Add this line to the settings section of config.json
+ // "watchDog": { "interval": 100, "timeout": 150 }
+ // This will check every 100ms, if the timer is more than 150ms late, it will warn.
+ if ((typeof config.settings.watchdog == 'object') && (typeof config.settings.watchdog.interval == 'number') && (typeof config.settings.watchdog.timeout == 'number') && (config.settings.watchdog.interval >= 50) && (config.settings.watchdog.timeout >= 50)) {
+ obj.watchdogtime = Date.now();
+ obj.watchdogmax = 0;
+ obj.watchdogmaxtime = null;
+ obj.watchdogtable = [];
+ obj.watchdog = setInterval(function () {
+ const now = Date.now(), delta = now - obj.watchdogtime - config.settings.watchdog.interval;
+ if (delta > obj.watchdogmax) { obj.watchdogmax = delta; obj.watchdogmaxtime = new Date().toLocaleString(); }
+ if (delta > config.settings.watchdog.timeout) {
+ const msg = obj.common.format("Watchdog timer timeout, {0}ms.", delta);
+ obj.watchdogtable.push(new Date().toLocaleString() + ', ' + delta + 'ms');
+ while (obj.watchdogtable.length > 10) { obj.watchdogtable.shift(); }
+ obj.debug('main', msg);
+ try {
+ var errlogpath = null;
+ if (typeof obj.args.mesherrorlogpath == 'string') { errlogpath = obj.path.join(obj.args.mesherrorlogpath, 'mesherrors.txt'); } else { errlogpath = obj.getConfigFilePath('mesherrors.txt'); }
+ obj.fs.appendFileSync(errlogpath, new Date().toLocaleString() + ': ' + msg + '\r\n');
+ } catch (ex) { console.log('ERROR: Unable to write to mesherrors.txt.'); }
+ }
+ obj.watchdogtime = now;
+ }, config.settings.watchdog.interval);
+ obj.debug('main', "Started watchdog timer.");
+ }
+
+ });
+ });
+ });
+ };
+
+ // Called when the web server finished loading
+ obj.StartEx5 = function () {
+ // Setup the email server for each domain
+ var ipKvmSupport = false;
+ for (var i in obj.config.domains) { if (obj.config.domains[i].ipkvm == true) { ipKvmSupport = true; } }
+ if (ipKvmSupport) { obj.ipKvmManager = require('./meshipkvm').CreateIPKVMManager(obj); }
+
+ // Run the server start script if present
+ if (typeof obj.config.settings.runonserverstarted == 'string') {
+ const child_process = require('child_process');
+ var parentpath = __dirname;
+ if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) { parentpath = require('path').join(__dirname, '../..'); }
+ child_process.exec(obj.config.settings.runonserverstarted + ' ' + getCurrentVersion(), { maxBuffer: 512000, timeout: 120000, cwd: parentpath }, function (error, stdout, stderr) { });
+ }
+ }
+
+ // Refresh any certificate hashs from the reverse proxy
+ obj.pendingProxyCertificatesRequests = 0;
+ obj.lastProxyCertificatesRequest = null;
+ obj.supportsProxyCertificatesRequest = false;
+ obj.updateProxyCertificates = function (force) {
+ if (force !== true) {
+ if ((obj.pendingProxyCertificatesRequests > 0) || (obj.supportsProxyCertificatesRequest == false)) return;
+ if ((obj.lastProxyCertificatesRequest != null) && ((Date.now() - obj.lastProxyCertificatesRequest) < 120000)) return; // Don't allow this call more than every 2 minutes.
+ obj.lastProxyCertificatesRequest = Date.now();
+ }
+
+ // Load any domain web certificates
+ for (var i in obj.config.domains) {
+ if (obj.config.domains[i].certurl != null) {
+ // Load web certs
+ obj.pendingProxyCertificatesRequests++;
+ var dnsname = obj.config.domains[i].dns;
+ if ((dnsname == null) && (obj.config.settings.cert != null)) { dnsname = obj.config.settings.cert; }
+ obj.certificateOperations.loadCertificate(obj.config.domains[i].certurl, dnsname, obj.config.domains[i], function (url, cert, xhostname, xdomain) {
+ obj.pendingProxyCertificatesRequests--;
+ if (cert != null) {
+ // Hash the entire cert
+ const hash = obj.crypto.createHash('sha384').update(Buffer.from(cert, 'binary')).digest('hex');
+ if (xdomain.certhash != hash) { // The certificate has changed.
+ xdomain.certkeyhash = hash;
+ xdomain.certhash = hash;
+
+ try {
+ // Decode a RSA certificate and hash the public key, if this is not RSA, skip this.
+ const forgeCert = obj.certificateOperations.forge.pki.certificateFromAsn1(obj.certificateOperations.forge.asn1.fromDer(cert));
+ xdomain.certkeyhash = obj.certificateOperations.forge.pki.getPublicKeyFingerprint(forgeCert.publicKey, { md: obj.certificateOperations.forge.md.sha384.create(), encoding: 'hex' });
+ obj.webserver.webCertificateExpire[xdomain.id] = Date.parse(forgeCert.validity.notAfter); // Update certificate expire time
+ //console.log('V1: ' + xdomain.certkeyhash);
+ } catch (ex) {
+ delete obj.webserver.webCertificateExpire[xdomain.id]; // Remove certificate expire time
+ delete xdomain.certkeyhash;
+ }
+
+ if (obj.webserver) {
+ obj.webserver.webCertificateHashs[xdomain.id] = obj.webserver.webCertificateFullHashs[xdomain.id] = Buffer.from(hash, 'hex').toString('binary');
+ if (xdomain.certkeyhash != null) { obj.webserver.webCertificateHashs[xdomain.id] = Buffer.from(xdomain.certkeyhash, 'hex').toString('binary'); }
+
+ // Disconnect all agents with bad web certificates
+ for (var i in obj.webserver.wsagentsWithBadWebCerts) { obj.webserver.wsagentsWithBadWebCerts[i].close(1); }
+ }
+
+ console.log(obj.common.format("Loaded web certificate from \"{0}\", host: \"{1}\"", url, xhostname));
+ console.log(obj.common.format(" SHA384 cert hash: {0}", xdomain.certhash));
+ if ((xdomain.certkeyhash != null) && (xdomain.certhash != xdomain.certkeyhash)) { console.log(obj.common.format(" SHA384 key hash: {0}", xdomain.certkeyhash)); }
+ }
+ } else {
+ console.log(obj.common.format("Failed to load web certificate at: \"{0}\", host: \"{1}\"", url, xhostname));
+ }
+ });
+ }
+ }
+ }
+
+ // Perform maintenance operations (called every hour)
+ obj.maintenanceActions = function () {
+ // Perform database maintenance
+ obj.db.maintenance();
+
+ // Clean up any temporary files
+ const removeTime = new Date(Date.now()).getTime() - (30 * 60 * 1000); // 30 minutes
+ const dir = obj.fs.readdir(obj.path.join(obj.filespath, 'tmp'), function (err, files) {
+ if (err != null) return;
+ for (var i in files) { try { const filepath = obj.path.join(obj.filespath, 'tmp', files[i]); if (obj.fs.statSync(filepath).mtime.getTime() < removeTime) { obj.fs.unlink(filepath, function () { }); } } catch (ex) { } }
+ });
+
+ // Check for self-update that targets a specific version
+ if ((typeof obj.args.selfupdate == 'string') && (getCurrentVersion() === obj.args.selfupdate)) { obj.args.selfupdate = false; }
+
+ // Check if we need to perform server self-update
+ if ((obj.args.selfupdate) && (obj.serverSelfWriteAllowed == true)) {
+ obj.db.getValueOfTheDay('performSelfUpdate', 1, function (performSelfUpdate) {
+ if (performSelfUpdate.value > 0) {
+ performSelfUpdate.value--;
+ obj.db.Set(performSelfUpdate);
+ obj.getLatestServerVersion(function (currentVer, latestVer) { if (currentVer != latestVer) { obj.performServerUpdate(); return; } });
+ } else {
+ checkAutobackup();
+ }
+ });
+ } else {
+ checkAutobackup();
+ }
+ };
+
+ // Check if we need to perform an automatic backup
+ function checkAutobackup() {
+ if (obj.config.settings.autobackup.backupintervalhours >= 1 ) {
+ obj.db.Get('LastAutoBackupTime', function (err, docs) {
+ if (err != null) { console.error("checkAutobackup: Error getting LastBackupTime from DB"); return}
+ var lastBackup = 0;
+ const currentdate = new Date();
+ let currentHour = currentdate.getHours();
+ let now = currentdate.getTime();
+ if (docs.length == 1) { lastBackup = docs[0].value; }
+ const delta = now - lastBackup;
+ //const delta = 9999999999; // DEBUG: backup always
+ obj.debug ('backup', 'Entering checkAutobackup, lastAutoBackupTime: ' + new Date(lastBackup).toLocaleString('default', { dateStyle: 'medium', timeStyle: 'short' }) + ', delta: ' + (delta/(1000*60*60)).toFixed(2) + ' hours');
+ //start autobackup if interval has passed or at configured hour, whichever comes first. When an hour schedule is missed, it will make a backup immediately.
+ if ((delta > (obj.config.settings.autobackup.backupintervalhours * 60 * 60 * 1000)) || ((currentHour == obj.config.settings.autobackup.backuphour) && (delta >= 2 * 60 * 60 * 1000))) {
+ // A new auto-backup is required.
+ obj.db.Set({ _id: 'LastAutoBackupTime', value: now }); // Save the current time in the database
+ obj.db.performBackup(); // Perform the backup
+ }
+ });
+ }
+ }
+
+ // Stop the Meshcentral server
+ obj.Stop = function (restoreFile) {
+ // If the database is not setup, exit now.
+ if (!obj.db) return;
+
+ // Dispatch an event saying the server is now stopping
+ obj.DispatchEvent(['*'], obj, { etype: 'server', action: 'stopped', msg: "Server stopped" });
+
+ // Set all nodes to power state of unknown (0)
+ obj.db.storePowerEvent({ time: new Date(), nodeid: '*', power: 0, s: 2 }, obj.multiServer, function () { // s:2 indicates that the server is shutting down.
+ if (restoreFile) {
+ obj.debug('main', obj.common.format("Server stopped, updating settings: {0}", restoreFile));
+ console.log("Updating settings folder...");
+
+ const yauzl = require('yauzl');
+ yauzl.open(restoreFile, { lazyEntries: true }, function (err, zipfile) {
+ if (err) throw err;
+ zipfile.readEntry();
+ zipfile.on('entry', function (entry) {
+ if (/\/$/.test(entry.fileName)) {
+ // Directory file names end with '/'.
+ // Note that entires for directories themselves are optional.
+ // An entry's fileName implicitly requires its parent directories to exist.
+ zipfile.readEntry();
+ } else {
+ // File entry
+ zipfile.openReadStream(entry, function (err, readStream) {
+ if (err) throw err;
+ readStream.on('end', function () { zipfile.readEntry(); });
+ var directory = obj.path.dirname(entry.fileName);
+ if (directory != '.') {
+ directory = obj.getConfigFilePath(directory)
+ if (obj.fs.existsSync(directory) == false) { obj.fs.mkdirSync(directory); }
+ }
+ //console.log('Extracting:', obj.getConfigFilePath(entry.fileName));
+ readStream.pipe(obj.fs.createWriteStream(obj.getConfigFilePath(entry.fileName)));
+ });
+ }
+ });
+ zipfile.on('end', function () { setTimeout(function () { obj.fs.unlinkSync(restoreFile); process.exit(123); }); });
+ });
+ } else {
+ obj.debug('main', "Server stopped");
+ process.exit(0);
+ }
+ });
+
+ // Update the server state
+ obj.updateServerState('state', "stopped");
+ };
+
+ // Event Dispatch
+ obj.AddEventDispatch = function (ids, target) {
+ obj.debug('dispatch', 'AddEventDispatch', ids);
+ for (var i in ids) { var id = ids[i]; if (!obj.eventsDispatch[id]) { obj.eventsDispatch[id] = [target]; } else { obj.eventsDispatch[id].push(target); } }
+ };
+ obj.RemoveEventDispatch = function (ids, target) {
+ obj.debug('dispatch', 'RemoveEventDispatch', ids);
+ for (var i in ids) {
+ const id = ids[i];
+ if (obj.eventsDispatch[id]) {
+ var j = obj.eventsDispatch[id].indexOf(target);
+ if (j >= 0) {
+ if (obj.eventsDispatch[id].length == 1) {
+ delete obj.eventsDispatch[id];
+ } else {
+ const newList = []; // We create a new list so not to modify the original list. Allows this function to be called during an event dispatch.
+ for (var k in obj.eventsDispatch[i]) { if (obj.eventsDispatch[i][k] != target) { newList.push(obj.eventsDispatch[i][k]); } }
+ obj.eventsDispatch[i] = newList;
+ }
+ }
+ }
+ }
+ };
+ obj.RemoveEventDispatchId = function (id) {
+ obj.debug('dispatch', 'RemoveEventDispatchId', id);
+ if (obj.eventsDispatch[id] != null) { delete obj.eventsDispatch[id]; }
+ };
+ obj.RemoveAllEventDispatch = function (target) {
+ obj.debug('dispatch', 'RemoveAllEventDispatch');
+ for (var i in obj.eventsDispatch) {
+ const j = obj.eventsDispatch[i].indexOf(target);
+ if (j >= 0) {
+ if (obj.eventsDispatch[i].length == 1) {
+ delete obj.eventsDispatch[i];
+ } else {
+ const newList = []; // We create a new list so not to modify the original list. Allows this function to be called during an event dispatch.
+ for (var k in obj.eventsDispatch[i]) { if (obj.eventsDispatch[i][k] != target) { newList.push(obj.eventsDispatch[i][k]); } }
+ obj.eventsDispatch[i] = newList;
+ }
+ }
+ }
+ };
+ obj.DispatchEvent = function (ids, source, event, fromPeerServer) {
+ // If the database is not setup, exit now.
+ if (!obj.db) return;
+
+ // Send event to syslog if needed
+ if (obj.syslog && event.msg) { obj.syslog.log(obj.syslog.LOG_INFO, event.msg); }
+ if (obj.syslogjson) { obj.syslogjson.log(obj.syslogjson.LOG_INFO, JSON.stringify(event)); }
+ if (obj.syslogtcp && event.msg) { obj.syslogtcp.log(event.msg, obj.syslogtcp.LOG_INFO); }
+
+ obj.debug('dispatch', 'DispatchEvent', ids);
+ if ((typeof event == 'object') && (!event.nolog)) {
+ event.time = new Date();
+ // The event we store is going to skip some of the fields so we don't store too much stuff in the database.
+ const storeEvent = Object.assign({}, event);
+ if (storeEvent.node) { delete storeEvent.node; } // Skip the "node" field. May skip more in the future.
+ if (storeEvent.links) {
+ // Escape "links" names that may have "." and/or "$"
+ storeEvent.links = Object.assign({}, storeEvent.links);
+ for (var i in storeEvent.links) { var ue = obj.common.escapeFieldName(i); if (ue !== i) { storeEvent.links[ue] = storeEvent.links[i]; delete storeEvent.links[i]; } }
+ }
+ if (storeEvent.mesh) {
+ // Escape "mesh" names that may have "." and/or "$"
+ storeEvent.mesh = obj.common.escapeLinksFieldNameEx(storeEvent.mesh);
+ }
+ storeEvent.ids = ids;
+ obj.db.StoreEvent(storeEvent);
+ }
+ const targets = []; // List of targets we dispatched the event to, we don't want to dispatch to the same target twice.
+ for (var j in ids) {
+ const id = ids[j];
+ const eventsDispatch = obj.eventsDispatch[id];
+ if (eventsDispatch) {
+ for (var i in eventsDispatch) {
+ if (targets.indexOf(eventsDispatch[i]) == -1) { // Check if we already displatched to this target
+ targets.push(eventsDispatch[i]);
+ try { eventsDispatch[i].HandleEvent(source, event, ids, id); } catch (ex) { console.log(ex, eventsDispatch[i]); }
+ }
+ }
+ }
+ }
+ if ((fromPeerServer == null) && (obj.multiServer != null) && ((typeof event != 'object') || (event.nopeers != 1))) { obj.multiServer.DispatchEvent(ids, source, event); }
+ };
+
+ // Get the connection state of a node
+ obj.GetConnectivityState = function (nodeid) { return obj.connectivityByNode[nodeid]; };
+
+ // Get the routing server id for a given node and connection type, can never be self.
+ obj.GetRoutingServerIdNotSelf = function (nodeid, connectType) {
+ if (obj.multiServer == null) return null;
+ for (var serverid in obj.peerConnectivityByNode) {
+ if (serverid == obj.serverId) continue;
+ var state = obj.peerConnectivityByNode[serverid][nodeid];
+ if ((state != null) && ((state.connectivity & connectType) != 0)) { return { serverid: serverid, meshid: state.meshid }; }
+ }
+ return null;
+ };
+
+ // Get the routing server id for a given node and connection type, self first
+ obj.GetRoutingServerId = function (nodeid, connectType) {
+ if (obj.multiServer == null) return null;
+
+ // Look at our own server first
+ var connections = obj.peerConnectivityByNode[obj.serverId];
+ if (connections != null) {
+ var state = connections[nodeid];
+ if ((state != null) && ((state.connectivity & connectType) != 0)) { return { serverid: obj.serverId, meshid: state.meshid }; }
+ }
+
+ // Look at other servers
+ for (var serverid in obj.peerConnectivityByNode) {
+ if (serverid == obj.serverId) continue;
+ var state = obj.peerConnectivityByNode[serverid][nodeid];
+ if ((state != null) && ((state.connectivity & connectType) != 0)) { return { serverid: serverid, meshid: state.meshid }; }
+ }
+ return null;
+ };
+
+ // Update the connection state of a node when in multi-server mode
+ // Update obj.connectivityByNode using obj.peerConnectivityByNode for the list of nodes in argument
+ obj.UpdateConnectivityState = function (nodeids) {
+ for (var nodeid in nodeids) {
+ var meshid = null, state = null, oldConnectivity = 0, oldPowerState = 0, newConnectivity = 0, newPowerState = 0;
+ var oldState = obj.connectivityByNode[nodeid];
+ if (oldState != null) { meshid = oldState.meshid; oldConnectivity = oldState.connectivity; oldPowerState = oldState.powerState; }
+ for (var serverid in obj.peerConnectivityByNode) {
+ var peerState = obj.peerConnectivityByNode[serverid][nodeid];
+ if (peerState != null) {
+ if (state == null) {
+ // Copy the state
+ state = {};
+ newConnectivity = state.connectivity = peerState.connectivity;
+ newPowerState = state.powerState = peerState.powerState;
+ meshid = state.meshid = peerState.meshid;
+ //if (peerState.agentPower) { state.agentPower = peerState.agentPower; }
+ //if (peerState.ciraPower) { state.ciraPower = peerState.ciraPower; }
+ //if (peerState.amtPower) { state.amtPower = peerState.amtPower; }
+ } else {
+ // Merge the state
+ state.connectivity |= peerState.connectivity;
+ newConnectivity = state.connectivity;
+ if ((peerState.powerState != 0) && ((state.powerState == 0) || (peerState.powerState < state.powerState))) { newPowerState = state.powerState = peerState.powerState; }
+ meshid = state.meshid = peerState.meshid;
+ //if (peerState.agentPower) { state.agentPower = peerState.agentPower; }
+ //if (peerState.ciraPower) { state.ciraPower = peerState.ciraPower; }
+ //if (peerState.amtPower) { state.amtPower = peerState.amtPower; }
+ }
+ }
+ }
+ obj.connectivityByNode[nodeid] = state;
+
+ //console.log('xx', nodeid, meshid, newConnectivity, oldPowerState, newPowerState, oldPowerState);
+
+ // Event any changes on this server only
+ if ((newConnectivity != oldPowerState) || (newPowerState != oldPowerState)) {
+ obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: newConnectivity, pwr: newPowerState, nolog: 1, nopeers: 1, id: Math.random() });
+ }
+ }
+ };
+
+ // See if we need to notifiy any user of device state change
+ obj.NotifyUserOfDeviceStateChange = function (meshid, nodeid, connectTime, connectType, powerState, serverid, stateSet, extraInfo) {
+ // Check if there is a email server for this domain
+ const meshSplit = meshid.split('/');
+ if (meshSplit.length != 3) return;
+ const domainId = meshSplit[1];
+ if (obj.config.domains[domainId] == null) return;
+ const mailserver = obj.config.domains[domainId].mailserver;
+ if ((mailserver == null) && (obj.msgserver == null)) return;
+
+ // Get the device group for this device
+ const mesh = obj.webserver.meshes[meshid];
+ if ((mesh == null) || (mesh.links == null)) return;
+
+ // Get the list of users that have visibility to this device
+ // This includes users that are part of user groups
+ const users = [];
+ for (var i in mesh.links) {
+ if (i.startsWith('user/') && (users.indexOf(i) < 0)) { users.push(i); }
+ if (i.startsWith('ugrp/')) {
+ var usergrp = obj.webserver.userGroups[i];
+ if (usergrp.links != null) { for (var j in usergrp.links) { if (j.startsWith('user/') && (users.indexOf(j) < 0)) { users.push(j); } } }
+ }
+ }
+
+ // Check if any user needs email notification
+ for (var i in users) {
+ const user = obj.webserver.users[users[i]];
+ if (user != null) {
+ var notify = 0;
+
+ // Device group notifications
+ const meshLinks = user.links[meshid];
+ if ((meshLinks != null) && (meshLinks.notify != null)) { notify |= meshLinks.notify; }
+
+ // User notifications
+ if (user.notify != null) {
+ if (user.notify[meshid] != null) { notify |= user.notify[meshid]; }
+ if (user.notify[nodeid] != null) { notify |= user.notify[nodeid]; }
+ }
+
+ // Email notifications
+ if ((user.email != null) && (user.emailVerified == true) && (mailserver != null) && ((notify & 48) != 0)) {
+ if (stateSet == true) {
+ if ((notify & 16) != 0) {
+ mailserver.notifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
+ } else {
+ mailserver.cancelNotifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
+ }
+ }
+ else if (stateSet == false) {
+ if ((notify & 32) != 0) {
+ mailserver.notifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
+ } else {
+ mailserver.cancelNotifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
+ }
+ }
+ }
+
+ // Messaging notifications
+ if ((obj.msgserver != null) && ((notify & 384) != 0)) {
+ if (stateSet == true) {
+ if ((notify & 128) != 0) {
+ obj.msgserver.notifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
+ } else {
+ obj.msgserver.cancelNotifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
+ }
+ }
+ else if (stateSet == false) {
+ if ((notify & 256) != 0) {
+ obj.msgserver.notifyDeviceDisconnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
+ } else {
+ obj.msgserver.cancelNotifyDeviceConnect(user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // See if we need to notifiy any user of device requested help
+ //if (typeof device.name == 'string') { parent.parent.NotifyUserOfDeviceHelpRequest(domain, device._id, device.meshid, device.name, command.msgArgs[0], command.msgArgs[1]); }
+
+ obj.NotifyUserOfDeviceHelpRequest = function (domain, meshid, nodeid, devicename, helpusername, helprequest) {
+ // Check if there is a email server for this domain
+ const meshSplit = meshid.split('/');
+ if (meshSplit.length != 3) return;
+ const domainId = meshSplit[1];
+ if (obj.config.domains[domainId] == null) return;
+ const mailserver = obj.config.domains[domainId].mailserver;
+ if ((mailserver == null) && (obj.msgserver == null)) return;
+
+ // Get the device group for this device
+ const mesh = obj.webserver.meshes[meshid];
+ if ((mesh == null) || (mesh.links == null)) return;
+
+ // Get the list of users that have visibility to this device
+ // This includes users that are part of user groups
+ const users = [];
+ for (var i in mesh.links) {
+ if (i.startsWith('user/') && (users.indexOf(i) < 0)) { users.push(i); }
+ if (i.startsWith('ugrp/')) {
+ var usergrp = obj.webserver.userGroups[i];
+ if (usergrp.links != null) { for (var j in usergrp.links) { if (j.startsWith('user/') && (users.indexOf(j) < 0)) { users.push(j); } } }
+ }
+ }
+
+ // Check if any user needs email notification
+ for (var i in users) {
+ const user = obj.webserver.users[users[i]];
+ if (user != null) {
+ var notify = 0;
+
+ // Device group notifications
+ const meshLinks = user.links[meshid];
+ if ((meshLinks != null) && (meshLinks.notify != null)) { notify |= meshLinks.notify; }
+
+ // User notifications
+ if (user.notify != null) {
+ if (user.notify[meshid] != null) { notify |= user.notify[meshid]; }
+ if (user.notify[nodeid] != null) { notify |= user.notify[nodeid]; }
+ }
+
+ // Mail help request
+ if ((user.email != null) && (user.emailVerified == true) && ((notify & 64) != 0)) { mailserver.sendDeviceHelpMail(domain, user.name, user.email, devicename, nodeid, helpusername, helprequest, user.llang); }
+
+ // Message help request
+ if ((user.msghandle != null) && ((notify & 512) != 0)) { obj.msgserver.sendDeviceHelpRequest(domain, user.name, user.msghandle, devicename, nodeid, helpusername, helprequest, user.llang); }
+ }
+ }
+ }
+
+ // Set the connectivity state of a node and setup the server so that messages can be routed correctly.
+ // meshId: mesh identifier of format mesh/domain/meshidhex
+ // nodeId: node identifier of format node/domain/nodeidhex
+ // connectTime: time of connection, milliseconds elapsed since the UNIX epoch.
+ // connectType: Bitmask, 1 = MeshAgent, 2 = Intel AMT CIRA, 4 = Intel AMT local, 8 = Intel AMT Relay, 16 = MQTT
+ // powerState: Value, 0 = Unknown, 1 = S0 power on, 2 = S1 Sleep, 3 = S2 Sleep, 4 = S3 Sleep, 5 = S4 Hibernate, 6 = S5 Soft-Off, 7 = Present, 8 = Off
+ //var connectTypeStrings = ['', 'MeshAgent', 'Intel AMT CIRA', '', 'Intel AMT local', '', '', '', 'Intel AMT Relay', '', '', '', '', '', '', '', 'MQTT'];
+ //var powerStateStrings = ['Unknown', 'Powered', 'Sleep', 'Sleep', 'Deep Sleep', 'Hibernating', 'Soft-Off', 'Present', 'Off'];
+ obj.SetConnectivityState = function (meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
+ //console.log('SetConnectivity for ' + nodeid.substring(0, 16) + ', Type: ' + connectTypeStrings[connectType] + ', Power: ' + powerStateStrings[powerState] + (serverid == null ? ('') : (', ServerId: ' + serverid)));
+ if ((serverid == null) && (obj.multiServer != null)) { obj.multiServer.DispatchMessage({ action: 'SetConnectivityState', meshid: meshid, nodeid: nodeid, connectTime: connectTime, connectType: connectType, powerState: powerState, extraInfo: extraInfo }); }
+
+ if (obj.multiServer == null) {
+ // Single server mode
+
+ // Change the node connection state
+ var eventConnectChange = 0;
+ var state = obj.connectivityByNode[nodeid];
+ if (state) {
+ // Change the connection in the node and mesh state lists
+ if ((state.connectivity & connectType) == 0) { state.connectivity |= connectType; eventConnectChange = 1; }
+ state.meshid = meshid;
+ } else {
+ // Add the connection to the node and mesh state list
+ obj.connectivityByNode[nodeid] = state = { connectivity: connectType, meshid: meshid };
+ eventConnectChange = 1;
+ }
+
+ // Set node power state
+ if (connectType == 1) { state.agentPower = powerState; } else if (connectType == 2) { state.ciraPower = powerState; } else if (connectType == 4) { state.amtPower = powerState; }
+ var powerState = 0, oldPowerState = state.powerState;
+ if ((state.connectivity & 1) != 0) { powerState = state.agentPower; } else if ((state.connectivity & 2) != 0) { powerState = state.ciraPower; } else if ((state.connectivity & 4) != 0) { powerState = state.amtPower; }
+ if ((state.powerState == null) || (state.powerState == undefined) || (state.powerState != powerState)) {
+ state.powerState = powerState;
+ eventConnectChange = 1;
+
+ // Set new power state in database
+ const record = { time: new Date(connectTime), nodeid: nodeid, power: powerState };
+ if (oldPowerState != null) { record.oldPower = oldPowerState; }
+ obj.db.storePowerEvent(record, obj.multiServer);
+ }
+
+ // Event the node connection change
+ if (eventConnectChange == 1) {
+ obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: state.connectivity, pwr: state.powerState, ct: connectTime, nolog: 1, nopeers: 1, id: Math.random() });
+
+ // Save indication of node connection change
+ const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 1, connectType: connectType };
+ if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
+ obj.db.Set(lc);
+
+ // Notify any users of device connection
+ obj.NotifyUserOfDeviceStateChange(meshid, nodeid, connectTime, connectType, powerState, serverid, true, extraInfo);
+ }
+ } else {
+ // Multi server mode
+
+ // Change the node connection state
+ if (serverid == null) { serverid = obj.serverId; }
+ if (obj.peerConnectivityByNode[serverid] == null) return; // Guard against unknown serverid's
+ var eventConnectChange = 0;
+ var state = obj.peerConnectivityByNode[serverid][nodeid];
+ if (state) {
+ // Change the connection in the node and mesh state lists
+ if ((state.connectivity & connectType) == 0) { state.connectivity |= connectType; eventConnectChange = 1; }
+ state.meshid = meshid;
+ } else {
+ // Add the connection to the node and mesh state list
+ obj.peerConnectivityByNode[serverid][nodeid] = state = { connectivity: connectType, meshid: meshid };
+ eventConnectChange = 1;
+ }
+
+ // Set node power state
+ if (connectType == 1) { state.agentPower = powerState; } else if (connectType == 2) { state.ciraPower = powerState; } else if (connectType == 4) { state.amtPower = powerState; }
+ var powerState = 0, oldPowerState = state.powerState;
+ if ((state.connectivity & 1) != 0) { powerState = state.agentPower; } else if ((state.connectivity & 2) != 0) { powerState = state.ciraPower; } else if ((state.connectivity & 4) != 0) { powerState = state.amtPower; }
+ if ((state.powerState == null) || (state.powerState == undefined) || (state.powerState != powerState)) {
+ state.powerState = powerState;
+ eventConnectChange = 1;
+
+ // Set new power state in database
+ var record = { time: new Date(connectTime), nodeid: nodeid, power: powerState, server: obj.multiServer.serverid };
+ if (oldPowerState != null) { record.oldPower = oldPowerState; }
+ obj.db.storePowerEvent(record, obj.multiServer);
+ }
+
+ if (eventConnectChange == 1) {
+ // Update the combined node state
+ var x = {}; x[nodeid] = 1;
+ obj.UpdateConnectivityState(x);
+
+ // Save indication of node connection change
+ if (serverid == obj.serverId) {
+ const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 1, connectType: connectType, serverid: obj.serverId };
+ if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
+ obj.db.Set(lc);
+ }
+
+ // Notify any users of device connection
+ obj.NotifyUserOfDeviceStateChange(meshid, nodeid, connectTime, connectType, powerState, serverid, true, extraInfo);
+ }
+ }
+ };
+
+ // Clear the connectivity state of a node and setup the server so that messages can be routed correctly.
+ // meshId: mesh identifier of format mesh/domain/meshidhex
+ // nodeId: node identifier of format node/domain/nodeidhex
+ // connectType: Bitmask, 1 = MeshAgent, 2 = Intel AMT CIRA, 4 = Intel AMT local.
+ obj.ClearConnectivityState = function (meshid, nodeid, connectType, serverid, extraInfo) {
+ //console.log('ClearConnectivity for ' + nodeid.substring(0, 16) + ', Type: ' + connectTypeStrings[connectType] + (serverid == null?(''):(', ServerId: ' + serverid)));
+ if ((serverid == null) && (obj.multiServer != null)) { obj.multiServer.DispatchMessage({ action: 'ClearConnectivityState', meshid: meshid, nodeid: nodeid, connectType: connectType, extraInfo: extraInfo }); }
+
+ if (obj.multiServer == null) {
+ // Single server mode
+ var eventConnectChange = 0;
+
+ // Remove the agent connection from the nodes connection list
+ const state = obj.connectivityByNode[nodeid];
+ if (state == null) return;
+
+ if ((state.connectivity & connectType) != 0) {
+ state.connectivity -= connectType;
+
+ // Save indication of node connection change
+ const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 0, connectType: connectType };
+ if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
+ obj.db.Set(lc);
+
+ // If the node is completely disconnected, clean it up completely
+ if (state.connectivity == 0) { delete obj.connectivityByNode[nodeid]; }
+ eventConnectChange = 1;
+ }
+
+ // Clear node power state
+ var powerState = 0;
+ const oldPowerState = state.powerState;
+ if (connectType == 1) { state.agentPower = 0; } else if (connectType == 2) { state.ciraPower = 0; } else if (connectType == 4) { state.amtPower = 0; }
+ if ((state.connectivity & 1) != 0) { powerState = state.agentPower; } else if ((state.connectivity & 2) != 0) { powerState = state.ciraPower; } else if ((state.connectivity & 4) != 0) { powerState = state.amtPower; }
+ if ((state.powerState == null) || (state.powerState != powerState)) {
+ state.powerState = powerState;
+ eventConnectChange = 1;
+
+ // Set new power state in database
+ obj.db.storePowerEvent({ time: new Date(), nodeid: nodeid, power: powerState, oldPower: oldPowerState }, obj.multiServer);
+ }
+
+ // Event the node connection change
+ if (eventConnectChange == 1) {
+ obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: state.connectivity, pwr: state.powerState, nolog: 1, nopeers: 1, id: Math.random() });
+
+ // Notify any users of device disconnection
+ obj.NotifyUserOfDeviceStateChange(meshid, nodeid, Date.now(), connectType, -1, serverid, false, extraInfo);
+ }
+ } else {
+ // Multi server mode
+
+ // Remove the agent connection from the nodes connection list
+ if (serverid == null) { serverid = obj.serverId; }
+ if (obj.peerConnectivityByNode[serverid] == null) return; // Guard against unknown serverid's
+ var state = obj.peerConnectivityByNode[serverid][nodeid];
+ if (state == null) return;
+
+ // If existing state exist, remove this connection
+ if ((state.connectivity & connectType) != 0) {
+ state.connectivity -= connectType; // Remove one connectivity mode
+
+ // Save indication of node connection change
+ if (serverid == obj.serverId) {
+ const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 0, connectType: connectType, serverid: obj.serverId };
+ if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; }
+ obj.db.Set(lc);
+ }
+
+ // If the node is completely disconnected, clean it up completely
+ if (state.connectivity == 0) { delete obj.peerConnectivityByNode[serverid][nodeid]; state.powerState = 0; }
+
+ // Notify any users of device disconnection
+ obj.NotifyUserOfDeviceStateChange(meshid, nodeid, Date.now(), connectType, -1, serverid, false, extraInfo);
+ }
+
+ // Clear node power state
+ if (connectType == 1) { state.agentPower = 0; } else if (connectType == 2) { state.ciraPower = 0; } else if (connectType == 4) { state.amtPower = 0; }
+ var powerState = 0;
+ if ((state.connectivity & 1) != 0) { powerState = state.agentPower; } else if ((state.connectivity & 2) != 0) { powerState = state.ciraPower; } else if ((state.connectivity & 4) != 0) { powerState = state.amtPower; }
+ if ((state.powerState == null) || (state.powerState != powerState)) { state.powerState = powerState; }
+
+ // Update the combined node state
+ var x = {}; x[nodeid] = 1;
+ obj.UpdateConnectivityState(x);
+ }
+ };
+
+ // Escape a code string
+ obj.escapeCodeString = function (str, keepUtf8) {
+ const escapeCodeStringTable = { '\'': '\\\'', '\"': '\\"', '\\': '\\\\', '\b': '\\b', '\f': '\\f', '\n': '\\n', '\r': '\\r', '\t': '\\t' };
+ var r = '', c, cr, table;
+ for (var i = 0; i < str.length; i++) {
+ c = str[i];
+ table = escapeCodeStringTable[c];
+ if (table != null) {
+ r += table;
+ } else if (keepUtf8 === true) {
+ r += c;
+ } else {
+ cr = c.charCodeAt(0);
+ if ((cr >= 32) && (cr <= 127)) { r += c; }
+ }
+ }
+ return r;
+ }
+
+ // Update the default mesh core
+ obj.updateMeshCore = function (func, dumpToFile) {
+ // Figure out where meshcore.js is
+ var meshcorePath = obj.datapath;
+ if (obj.fs.existsSync(obj.path.join(meshcorePath, 'meshcore.js')) == false) {
+ meshcorePath = obj.path.join(__dirname, 'agents');
+ if (obj.fs.existsSync(obj.path.join(meshcorePath, 'meshcore.js')) == false) {
+ obj.defaultMeshCores = obj.defaultMeshCoresHash = {}; if (func != null) { func(false); } // meshcore.js not found
+ }
+ }
+
+ // Read meshcore.js and all .js files in the modules folder.
+ var meshCore = null, modulesDir = null;
+ const modulesAdd = {
+ 'windows-amt': ['var addedModules = [];\r\n'],
+ 'linux-amt': ['var addedModules = [];\r\n'],
+ 'linux-noamt': ['var addedModules = [];\r\n']
+ };
+
+ // Read the recovery core if present
+ var meshRecoveryCore = null;
+ if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'recoverycore.js')) == true) {
+ try { meshRecoveryCore = obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'recoverycore.js')).toString(); } catch (ex) { }
+ if (meshRecoveryCore != null) {
+ modulesAdd['windows-recovery'] = ['var addedModules = [];\r\n'];
+ modulesAdd['linux-recovery'] = ['var addedModules = [];\r\n'];
+ }
+ }
+
+ // Read the agent recovery core if present
+ var meshAgentRecoveryCore = null;
+ if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'meshcore_diagnostic.js')) == true) {
+ try { meshAgentRecoveryCore = obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'meshcore_diagnostic.js')).toString(); } catch (ex) { }
+ if (meshAgentRecoveryCore != null) {
+ modulesAdd['windows-agentrecovery'] = ['var addedModules = [];\r\n'];
+ modulesAdd['linux-agentrecovery'] = ['var addedModules = [];\r\n'];
+ }
+ }
+
+ // Read the tiny core if present
+ var meshTinyCore = null;
+ if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'tinycore.js')) == true) {
+ try { meshTinyCore = obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'tinycore.js')).toString(); } catch (ex) { }
+ if (meshTinyCore != null) {
+ modulesAdd['windows-tiny'] = ['var addedModules = [];\r\n'];
+ modulesAdd['linux-tiny'] = ['var addedModules = [];\r\n'];
+ }
+ }
+
+ if (obj.args.minifycore !== false) { try { meshCore = obj.fs.readFileSync(obj.path.join(meshcorePath, 'meshcore.min.js')).toString(); } catch (ex) { } } // Favor minified meshcore if present.
+ if (meshCore == null) { try { meshCore = obj.fs.readFileSync(obj.path.join(meshcorePath, 'meshcore.js')).toString(); } catch (ex) { } } // Use non-minified meshcore.
+ if (meshCore != null) {
+ var moduleDirPath = null;
+ if (obj.args.minifycore !== false) { try { moduleDirPath = obj.path.join(meshcorePath, 'modules_meshcore_min'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Favor minified modules if present.
+ if (modulesDir == null) { try { moduleDirPath = obj.path.join(meshcorePath, 'modules_meshcore'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Use non-minified mofules.
+ if (modulesDir != null) {
+ for (var i in modulesDir) {
+ if (modulesDir[i].toLowerCase().endsWith('.json')) {
+ // We are adding a JSON file to the meshcores
+ var moduleName = modulesDir[i].substring(0, modulesDir[i].length - 5);
+ if (moduleName.endsWith('.min')) { moduleName = moduleName.substring(0, moduleName.length - 6); } // Remove the ".min" for ".min.json" files.
+ const jsonData = obj.escapeCodeString(obj.fs.readFileSync(obj.path.join(moduleDirPath, modulesDir[i])).toString('utf8'), true);
+ const moduleData = ['var ', moduleName, ' = JSON.parse(\'', jsonData, '\');\r\n'];
+
+ // Add to all major cores
+ modulesAdd['windows-amt'].push(...moduleData);
+ modulesAdd['linux-amt'].push(...moduleData);
+ modulesAdd['linux-noamt'].push(...moduleData);
+ }
+ if (modulesDir[i].toLowerCase().endsWith('.js')) {
+ // We are adding a JS file to the meshcores
+ var moduleName = modulesDir[i].substring(0, modulesDir[i].length - 3);
+ if (moduleName.endsWith('.min')) { moduleName = moduleName.substring(0, moduleName.length - 4); } // Remove the ".min" for ".min.js" files.
+ const moduleData = ['try { addModule("', moduleName, '", "', obj.escapeCodeString(obj.fs.readFileSync(obj.path.join(moduleDirPath, modulesDir[i])).toString('binary')), '"); addedModules.push("', moduleName, '"); } catch (ex) { }\r\n'];
+
+ // Merge this module
+ // NOTE: "smbios" module makes some non-AI Linux segfault, only include for IA platforms.
+ if (moduleName.startsWith('amt-') || (moduleName == 'smbios')) {
+ // Add to IA / Intel AMT cores only
+ modulesAdd['windows-amt'].push(...moduleData);
+ modulesAdd['linux-amt'].push(...moduleData);
+ } else if (moduleName.startsWith('win-')) {
+ // Add to Windows cores only
+ modulesAdd['windows-amt'].push(...moduleData);
+ } else if (moduleName.startsWith('linux-')) {
+ // Add to Linux cores only
+ modulesAdd['linux-amt'].push(...moduleData);
+ modulesAdd['linux-noamt'].push(...moduleData);
+ } else {
+ // Add to all cores
+ modulesAdd['windows-amt'].push(...moduleData);
+ modulesAdd['linux-amt'].push(...moduleData);
+ modulesAdd['linux-noamt'].push(...moduleData);
+ }
+
+ // Merge this module to recovery modules if needed
+ if (modulesAdd['windows-recovery'] != null) {
+ if ((moduleName == 'win-console') || (moduleName == 'win-message-pump') || (moduleName == 'win-terminal') || (moduleName == 'win-virtual-terminal')) {
+ modulesAdd['windows-recovery'].push(...moduleData);
+ }
+ }
+
+ // Merge this module to agent recovery modules if needed
+ if (modulesAdd['windows-agentrecovery'] != null) {
+ if ((moduleName == 'win-console') || (moduleName == 'win-message-pump') || (moduleName == 'win-terminal') || (moduleName == 'win-virtual-terminal')) {
+ modulesAdd['windows-agentrecovery'].push(...moduleData);
+ }
+ }
+ }
+ }
+ }
+
+ // Add plugins to cores
+ if (obj.pluginHandler) { obj.pluginHandler.addMeshCoreModules(modulesAdd); }
+
+ // If we need to dump modules to file, create a meshcores folder
+ if (dumpToFile) { try { obj.fs.mkdirSync('meshcores'); } catch (ex) { } }
+
+ // Merge the cores and compute the hashes
+ for (var i in modulesAdd) {
+ if ((i == 'windows-recovery') || (i == 'linux-recovery')) {
+ obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshRecoveryCore].join('');
+ } else if ((i == 'windows-agentrecovery') || (i == 'linux-agentrecovery')) {
+ obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshAgentRecoveryCore].join('');
+ } else if ((i == 'windows-tiny') || (i == 'linux-tiny')) {
+ obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshTinyCore].join('');
+ } else {
+ obj.defaultMeshCores[i] = [obj.common.IntToStr(0), ...modulesAdd[i], meshCore].join('');
+ }
+ obj.defaultMeshCores[i] = Buffer.from(obj.defaultMeshCores[i], 'utf8');
+ obj.defaultMeshCoresHash[i] = obj.crypto.createHash('sha384').update(obj.defaultMeshCores[i]).digest('binary');
+ obj.debug('main', 'Core module ' + i + ' is ' + obj.defaultMeshCores[i].length + ' bytes.');
+
+ // Write all modules to files. Great for debugging.
+ if (dumpToFile) {
+ console.log('Core module ' + i + ' is ' + obj.defaultMeshCores[i].length + ' bytes, saving to meshcores/' + i + '.js.'); // Print the core size and filename
+ obj.fs.writeFile('meshcores/' + i + '.js', obj.defaultMeshCores[i].slice(4), function () { }); // Write the core to file
+ }
+
+ // Compress the mesh cores with DEFLATE
+ const callback = function MeshCoreDeflateCb(err, buffer) { if (err == null) { obj.defaultMeshCoresDeflate[MeshCoreDeflateCb.i] = buffer; } }
+ callback.i = i;
+ require('zlib').deflate(obj.defaultMeshCores[i], { level: require('zlib').Z_BEST_COMPRESSION }, callback);
+ }
+ }
+
+ // We are done creating all the mesh cores.
+ if (func != null) { func(true); }
+ };
+
+ // Update the default meshcmd
+ obj.updateMeshCmdTimer = 'notset';
+ obj.updateMeshCmd = function (func) {
+ // Figure out where meshcmd.js is and read it.
+ var meshCmd = null, meshcmdPath, moduleAdditions = ['var addedModules = [];\r\n'], moduleDirPath, modulesDir = null;
+ if ((obj.args.minifycore !== false) && (obj.fs.existsSync(obj.path.join(obj.datapath, 'meshcmd.min.js')))) { meshcmdPath = obj.path.join(obj.datapath, 'meshcmd.min.js'); meshCmd = obj.fs.readFileSync(meshcmdPath).toString(); }
+ else if (obj.fs.existsSync(obj.path.join(obj.datapath, 'meshcmd.js'))) { meshcmdPath = obj.path.join(obj.datapath, 'meshcmd.js'); meshCmd = obj.fs.readFileSync(meshcmdPath).toString(); }
+ else if ((obj.args.minifycore !== false) && (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'meshcmd.min.js')))) { meshcmdPath = obj.path.join(__dirname, 'agents', 'meshcmd.min.js'); meshCmd = obj.fs.readFileSync(meshcmdPath).toString(); }
+ else if (obj.fs.existsSync(obj.path.join(__dirname, 'agents', 'meshcmd.js'))) { meshcmdPath = obj.path.join(__dirname, 'agents', 'meshcmd.js'); meshCmd = obj.fs.readFileSync(meshcmdPath).toString(); }
+ else { obj.defaultMeshCmd = null; if (func != null) { func(false); } return; } // meshcmd.js not found
+ meshCmd = meshCmd.replace("'***Mesh*Cmd*Version***'", '\'' + getCurrentVersion() + '\'');
+
+ // Figure out where the modules_meshcmd folder is.
+ if (obj.args.minifycore !== false) { try { moduleDirPath = obj.path.join(meshcmdPath, 'modules_meshcmd_min'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Favor minified modules if present.
+ if (modulesDir == null) { try { moduleDirPath = obj.path.join(meshcmdPath, 'modules_meshcmd'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Use non-minified mofules.
+ if (obj.args.minifycore !== false) { if (modulesDir == null) { try { moduleDirPath = obj.path.join(__dirname, 'agents', 'modules_meshcmd_min'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } } // Favor minified modules if present.
+ if (modulesDir == null) { try { moduleDirPath = obj.path.join(__dirname, 'agents', 'modules_meshcmd'); modulesDir = obj.fs.readdirSync(moduleDirPath); } catch (ex) { } } // Use non-minified mofules.
+
+ // Read all .js files in the meshcmd modules folder.
+ if (modulesDir != null) {
+ for (var i in modulesDir) {
+ if (modulesDir[i].toLowerCase().endsWith('.js')) {
+ // Merge this module
+ var moduleName = modulesDir[i].substring(0, modulesDir[i].length - 3);
+ if (moduleName.endsWith('.min')) { moduleName = moduleName.substring(0, moduleName.length - 4); } // Remove the ".min" for ".min.js" files.
+ moduleAdditions.push('try { addModule("', moduleName, '", "', obj.escapeCodeString(obj.fs.readFileSync(obj.path.join(moduleDirPath, modulesDir[i])).toString('binary')), '"); addedModules.push("', moduleName, '"); } catch (ex) { }\r\n');
+ }
+ }
+ }
+
+ // Set the new default meshcmd.js
+ moduleAdditions.push(meshCmd);
+ obj.defaultMeshCmd = moduleAdditions.join('');
+ //console.log('MeshCmd is ' + obj.defaultMeshCmd.length + ' bytes.'); // DEBUG, Print the merged meshcmd.js size
+ //obj.fs.writeFile("C:\\temp\\meshcmd.js", obj.defaultMeshCmd.substring(4)); // DEBUG, Write merged meshcmd.js to file
+ if (func != null) { func(true); }
+
+ // Monitor for changes in meshcmd.js
+ if (obj.updateMeshCmdTimer === 'notset') {
+ obj.updateMeshCmdTimer = null;
+ obj.fs.watch(meshcmdPath, function (eventType, filename) {
+ if (obj.updateMeshCmdTimer != null) { clearTimeout(obj.updateMeshCmdTimer); obj.updateMeshCmdTimer = null; }
+ obj.updateMeshCmdTimer = setTimeout(function () { obj.updateMeshCmd(); }, 5000);
+ });
+ }
+ };
+
+ // List of possible mesh agent install scripts
+ const meshToolsList = {
+ 'MeshCentralRouter': { localname: 'MeshCentralRouter.exe', dlname: 'winrouter' },
+ 'MeshCentralAssistant': { localname: 'MeshCentralAssistant.exe', dlname: 'winassistant', winhash: true }
+ //'MeshCentralRouterMacOS': { localname: 'MeshCentralRouter.dmg', dlname: 'MeshCentralRouter.dmg' }
+ };
+
+ // Update the list of available mesh agents
+ obj.updateMeshTools = function () {
+ for (var toolname in meshToolsList) {
+ if (meshToolsList[toolname].winhash === true) {
+ var toolpath = obj.path.join(__dirname, 'agents', meshToolsList[toolname].localname);
+ const toolpath2 = obj.path.join(obj.datapath, 'agents', meshToolsList[toolname].localname);
+ if (obj.fs.existsSync(toolpath2)) { toolpath = toolpath2; } // If the tool is present in "meshcentral-data/agents", use that one instead.
+
+ var hashStream = obj.crypto.createHash('sha384');
+ hashStream.toolname = toolname;
+ hashStream.toolpath = toolpath;
+ hashStream.dlname = meshToolsList[toolname].dlname;
+ hashStream.hashx = 0;
+ hashStream.on('data', function (data) {
+ obj.meshToolsBinaries[this.toolname] = { hash: data.toString('hex'), hashx: this.hashx, path: this.toolpath, dlname: this.dlname, url: this.url };
+ obj.meshToolsBinaries[this.toolname].url = 'https://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?meshaction=' + this.dlname;
+ var stats = null;
+ try { stats = obj.fs.statSync(this.toolpath); } catch (ex) { }
+ if (stats != null) { obj.meshToolsBinaries[this.toolname].size = stats.size; }
+ });
+ const options = { sourcePath: toolpath, targetStream: hashStream };
+ obj.exeHandler.hashExecutableFile(options);
+ } else {
+ var toolpath = obj.path.join(__dirname, 'agents', meshToolsList[toolname].localname);
+ const toolpath2 = obj.path.join(obj.datapath, 'agents', meshToolsList[toolname].localname);
+ if (obj.fs.existsSync(toolpath2)) { toolpath = toolpath2; } // If the tool is present in "meshcentral-data/agents", use that one instead.
+
+ var stream = null;
+ try {
+ stream = obj.fs.createReadStream(toolpath);
+ stream.on('data', function (data) { this.hash.update(data, 'binary'); this.hashx += data.length; });
+ stream.on('error', function (data) {
+ // If there is an error reading this file, make sure this agent is not in the agent table
+ if (obj.meshToolsBinaries[this.toolname] != null) { delete obj.meshToolsBinaries[this.toolname]; }
+ });
+ stream.on('end', function () {
+ // Add the agent to the agent table with all information and the hash
+ obj.meshToolsBinaries[this.toolname] = {};
+ obj.meshToolsBinaries[this.toolname].hash = this.hash.digest('hex');
+ obj.meshToolsBinaries[this.toolname].hashx = this.hashx;
+ obj.meshToolsBinaries[this.toolname].path = this.agentpath;
+ obj.meshToolsBinaries[this.toolname].dlname = this.dlname;
+ obj.meshToolsBinaries[this.toolname].url = 'https://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?meshaction=' + this.dlname;
+ var stats = null;
+ try { stats = obj.fs.statSync(this.agentpath); } catch (ex) { }
+ if (stats != null) { obj.meshToolsBinaries[this.toolname].size = stats.size; }
+ });
+ stream.toolname = toolname;
+ stream.agentpath = toolpath;
+ stream.dlname = meshToolsList[toolname].dlname;
+ stream.hash = obj.crypto.createHash('sha384', stream);
+ stream.hashx = 0;
+ } catch (ex) { }
+ }
+ }
+ };
+
+ // List of possible mesh agent install scripts
+ const meshAgentsInstallScriptList = {
+ 1: { id: 1, localname: 'meshinstall-linux.sh', rname: 'meshinstall.sh', linux: true },
+ 2: { id: 2, localname: 'meshinstall-initd.sh', rname: 'meshagent', linux: true },
+ 5: { id: 5, localname: 'meshinstall-bsd-rcd.sh', rname: 'meshagent', linux: true },
+ 6: { id: 6, localname: 'meshinstall-linux.js', rname: 'meshinstall.js', linux: true }
+ };
+
+ // Update the list of available mesh agents
+ obj.updateMeshAgentInstallScripts = function () {
+ for (var scriptid in meshAgentsInstallScriptList) {
+ var scriptpath = obj.path.join(__dirname, 'agents', meshAgentsInstallScriptList[scriptid].localname);
+ var stream = null;
+ try {
+ stream = obj.fs.createReadStream(scriptpath);
+ stream.xdata = '';
+ stream.on('data', function (data) { this.hash.update(data, 'binary'); this.xdata += data; });
+ stream.on('error', function (data) {
+ // If there is an error reading this file, make sure this agent is not in the agent table
+ if (obj.meshAgentInstallScripts[this.info.id] != null) { delete obj.meshAgentInstallScripts[this.info.id]; }
+ });
+ stream.on('end', function () {
+ // Add the agent to the agent table with all information and the hash
+ obj.meshAgentInstallScripts[this.info.id] = Object.assign({}, this.info);
+ obj.meshAgentInstallScripts[this.info.id].hash = this.hash.digest('hex');
+ obj.meshAgentInstallScripts[this.info.id].path = this.agentpath;
+ obj.meshAgentInstallScripts[this.info.id].data = this.xdata;
+ obj.meshAgentInstallScripts[this.info.id].url = 'https://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?script=' + this.info.id;
+ var stats = null;
+ try { stats = obj.fs.statSync(this.agentpath); } catch (ex) { }
+ if (stats != null) { obj.meshAgentInstallScripts[this.info.id].size = stats.size; }
+
+ // Place Unit line breaks on Linux scripts if not already present.
+ if (obj.meshAgentInstallScripts[this.info.id].linux === true) { obj.meshAgentInstallScripts[this.info.id].data = obj.meshAgentInstallScripts[this.info.id].data.split('\r\n').join('\n') }
+ });
+ stream.info = meshAgentsInstallScriptList[scriptid];
+ stream.agentpath = scriptpath;
+ stream.hash = obj.crypto.createHash('sha384', stream);
+ } catch (ex) { }
+ }
+ };
+
+ // List of possible mesh agents
+ obj.meshAgentsArchitectureNumbers = {
+ 0: { id: 0, localname: 'Unknown', rname: 'meshconsole.exe', desc: 'Unknown agent', update: false, amt: true, platform: 'unknown', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 1: { id: 1, localname: 'MeshConsole.exe', rname: 'meshconsole32.exe', desc: 'Windows x86-32 console', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
+ 2: { id: 2, localname: 'MeshConsole64.exe', rname: 'meshconsole64.exe', desc: 'Windows x86-64 console', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
+ 3: { id: 3, localname: 'MeshService.exe', rname: 'meshagent32.exe', desc: 'Windows x86-32 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', codesign: true },
+ 4: { id: 4, localname: 'MeshService64.exe', rname: 'meshagent64.exe', desc: 'Windows x86-64 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', codesign: true },
+ 5: { id: 5, localname: 'meshagent_x86', rname: 'meshagent', desc: 'Linux x86-32', update: true, amt: true, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 6: { id: 6, localname: 'meshagent_x86-64', rname: 'meshagent', desc: 'Linux x86-64', update: true, amt: true, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 7: { id: 7, localname: 'meshagent_mips', rname: 'meshagent', desc: 'Linux MIPS', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 8: { id: 8, localname: 'MeshAgent-Linux-XEN-x86-32', rname: 'meshagent', desc: 'XEN x86-64', update: true, amt: false, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 9: { id: 9, localname: 'meshagent_arm', rname: 'meshagent', desc: 'Linux ARM5', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 10: { id: 10, localname: 'MeshAgent-Linux-ARM-PlugPC', rname: 'meshagent', desc: 'Linux ARM PlugPC', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 11: { id: 11, localname: 'meshagent_osx-x86-32', rname: 'meshosx', desc: 'Apple macOS x86-32', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Apple x86-32 binary, no longer supported.
+ 12: { id: 12, localname: 'MeshAgent-Android-x86', rname: 'meshandroid', desc: 'Android x86-32', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 13: { id: 13, localname: 'meshagent_pogo', rname: 'meshagent', desc: 'Linux ARM PogoPlug', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 14: { id: 14, localname: 'meshagent_android.apk', rname: 'meshandroid.apk', desc: 'Android', update: false, amt: false, platform: 'android', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Get this one from Google Play
+ 15: { id: 15, localname: 'meshagent_poky', rname: 'meshagent', desc: 'Linux Poky x86-32', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 16: { id: 16, localname: 'meshagent_osx-x86-64', rname: 'meshagent', desc: 'Apple macOS x86-64', update: true, amt: false, platform: 'osx', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Apple x86-64 binary
+ 17: { id: 17, localname: 'MeshAgent-ChromeOS', rname: 'meshagent', desc: 'Google ChromeOS', update: false, amt: false, platform: 'chromeos', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Get this one from Chrome store
+ 18: { id: 18, localname: 'meshagent_poky64', rname: 'meshagent', desc: 'Linux Poky x86-64', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 19: { id: 19, localname: 'meshagent_x86_nokvm', rname: 'meshagent', desc: 'Linux x86-32 NoKVM', update: true, amt: true, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 20: { id: 20, localname: 'meshagent_x86-64_nokvm', rname: 'meshagent', desc: 'Linux x86-64 NoKVM', update: true, amt: true, platform: 'linux', core: 'linux-amt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 21: { id: 21, localname: 'MeshAgent-WinMinCore-Console-x86-32.exe', rname: 'meshagent.exe', desc: 'Windows MinCore Console x86-32', update: true, amt: false, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
+ 22: { id: 22, localname: 'MeshAgent-WinMinCore-Service-x86-64.exe', rname: 'meshagent.exe', desc: 'Windows MinCore Service x86-32', update: true, amt: false, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
+ 23: { id: 23, localname: 'MeshAgent-NodeJS', rname: 'meshagent', desc: 'NodeJS', update: false, amt: false, platform: 'node', core: 'nodejs', rcore: 'nodejs', arcore: 'nodejs', tcore: 'nodejs' }, // NodeJS based agent
+ 24: { id: 24, localname: 'meshagent_arm-linaro', rname: 'meshagent', desc: 'Linux ARM Linaro', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 25: { id: 25, localname: 'meshagent_armhf', rname: 'meshagent', desc: 'Linux ARM - HardFloat', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // "armv6l" and "armv7l"
+ 26: { id: 26, localname: 'meshagent_aarch64', rname: 'meshagent', desc: 'Linux ARM 64 bit (glibc/2.24)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // This is replaced by ARCHID 32
+ 27: { id: 27, localname: 'meshagent_armhf2', rname: 'meshagent', desc: 'Linux ARM - HardFloat', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Raspbian 7 2015-02-02 for old Raspberry Pi.
+ 28: { id: 28, localname: 'meshagent_mips24kc', rname: 'meshagent', desc: 'Linux MIPS24KC/MUSL (OpenWRT)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // MIPS Router with OpenWRT
+ 29: { id: 29, localname: 'meshagent_osx-arm-64', rname: 'meshagent', desc: 'Apple macOS ARM-64', update: true, amt: false, platform: 'osx', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Apple Silicon ARM 64bit
+ 30: { id: 30, localname: 'meshagent_freebsd_x86-64', rname: 'meshagent', desc: 'FreeBSD x86-64', update: true, amt: false, platform: 'freebsd', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // FreeBSD x64
+ 32: { id: 32, localname: 'meshagent_aarch64', rname: 'meshagent', desc: 'Linux ARM 64 bit (glibc/2.24)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' },
+ 33: { id: 33, localname: 'meshagent_openwrt_x86_64', rname: 'meshagent', desc: 'OpenWRT x86-64', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // This is replaced with ARCHID 36.
+ 34: { id: 34, localname: 'assistant_windows', rname: 'meshassistant', desc: 'MeshCentral Assistant (Windows)', update: false, amt: false, platform: 'assistant', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // MeshCentral Assistant for Windows
+ 35: { id: 35, localname: 'meshagent_linux-armada370-hf', rname: 'meshagent', desc: 'Armada370 - ARM32/HF (libc/2.26)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Armada370
+ 36: { id: 36, localname: 'meshagent_openwrt_x86_64', rname: 'meshagent', desc: 'OpenWRT x86-64', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // OpenWRT x86-64
+ 37: { id: 37, localname: 'meshagent_openbsd_x86-64', rname: 'meshagent', desc: 'OpenBSD x86-64', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // OpenBSD x86-64
+ 40: { id: 40, localname: 'meshagent_mipsel24kc', rname: 'meshagent', desc: 'Linux MIPSEL24KC (OpenWRT)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // MIPS Router with OpenWRT
+ 41: { id: 41, localname: 'meshagent_aarch64-cortex-a53', rname: 'meshagent', desc: 'ARMADA/CORTEX-A53/MUSL (OpenWRT)', update: true, amt: false, platform: 'linux', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // OpenWRT Routers
+ 42: { id: 42, localname: 'MeshConsoleARM64.exe', rname: 'meshconsolearm64.exe', desc: 'Windows ARM-64 console', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny' },
+ 43: { id: 43, localname: 'MeshServiceARM64.exe', rname: 'meshagentarm64.exe', desc: 'Windows ARM-64 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', codesign: true },
+ // 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)
+ 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
+ 10003: { id: 10003, localname: 'MeshService.exe', rname: 'meshagent32.exe', desc: 'Windows x86-32 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', unsigned: true },
+ 10004: { id: 10004, localname: 'MeshService64.exe', rname: 'meshagent64.exe', desc: 'Windows x86-64 service', update: true, amt: true, platform: 'win32', core: 'windows-amt', rcore: 'windows-recovery', arcore: 'windows-agentrecovery', tcore: 'windows-tiny', unsigned: true },
+ 10005: { id: 10005, localname: 'meshagent_osx-universal-64', rname: 'meshagent', desc: 'Apple macOS Universal Binary', update: true, amt: false, platform: 'osx', core: 'linux-noamt', rcore: 'linux-recovery', arcore: 'linux-agentrecovery', tcore: 'linux-tiny' }, // Apple Silicon + x86 universal binary
+ 10006: { id: 10006, localname: 'MeshCentralAssistant.exe', rname: 'MeshCentralAssistant.exe', desc: 'MeshCentral Assistant for Windows', update: false, amt: false, platform: 'win32' }, // MeshCentral Assistant
+ 11000: { id: 11000, localname: 'MeshCmd.exe', rname: 'MeshCmd.exe', desc: 'Windows x86-32 meshcmd', update: false, amt: true, platform: 'win32', codesign: true }, // MeshCMD for Windows x86 32-bit
+ 11001: { id: 11001, localname: 'MeshCmd64.exe', rname: 'MeshCmd64.exe', desc: 'Windows x86-64 meshcmd', update: false, amt: true, platform: 'win32', codesign: true }, // MeshCMD for Windows x86 64-bit
+ 11002: { id: 11002, localname: 'MeshCmdARM64.exe', rname: 'MeshCmdARM64.exe', desc: 'Windows ARM-64 meshcmd', update: false, amt: true, platform: 'win32', codesign: true } // MeshCMD for Windows ARM 64-bit
+ };
+
+ // Sign windows agents
+ obj.signMeshAgents = function (domain, func) {
+ // Setup the domain is specified
+ var objx = domain, suffix = '';
+ if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
+
+ // Check if a custom agent signing certificate is available
+ var agentSignCertInfo = require('./authenticode.js').loadCertificates([obj.path.join(obj.datapath, 'agentsigningcert.pem')]);
+
+ // If not using a custom signing cert, get agent code signature certificate ready with the full cert chain
+ if ((agentSignCertInfo == null) && (obj.certificates.codesign != null)) {
+ agentSignCertInfo = {
+ cert: obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.codesign.cert),
+ key: obj.certificateOperations.forge.pki.privateKeyFromPem(obj.certificates.codesign.key),
+ extraCerts: [obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.root.cert)]
+ }
+ }
+ if (agentSignCertInfo == null) { func(); return; } // No code signing certificate, nothing to do.
+
+ // Setup the domain is specified
+ var objx = domain, suffix = '';
+ if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
+
+ // Generate the agent signature description and URL
+ const serverSignedAgentsPath = obj.path.join(obj.datapath, 'signedagents' + suffix);
+ const signDesc = (domain.title ? domain.title : agentSignCertInfo.cert.subject.hash);
+ const httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
+ var httpsHost = ((domain.dns != null) ? domain.dns : obj.certificates.CommonName);
+ if (obj.args.agentaliasdns != null) { httpsHost = obj.args.agentaliasdns; }
+ var signUrl = 'https://' + httpsHost;
+ if (httpsPort != 443) { signUrl += ':' + httpsPort; }
+ var xdomain = (domain.dns == null) ? domain.id : '';
+ if (xdomain != '') xdomain += '/';
+ signUrl += '/' + xdomain;
+
+ // If requested, lock the agent to this server
+ if (obj.config.settings.agentsignlock) { signUrl += '?ServerID=' + obj.certificateOperations.getPublicKeyHash(obj.certificates.agent.cert).toUpperCase(); }
+
+ // Setup the time server
+ var timeStampUrl = 'http://timestamp.comodoca.com/authenticode';
+ if (obj.args.agenttimestampserver === false) { timeStampUrl = null; }
+ else if (typeof obj.args.agenttimestampserver == 'string') { timeStampUrl = obj.args.agenttimestampserver; }
+
+ // Setup the time server proxy
+ var timeStampProxy = null;
+ if (typeof obj.args.agenttimestampproxy == 'string') { timeStampProxy = obj.args.agenttimestampproxy; }
+ else if ((obj.args.agenttimestampproxy !== false) && (typeof obj.args.npmproxy == 'string')) { timeStampProxy = obj.args.npmproxy; }
+
+ // Setup the pending operations counter
+ var pendingOperations = 1;
+
+ for (var archid in obj.meshAgentsArchitectureNumbers) {
+ if (obj.meshAgentsArchitectureNumbers[archid].codesign !== true) continue;
+
+ var agentpath;
+ if (domain.id == '') {
+ // Load all agents when processing the default domain
+ agentpath = obj.path.join(__dirname, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
+ var agentpath2 = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
+ if (obj.fs.existsSync(agentpath2)) { agentpath = agentpath2; delete obj.meshAgentsArchitectureNumbers[archid].codesign; } // If the agent is present in "meshcentral-data/agents", use that one instead.
+ } else {
+ // When processing an extra domain, only load agents that are specific to that domain
+ agentpath = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
+ if (obj.fs.existsSync(agentpath)) { delete obj.meshAgentsArchitectureNumbers[archid].codesign; } else { continue; } // If the agent is not present in "meshcentral-data/agents" skip.
+ }
+
+ // Open the original agent with authenticode
+ const signeedagentpath = obj.path.join(serverSignedAgentsPath, obj.meshAgentsArchitectureNumbers[archid].localname);
+ const originalAgent = require('./authenticode.js').createAuthenticodeHandler(agentpath);
+ if (originalAgent != null) {
+ // Check if the agent is already signed correctly
+ const destinationAgent = require('./authenticode.js').createAuthenticodeHandler(signeedagentpath);
+ var destinationAgentOk = (
+ (destinationAgent != null) &&
+ (destinationAgent.fileHashSigned != null) &&
+ (Buffer.compare(destinationAgent.fileHashSigned, destinationAgent.fileHashActual) == 0) &&
+ (destinationAgent.signingAttribs.indexOf(signUrl) >= 0) &&
+ (destinationAgent.signingAttribs.indexOf(signDesc) >= 0)
+ );
+
+ if (destinationAgent != null) {
+ // If the agent is signed correctly, look to see if the resources in the destination agent are correct
+ var orgVersionStrings = originalAgent.getVersionInfo();
+ if (destinationAgentOk == true) {
+ const versionStrings = destinationAgent.getVersionInfo();
+ const versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
+ for (var i in versionProperties) {
+ const prop = versionProperties[i], propl = prop.toLowerCase();
+ if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo[propl] == 'string')) {
+ if (domain.agentfileinfo[propl] != versionStrings[prop]) { destinationAgentOk = false; break; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
+ } else {
+ if (orgVersionStrings[prop] != versionStrings[prop]) { destinationAgentOk = false; break; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
+ }
+ }
+
+ // Check file version number
+ if (destinationAgentOk == true) {
+ if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo['fileversionnumber'] == 'string')) {
+ if (domain.agentfileinfo['fileversionnumber'] != versionStrings['~FileVersion']) { destinationAgentOk = false; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
+ } else {
+ if (orgVersionStrings['~FileVersion'] != versionStrings['~FileVersion']) { destinationAgentOk = false; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
+ }
+ }
+
+ // Check product version number
+ if (destinationAgentOk == true) {
+ if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo['productversionnumber'] == 'string')) {
+ if (domain.agentfileinfo['productversionnumber'] != versionStrings['~ProductVersion']) { destinationAgentOk = false; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
+ } else {
+ if (orgVersionStrings['~ProductVersion'] != versionStrings['~ProductVersion']) { destinationAgentOk = false; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
+ }
+ }
+
+ // Check the agent icon
+ if (destinationAgentOk == true) {
+ if ((domain.agentfileinfo != null) && (domain.agentfileinfo.icon != null)) {
+ // Check if the destination agent matches the icon we want
+ const agentIconGroups = destinationAgent.getIconInfo();
+ if (agentIconGroups != null) {
+ const agentIconGroupNames = Object.keys(agentIconGroups);
+ if (agentIconGroupNames.length > 0) {
+ const agentMainIconGroup = agentIconGroups[agentIconGroupNames[0]];
+ if (agentMainIconGroup.resCount != domain.agentfileinfo.icon.resCount) {
+ destinationAgentOk = false; // The icon image count is different, don't bother hashing to see if the icons are different.
+ } else {
+ const agentMainIconGroupHash = require('./authenticode.js').hashObject(agentMainIconGroup);
+ const iconHash = require('./authenticode.js').hashObject(domain.agentfileinfo.icon);
+ if (agentMainIconGroupHash != iconHash) { destinationAgentOk = false; } // If the existing agent icon does not match the desired icon, we need to re-sign the agent.
+ }
+ }
+ }
+ } else {
+ // Check if the destination agent has the default icon
+ const agentIconGroups1 = destinationAgent.getIconInfo();
+ const agentIconGroups2 = originalAgent.getIconInfo();
+ if (agentIconGroups1.resCount != agentIconGroups2.resCount) {
+ destinationAgentOk = false; // The icon image count is different, don't bother hashing to see if the icons are different.
+ } else {
+ const iconHash1 = require('./authenticode.js').hashObject(agentIconGroups1);
+ const iconHash2 = require('./authenticode.js').hashObject(agentIconGroups2);
+ if (iconHash1 != iconHash2) { destinationAgentOk = false; } // If the existing agent icon does not match the desired icon, we need to re-sign the agent.
+ }
+ }
+ }
+
+ // Check the agent logo
+ if (destinationAgentOk == true) {
+ if ((domain.agentfileinfo != null) && (domain.agentfileinfo.logo != null)) {
+ // Check if the destination agent matches the logo we want
+ const agentBitmaps = destinationAgent.getBitmapInfo();
+ if (agentBitmaps != null) {
+ const agentBitmapNames = Object.keys(agentBitmaps);
+ if (agentBitmapNames.length > 0) {
+ const agentMainBitmap = agentBitmaps[agentBitmapNames[0]];
+ const agentMainBitmapHash = require('./authenticode.js').hashObject(agentMainBitmap);
+ const bitmapHash = require('./authenticode.js').hashObject(domain.agentfileinfo.logo);
+ if (agentMainBitmapHash != bitmapHash) { destinationAgentOk = false; } // If the existing agent logo does not match the desired logo, we need to re-sign the agent.
+ }
+ }
+ } else {
+ // Check if the destination agent has the default icon
+ const agentBitmaps1 = destinationAgent.getBitmapInfo();
+ const agentBitmaps2 = originalAgent.getBitmapInfo();
+ const agentBitmapNames = Object.keys(agentBitmaps1);
+ if (agentBitmapNames.length == 0) {
+ destinationAgentOk = false;
+ } else {
+ const iconHash1 = require('./authenticode.js').hashObject(agentBitmaps1[agentBitmapNames[0]]);
+ const iconHash2 = require('./authenticode.js').hashObject(agentBitmaps2[agentBitmapNames[0]]);
+ if (iconHash1 != iconHash2) { destinationAgentOk = false; } // If the existing agent icon does not match the desired icon, we need to re-sign the agent.
+ }
+ }
+ }
+ }
+
+ // If everything looks ok, runs a hash of the original and destination agent .text, .data and .rdata sections. If different, sign the agent again.
+ if ((destinationAgentOk == true) && (originalAgent.getHashOfSection('sha384', '.text').compare(destinationAgent.getHashOfSection('sha384', '.text')) != 0)) { destinationAgentOk = false; }
+ if ((destinationAgentOk == true) && (originalAgent.getHashOfSection('sha384', '.data').compare(destinationAgent.getHashOfSection('sha384', '.data')) != 0)) { destinationAgentOk = false; }
+ if ((destinationAgentOk == true) && (originalAgent.getHashOfSection('sha384', '.rdata').compare(destinationAgent.getHashOfSection('sha384', '.rdata')) != 0)) { destinationAgentOk = false; }
+
+ // We are done comparing the destination agent, close it.
+ destinationAgent.close();
+ }
+
+ if (destinationAgentOk == false) {
+ // If not signed correctly, sign it. First, create the server signed agent folder if needed
+ try { obj.fs.mkdirSync(serverSignedAgentsPath); } catch (ex) { }
+ const xagentSignedFunc = function agentSignedFunc(err, size) {
+ if (err == null) {
+ // Agent was signed succesfuly
+ console.log(obj.common.format('Code signed {0}.', agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname));
+ } else {
+ // Failed to sign agent
+ addServerWarning('Failed to sign \"' + agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname + '\": ' + err, 22, [agentSignedFunc.objx.meshAgentsArchitectureNumbers[agentSignedFunc.archid].localname, err]);
+ }
+ obj.callExternalSignJob(agentSignedFunc.signingArguments); // Call external signing job regardless of success or failure
+ if (--pendingOperations === 0) { agentSignedFunc.func(); }
+ }
+ pendingOperations++;
+ xagentSignedFunc.func = func;
+ xagentSignedFunc.objx = objx;
+ xagentSignedFunc.archid = archid;
+ xagentSignedFunc.signeedagentpath = signeedagentpath;
+
+ // Parse the resources in the executable and make any required changes
+ var resChanges = false, versionStrings = null;
+ if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object')) {
+ versionStrings = originalAgent.getVersionInfo();
+ var versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
+ // Change the agent string properties
+ for (var i in versionProperties) {
+ const prop = versionProperties[i], propl = prop.toLowerCase();
+ if (domain.agentfileinfo[propl] && (domain.agentfileinfo[propl] != versionStrings[prop])) { versionStrings[prop] = domain.agentfileinfo[propl]; resChanges = true; }
+ }
+ // Change the agent file version
+ if (domain.agentfileinfo['fileversionnumber'] && (domain.agentfileinfo['fileversionnumber'] != versionStrings['~FileVersion'])) {
+ versionStrings['~FileVersion'] = domain.agentfileinfo['fileversionnumber']; resChanges = true;
+ }
+ // Change the agent product version
+ if (domain.agentfileinfo['productversionnumber'] && (domain.agentfileinfo['productversionnumber'] != versionStrings['~ProductVersion'])) {
+ versionStrings['~ProductVersion'] = domain.agentfileinfo['productversionnumber']; resChanges = true;
+ }
+ if (resChanges == true) { originalAgent.setVersionInfo(versionStrings); }
+
+ // Change the agent icon
+ if (domain.agentfileinfo.icon != null) {
+ const agentIconGroups = originalAgent.getIconInfo();
+ if (agentIconGroups != null) {
+ const agentIconGroupNames = Object.keys(agentIconGroups);
+ if (agentIconGroupNames.length > 0) {
+ const agentMainIconGroupName = agentIconGroupNames[0];
+ agentIconGroups[agentIconGroupNames[0]] = domain.agentfileinfo.icon;
+ originalAgent.setIconInfo(agentIconGroups);
+ }
+ }
+ }
+
+ // Change the agent logo
+ if (domain.agentfileinfo.logo != null) {
+ const agentBitmaps = originalAgent.getBitmapInfo();
+ if (agentBitmaps != null) {
+ const agentBitmapNames = Object.keys(agentBitmaps);
+ if (agentBitmapNames.length > 0) {
+ agentBitmaps[agentBitmapNames[0]] = domain.agentfileinfo.logo;
+ originalAgent.setBitmapInfo(agentBitmaps);
+ }
+ }
+ }
+ }
+
+ const signingArguments = { out: signeedagentpath, desc: signDesc, url: signUrl, time: timeStampUrl, proxy: timeStampProxy }; // Shallow clone
+ signingArguments.resChanges = resChanges;
+
+ obj.debug('main', "Code signing with arguments: " + JSON.stringify(signingArguments));
+ xagentSignedFunc.signingArguments = signingArguments; // Attach the signing arguments to the callback function
+ if (resChanges == false) {
+ // Sign the agent the simple way, without changing any resources.
+ originalAgent.sign(agentSignCertInfo, signingArguments, xagentSignedFunc);
+ } else {
+ // Change the agent resources and sign the agent, this is a much more involved process.
+ // NOTE: This is experimental and could corupt the agent.
+ originalAgent.writeExecutable(signingArguments, agentSignCertInfo, xagentSignedFunc);
+ }
+
+ } else {
+ // Signed agent is already ok, use it.
+ originalAgent.close();
+ }
+
+
+ }
+ }
+
+ if (--pendingOperations === 0) { func(); }
+ }
+
+ obj.callExternalSignJob = function (signingArguments) {
+ if (obj.config.settings && !obj.config.settings.externalsignjob) {
+ return;
+ }
+ obj.debug('main', "External signing job called for file: " + signingArguments.out);
+
+ const { spawnSync } = require('child_process');
+
+ const signResult = spawnSync('"' + obj.config.settings.externalsignjob + '"', ['"' + signingArguments.out + '"'], {
+ encoding: 'utf-8',
+ shell: true,
+ stdio: 'inherit'
+ });
+
+ if (signResult.error || signResult.status !== 0) {
+ obj.debug('main', "External signing failed for file: " + signingArguments.out);
+ console.error("External signing failed for file: " + signingArguments.out);
+ return;
+ }
+ }
+
+ // Update the list of available mesh agents
+ obj.updateMeshAgentsTable = function (domain, func) {
+ // Check if a custom agent signing certificate is available
+ var agentSignCertInfo = require('./authenticode.js').loadCertificates([obj.path.join(obj.datapath, 'agentsigningcert.pem')]);
+
+ // If not using a custom signing cert, get agent code signature certificate ready with the full cert chain
+ if ((agentSignCertInfo == null) && (obj.certificates.codesign != null)) {
+ agentSignCertInfo = {
+ cert: obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.codesign.cert),
+ key: obj.certificateOperations.forge.pki.privateKeyFromPem(obj.certificates.codesign.key),
+ extraCerts: [obj.certificateOperations.forge.pki.certificateFromPem(obj.certificates.root.cert)]
+ }
+ }
+
+ // Setup the domain is specified
+ var objx = domain, suffix = '';
+ if (domain.id == '') { objx = obj; } else { suffix = '-' + domain.id; objx.meshAgentBinaries = {}; }
+
+ // Load agent information file. This includes the data & time of the agent.
+ const agentInfo = [];
+ try { agentInfo = JSON.parse(obj.fs.readFileSync(obj.path.join(__dirname, 'agents', 'hashagents.json'), 'utf8')); } catch (ex) { }
+
+ var archcount = 0;
+ for (var archid in obj.meshAgentsArchitectureNumbers) {
+ var agentpath;
+ if (domain.id == '') {
+ // Load all agents when processing the default domain
+ agentpath = obj.path.join(__dirname, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
+ if (obj.meshAgentsArchitectureNumbers[archid].unsigned !== true) {
+ const agentpath2 = obj.path.join(obj.datapath, 'signedagents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
+ if (obj.fs.existsSync(agentpath2)) { agentpath = agentpath2; } // If the agent is present in "meshcentral-data/signedagents", use that one instead.
+ const agentpath3 = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
+ if (obj.fs.existsSync(agentpath3)) { agentpath = agentpath3; } // If the agent is present in "meshcentral-data/agents", use that one instead.
+ }
+ } else {
+ // When processing an extra domain, only load agents that are specific to that domain
+ agentpath = obj.path.join(obj.datapath, 'agents' + suffix, obj.meshAgentsArchitectureNumbers[archid].localname);
+ if (obj.fs.existsSync(agentpath)) { delete obj.meshAgentsArchitectureNumbers[archid].codesign; } else { continue; } // If the agent is not present in "meshcentral-data/agents" skip.
+ }
+
+ // Fetch agent binary information
+ var stats = null;
+ try { stats = obj.fs.statSync(agentpath); } catch (ex) { }
+ if ((stats == null)) continue; // If this agent does not exist, skip it.
+
+ // Setup agent information
+ archcount++;
+ objx.meshAgentBinaries[archid] = Object.assign({}, obj.meshAgentsArchitectureNumbers[archid]);
+ objx.meshAgentBinaries[archid].path = agentpath;
+ objx.meshAgentBinaries[archid].url = 'http://' + obj.certificates.CommonName + ':' + ((typeof obj.args.aliasport == 'number') ? obj.args.aliasport : obj.args.port) + '/meshagents?id=' + archid;
+ objx.meshAgentBinaries[archid].size = stats.size;
+ if ((agentInfo[archid] != null) && (agentInfo[archid].mtime != null)) { objx.meshAgentBinaries[archid].mtime = new Date(agentInfo[archid].mtime); } // Set agent time if available
+
+ // If this is a windows binary, pull binary information
+ if (obj.meshAgentsArchitectureNumbers[archid].platform == 'win32') {
+ try { objx.meshAgentBinaries[archid].pe = obj.exeHandler.parseWindowsExecutable(agentpath); } catch (ex) { }
+ }
+
+ // If agents must be stored in RAM or if this is a Windows 32/64 agent, load the agent in RAM.
+ if ((obj.args.agentsinram === true) || (((archid == 3) || (archid == 4)) && (obj.args.agentsinram !== false))) {
+ if ((archid == 3) || (archid == 4)) {
+ // Load the agent with a random msh added to it.
+ const outStream = new require('stream').Duplex();
+ outStream.meshAgentBinary = objx.meshAgentBinaries[archid];
+ if (agentSignCertInfo) { outStream.meshAgentBinary.randomMsh = agentSignCertInfo.cert.subject.hash; } else { outStream.meshAgentBinary.randomMsh = obj.crypto.randomBytes(16).toString('hex'); }
+ outStream.bufferList = [];
+ outStream._write = function (chunk, encoding, callback) { this.bufferList.push(chunk); if (callback) callback(); }; // Append the chuck.
+ outStream._read = function (size) { }; // Do nothing, this is not going to be called.
+ outStream.on('finish', function () {
+ // Merge all chunks
+ this.meshAgentBinary.data = Buffer.concat(this.bufferList);
+ this.meshAgentBinary.size = this.meshAgentBinary.data.length;
+ delete this.bufferList;
+
+ // Hash the uncompressed binary
+ const hash = obj.crypto.createHash('sha384').update(this.meshAgentBinary.data);
+ this.meshAgentBinary.fileHash = hash.digest('binary');
+ this.meshAgentBinary.fileHashHex = Buffer.from(this.meshAgentBinary.fileHash, 'binary').toString('hex');
+
+ // Compress the agent using ZIP
+ const archive = require('archiver')('zip', { level: 9 }); // Sets the compression method.
+ const onZipData = function onZipData(buffer) { onZipData.x.zacc.push(buffer); }
+ const onZipEnd = function onZipEnd() {
+ // Concat all the buffer for create compressed zip agent
+ const concatData = Buffer.concat(onZipData.x.zacc);
+ delete onZipData.x.zacc;
+
+ // Hash the compressed binary
+ const hash = obj.crypto.createHash('sha384').update(concatData);
+ onZipData.x.zhash = hash.digest('binary');
+ onZipData.x.zhashhex = Buffer.from(onZipData.x.zhash, 'binary').toString('hex');
+
+ // Set the agent
+ onZipData.x.zdata = concatData;
+ onZipData.x.zsize = concatData.length;
+ }
+ const onZipError = function onZipError() { delete onZipData.x.zacc; }
+ this.meshAgentBinary.zacc = [];
+ onZipData.x = this.meshAgentBinary;
+ onZipEnd.x = this.meshAgentBinary;
+ onZipError.x = this.meshAgentBinary;
+ archive.on('data', onZipData);
+ archive.on('end', onZipEnd);
+ archive.on('error', onZipError);
+
+ // Starting with NodeJS v16, passing in a buffer at archive.append() will result a compressed file with zero byte length. To fix this, we pass in the buffer as a stream.
+ // archive.append(this.meshAgentBinary.data, { name: 'meshagent' }); // This is the version that does not work on NodeJS v16.
+ const ReadableStream = require('stream').Readable;
+ const zipInputStream = new ReadableStream();
+ zipInputStream.push(this.meshAgentBinary.data);
+ zipInputStream.push(null);
+ archive.append(zipInputStream, { name: 'meshagent' });
+
+ archive.finalize();
+ })
+ obj.exeHandler.streamExeWithMeshPolicy(
+ {
+ platform: 'win32',
+ sourceFileName: agentpath,
+ destinationStream: outStream,
+ randomPolicy: true, // Indicates that the msh policy is random data.
+ msh: outStream.meshAgentBinary.randomMsh,
+ peinfo: objx.meshAgentBinaries[archid].pe
+ });
+ } else {
+ // Load the agent as-is
+ objx.meshAgentBinaries[archid].data = obj.fs.readFileSync(agentpath);
+
+ // Compress the agent using ZIP
+ const archive = require('archiver')('zip', { level: 9 }); // Sets the compression method.
+
+ const onZipData = function onZipData(buffer) { onZipData.x.zacc.push(buffer); }
+ const onZipEnd = function onZipEnd() {
+ // Concat all the buffer for create compressed zip agent
+ const concatData = Buffer.concat(onZipData.x.zacc);
+ delete onZipData.x.zacc;
+
+ // Hash the compressed binary
+ const hash = obj.crypto.createHash('sha384').update(concatData);
+ onZipData.x.zhash = hash.digest('binary');
+ onZipData.x.zhashhex = Buffer.from(onZipData.x.zhash, 'binary').toString('hex');
+
+ // Set the agent
+ onZipData.x.zdata = concatData;
+ onZipData.x.zsize = concatData.length;
+
+ //console.log('Packed', onZipData.x.size, onZipData.x.zsize);
+ }
+ const onZipError = function onZipError() { delete onZipData.x.zacc; }
+ objx.meshAgentBinaries[archid].zacc = [];
+ onZipData.x = objx.meshAgentBinaries[archid];
+ onZipEnd.x = objx.meshAgentBinaries[archid];
+ onZipError.x = objx.meshAgentBinaries[archid];
+ archive.on('data', onZipData);
+ archive.on('end', onZipEnd);
+ archive.on('error', onZipError);
+ archive.append(objx.meshAgentBinaries[archid].data, { name: 'meshagent' });
+ archive.finalize();
+ }
+ }
+
+ // Hash the binary
+ const hashStream = obj.crypto.createHash('sha384');
+ hashStream.archid = archid;
+ hashStream.on('data', function (data) {
+ objx.meshAgentBinaries[this.archid].hash = data.toString('binary');
+ objx.meshAgentBinaries[this.archid].hashhex = data.toString('hex');
+ if ((--archcount == 0) && (func != null)) { func(); }
+ });
+ const options = { sourcePath: agentpath, targetStream: hashStream, platform: obj.meshAgentsArchitectureNumbers[archid].platform };
+ if (objx.meshAgentBinaries[archid].pe != null) { options.peinfo = objx.meshAgentBinaries[archid].pe; }
+ obj.exeHandler.hashExecutableFile(options);
+
+ // If we are not loading Windows binaries to RAM, compute the RAW file hash of the signed binaries here.
+ if ((obj.args.agentsinram === false) && ((archid == 3) || (archid == 4))) {
+ const hash = obj.crypto.createHash('sha384').update(obj.fs.readFileSync(agentpath));
+ objx.meshAgentBinaries[archid].fileHash = hash.digest('binary');
+ objx.meshAgentBinaries[archid].fileHashHex = Buffer.from(objx.meshAgentBinaries[archid].fileHash, 'binary').toString('hex');
+ }
+ }
+ };
+
+ // Generate a time limited user login token
+ obj.getLoginToken = function (userid, func) {
+ if ((userid == null) || (typeof userid != 'string')) { func('Invalid userid.'); return; }
+ const x = userid.split('/');
+ if (x == null || x.length != 3 || x[0] != 'user') { func('Invalid userid.'); return; }
+ obj.db.Get(userid, function (err, docs) {
+ if (err != null || docs == null || docs.length == 0) {
+ func('User ' + userid + ' not found.'); return;
+ } else {
+ // Load the login cookie encryption key from the database
+ obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
+ if ((docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 160)) {
+ // Key is present, use it.
+ obj.loginCookieEncryptionKey = Buffer.from(docs[0].key, 'hex');
+ func(obj.encodeCookie({ u: userid, a: 3 }, obj.loginCookieEncryptionKey));
+ } else {
+ // Key is not present, generate one.
+ obj.loginCookieEncryptionKey = obj.generateCookieKey();
+ obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() }, function () { func(obj.encodeCookie({ u: userid, a: 3 }, obj.loginCookieEncryptionKey)); });
+ }
+ });
+ }
+ });
+ };
+
+ // Show the user login token generation key
+ obj.showLoginTokenKey = function (func) {
+ // Load the login cookie encryption key from the database
+ obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
+ if ((docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 160)) {
+ // Key is present, use it.
+ func(docs[0].key);
+ } else {
+ // Key is not present, generate one.
+ obj.loginCookieEncryptionKey = obj.generateCookieKey();
+ obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() }, function () { func(obj.loginCookieEncryptionKey.toString('hex')); });
+ }
+ });
+ };
+
+ // Load the list of Intel AMT UUID and passwords from "amtactivation.log"
+ obj.loadAmtActivationLogPasswords = function (func) {
+ const amtlogfilename = obj.path.join(obj.datapath, 'amtactivation.log');
+ obj.fs.readFile(amtlogfilename, 'utf8', function (err, data) {
+ const amtPasswords = {}; // UUID --> [Passwords]
+ if ((err == null) && (data != null)) {
+ const lines = data.split('\n');
+ for (var i in lines) {
+ const line = lines[i];
+ if (line.startsWith('{')) {
+ var j = null;
+ try { j = JSON.parse(line); } catch (ex) { }
+ if ((j != null) && (typeof j == 'object')) {
+ if ((typeof j.amtUuid == 'string') && (typeof j.password == 'string')) {
+ if (amtPasswords[j.amtUuid] == null) {
+ amtPasswords[j.amtUuid] = [j.password]; // Add password to array
+ } else {
+ amtPasswords[j.amtUuid].unshift(j.password); // Add password at the start of the array
+ }
+ }
+ }
+ }
+ }
+ // Remove all duplicates and only keep the 3 last passwords for any given device
+ for (var i in amtPasswords) {
+ amtPasswords[i] = [...new Set(amtPasswords[i])];
+ while (amtPasswords[i].length > 3) { amtPasswords[i].pop(); }
+ }
+ }
+ func(obj.common.sortObj(amtPasswords)); // Sort by UUID
+ });
+ }
+
+ // Encrypt session data
+ obj.encryptSessionData = function (data, key) {
+ if (data == null) return null;
+ if (key == null) { key = obj.loginCookieEncryptionKey; }
+ try {
+ const iv = Buffer.from(obj.crypto.randomBytes(12), 'binary'), cipher = obj.crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv);
+ const crypted = Buffer.concat([cipher.update(JSON.stringify(data), 'utf8'), cipher.final()]);
+ return Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString(obj.args.cookieencoding ? obj.args.cookieencoding : 'base64').replace(/\+/g, '@').replace(/\//g, '$');
+ } catch (ex) { return null; }
+ }
+
+ // Decrypt the session data
+ obj.decryptSessionData = function (data, key) {
+ if ((typeof data != 'string') || (data.length < 13)) return {};
+ if (key == null) { key = obj.loginCookieEncryptionKey; }
+ try {
+ const buf = Buffer.from(data.replace(/\@/g, '+').replace(/\$/g, '/'), obj.args.cookieencoding ? obj.args.cookieencoding : 'base64');
+ const decipher = obj.crypto.createDecipheriv('aes-256-gcm', key.slice(0, 32), buf.slice(0, 12));
+ decipher.setAuthTag(buf.slice(12, 28));
+ return JSON.parse(decipher.update(buf.slice(28), 'binary', 'utf8') + decipher.final('utf8'));
+ } catch (ex) { return {}; }
+ }
+
+ // Generate a cryptographic key used to encode and decode cookies
+ obj.generateCookieKey = function () {
+ return Buffer.from(obj.crypto.randomBytes(80), 'binary');
+ //return Buffer.alloc(80, 0); // Sets the key to zeros, debug only.
+ };
+
+ // Encode an object as a cookie using a key using AES-GCM. (key must be 32 bytes or more)
+ obj.encodeCookie = function (o, key) {
+ try {
+ if (key == null) { key = obj.serverKey; }
+ o.time = Math.floor(Date.now() / 1000); // Add the cookie creation time
+ const iv = Buffer.from(obj.crypto.randomBytes(12), 'binary'), cipher = obj.crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv);
+ const crypted = Buffer.concat([cipher.update(JSON.stringify(o), 'utf8'), cipher.final()]);
+ const r = Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString(obj.args.cookieencoding ? obj.args.cookieencoding : 'base64').replace(/\+/g, '@').replace(/\//g, '$');
+ obj.debug('cookie', 'Encoded AESGCM cookie: ' + JSON.stringify(o));
+ return r;
+ } catch (ex) { obj.debug('cookie', 'ERR: Failed to encode AESGCM cookie due to exception: ' + ex); return null; }
+ };
+
+ // Decode a cookie back into an object using a key using AES256-GCM or AES128-CBC/HMAC-SHA384. Return null if it's not a valid cookie. (key must be 32 bytes or more)
+ obj.decodeCookie = function (cookie, key, timeout) {
+ if (cookie == null) return null;
+ var r = obj.decodeCookieAESGCM(cookie, key, timeout);
+ if (r === -1) { r = obj.decodeCookieAESSHA(cookie, key, timeout); } // If decodeCookieAESGCM() failed to decode, try decodeCookieAESSHA()
+ if ((r == null) && (obj.args.cookieencoding == null) && (cookie.length != 64) && ((cookie == cookie.toLowerCase()) || (cookie == cookie.toUpperCase()))) {
+ obj.debug('cookie', 'Upper/Lowercase cookie, try "CookieEncoding":"hex" in settings section of config.json.');
+ console.log('Upper/Lowercase cookie, try "CookieEncoding":"hex" in settings section of config.json.');
+ }
+ if ((r != null) && (typeof r.once == 'string') && (r.once.length > 0)) {
+ // This cookie must only be used once.
+ if (timeout == null) { timeout = 2; }
+ if (obj.cookieUseOnceTable[r.once] == null) {
+ const ctimeout = (((r.expire) == null || (typeof r.expire != 'number')) ? (r.time + ((timeout + 3) * 60000)) : (r.time + ((r.expire + 3) * 60000)));
+
+ // Store the used cookie in RAM
+ obj.cookieUseOnceTable[r.once] = ctimeout;
+
+ // Store the used cookie in the database
+ // TODO
+
+ // Send the used cookie to peer servers
+ // TODO
+
+ // Clean up the used table
+ if (++obj.cookieUseOnceTableCleanCounter > 20) {
+ const now = Date.now();
+ for (var i in obj.cookieUseOnceTable) { if (obj.cookieUseOnceTable[i] < now) { delete obj.cookieUseOnceTable[i]; } }
+ obj.cookieUseOnceTableCleanCounter = 0;
+ }
+ } else { return null; }
+ }
+ return r;
+ }
+
+ // Decode a cookie back into an object using a key using AES256-GCM. Return null if it's not a valid cookie. (key must be 32 bytes or more)
+ obj.decodeCookieAESGCM = function (cookie, key, timeout) {
+ try {
+ if (key == null) { key = obj.serverKey; }
+ cookie = Buffer.from(cookie.replace(/\@/g, '+').replace(/\$/g, '/'), obj.args.cookieencoding ? obj.args.cookieencoding : 'base64');
+ const decipher = obj.crypto.createDecipheriv('aes-256-gcm', key.slice(0, 32), cookie.slice(0, 12));
+ decipher.setAuthTag(cookie.slice(12, 28));
+ const o = JSON.parse(decipher.update(cookie.slice(28), 'binary', 'utf8') + decipher.final('utf8'));
+ if ((o.time == null) || (o.time == null) || (typeof o.time != 'number')) { obj.debug('cookie', 'ERR: Bad cookie due to invalid time'); return null; }
+ o.time = o.time * 1000; // Decode the cookie creation time
+ o.dtime = Date.now() - o.time; // Decode how long ago the cookie was created (in milliseconds)
+ if ((o.expire) == null || (typeof o.expire != 'number')) {
+ // Use a fixed cookie expire time
+ if (timeout == null) { timeout = 2; }
+ if ((o.dtime > (timeout * 60000)) || (o.dtime < -30000)) { obj.debug('cookie', 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right)
+ } else {
+ // An expire time is included in the cookie (in minutes), use this.
+ if ((o.expire !== 0) && ((o.dtime > (o.expire * 60000)) || (o.dtime < -30000))) { obj.debug('cookie', 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right)
+ }
+ obj.debug('cookie', 'Decoded AESGCM cookie: ' + JSON.stringify(o));
+ return o;
+ } catch (ex) { obj.debug('cookie', 'ERR: Bad AESGCM cookie due to exception: ' + ex); return -1; }
+ };
+
+ // Decode a cookie back into an object using a key using AES256 / HMAC-SHA384. Return null if it's not a valid cookie. (key must be 80 bytes or more)
+ // We do this because poor .NET does not support AES256-GCM.
+ obj.decodeCookieAESSHA = function (cookie, key, timeout) {
+ try {
+ if (key == null) { key = obj.serverKey; }
+ if (key.length < 80) { return null; }
+ cookie = Buffer.from(cookie.replace(/\@/g, '+').replace(/\$/g, '/'), obj.args.cookieencoding ? obj.args.cookieencoding : 'base64');
+ const decipher = obj.crypto.createDecipheriv('aes-256-cbc', key.slice(48, 80), cookie.slice(0, 16));
+ const rawmsg = decipher.update(cookie.slice(16), 'binary', 'binary') + decipher.final('binary');
+ const hmac = obj.crypto.createHmac('sha384', key.slice(0, 48));
+ hmac.update(rawmsg.slice(48));
+ if (Buffer.compare(hmac.digest(), Buffer.from(rawmsg.slice(0, 48))) == false) { return null; }
+ const o = JSON.parse(rawmsg.slice(48).toString('utf8'));
+ if ((o.time == null) || (o.time == null) || (typeof o.time != 'number')) { obj.debug('cookie', 'ERR: Bad cookie due to invalid time'); return null; }
+ o.time = o.time * 1000; // Decode the cookie creation time
+ o.dtime = Date.now() - o.time; // Decode how long ago the cookie was created (in milliseconds)
+ if ((o.expire) == null || (typeof o.expire != 'number')) {
+ // Use a fixed cookie expire time
+ if (timeout == null) { timeout = 2; }
+ if ((o.dtime > (timeout * 60000)) || (o.dtime < -30000)) { obj.debug('cookie', 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right)
+ } else {
+ // An expire time is included in the cookie (in minutes), use this.
+ if ((o.expire !== 0) && ((o.dtime > (o.expire * 60000)) || (o.dtime < -30000))) { obj.debug('cookie', 'ERR: Bad cookie due to timeout'); return null; } // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right)
+ }
+ obj.debug('cookie', 'Decoded AESSHA cookie: ' + JSON.stringify(o));
+ return o;
+ } catch (ex) { obj.debug('cookie', 'ERR: Bad AESSHA cookie due to exception: ' + ex); return null; }
+ };
+
+ // Debug
+ obj.debug = function (source, ...args) {
+ // Send event to console
+ if ((obj.debugSources != null) && ((obj.debugSources == '*') || (obj.debugSources.indexOf(source) >= 0))) { console.log(source.toUpperCase() + ':', ...args); }
+
+ // Send event to log file
+ if (obj.config.settings && obj.config.settings.log) {
+ if (typeof obj.args.log == 'string') { obj.args.log = obj.args.log.split(','); }
+ if ((obj.args.log.indexOf(source) >= 0) || (obj.args.log[0] == '*')) {
+ const d = new Date();
+ if (obj.xxLogFile == null) {
+ try {
+ obj.xxLogFile = obj.fs.openSync(obj.getConfigFilePath('log.txt'), 'a+', 0o666);
+ obj.fs.writeSync(obj.xxLogFile, '---- Log start at ' + new Date().toLocaleString() + ' ----\r\n');
+ obj.xxLogDateStr = d.toLocaleDateString();
+ } catch (ex) { }
+ }
+ if (obj.xxLogFile != null) {
+ try {
+ if (obj.xxLogDateStr != d.toLocaleDateString()) { obj.xxLogDateStr = d.toLocaleDateString(); obj.fs.writeSync(obj.xxLogFile, '---- ' + d.toLocaleDateString() + ' ----\r\n'); }
+ const formattedArgs = args.map(function (arg) { return (typeof arg === 'object' && arg !== null) ? JSON.stringify(arg) : arg; });
+ obj.fs.writeSync(obj.xxLogFile, new Date().toLocaleTimeString() + ' - ' + source + ': ' + formattedArgs.join(', ') + '\r\n');
+ } catch (ex) { }
+ }
+ }
+ }
+
+ // Send the event to logged in administrators
+ if ((obj.debugRemoteSources != null) && ((obj.debugRemoteSources == '*') || (obj.debugRemoteSources.indexOf(source) >= 0))) {
+ var sendcount = 0;
+ for (var sessionid in obj.webserver.wssessions2) {
+ const ws = obj.webserver.wssessions2[sessionid];
+ if ((ws != null) && (ws.userid != null)) {
+ const user = obj.webserver.users[ws.userid];
+ if ((user != null) && (user.siteadmin == 4294967295)) {
+ try { ws.send(JSON.stringify({ action: 'trace', source: source, args: args, time: Date.now() })); sendcount++; } catch (ex) { }
+ }
+ }
+ }
+ if (sendcount == 0) { obj.debugRemoteSources = null; } // If there are no listeners, remove debug sources.
+ }
+ };
+
+ // Update server state. Writes a server state file.
+ const meshServerState = {};
+ obj.updateServerState = function (name, val) {
+ //console.log('updateServerState', name, val);
+ try {
+ if ((name != null) && (val != null)) {
+ var changed = false;
+ if ((name != null) && (meshServerState[name] != val)) { if ((val == null) && (meshServerState[name] != null)) { delete meshServerState[name]; changed = true; } else { if (meshServerState[name] != val) { meshServerState[name] = val; changed = true; } } }
+ if (changed == false) return;
+ }
+ var r = 'time=' + Date.now() + '\r\n';
+ for (var i in meshServerState) { r += (i + '=' + meshServerState[i] + '\r\n'); }
+ try {
+ obj.fs.writeFileSync(obj.getConfigFilePath('serverstate.txt'), r); // Try to write the server state, this may fail if we don't have permission.
+ } catch (ex) { obj.serverSelfWriteAllowed = false; }
+ } catch (ex) { } // Do nothing since this is not a critical feature.
+ };
+
+ // Read a list of IP addresses from a file
+ function readIpListFromFile(arg) {
+ if ((typeof arg != 'string') || (!arg.startsWith('file:'))) return arg;
+ var lines = null;
+ try { lines = obj.fs.readFileSync(obj.path.join(obj.datapath, arg.substring(5))).toString().split(/\r?\n/).join('\r').split('\r'); } catch (ex) { }
+ if (lines == null) return null;
+ const validLines = [];
+ for (var i in lines) { if ((lines[i].length > 0) && (((lines[i].charAt(0) > '0') && (lines[i].charAt(0) < '9')) || (lines[i].charAt(0) == ':'))) validLines.push(lines[i]); }
+ return validLines;
+ }
+
+ // Logging funtions
+ function logException(e) { e += ''; logErrorEvent(e); }
+ function logInfoEvent(msg) { if (obj.servicelog != null) { obj.servicelog.info(msg); } console.log(msg); }
+ function logWarnEvent(msg) { if (obj.servicelog != null) { obj.servicelog.warn(msg); } console.log(msg); }
+ function logErrorEvent(msg) { if (obj.servicelog != null) { obj.servicelog.error(msg); } console.error(msg); }
+ obj.getServerWarnings = function () { return serverWarnings; }
+ // TODO: migrate from other addServerWarning function and add timestamp
+ obj.addServerWarning = function (msg, id, args, print) { serverWarnings.push({ msg: msg, id: id, args: args }); if (print !== false) { console.log("WARNING: " + msg); } }
+
+ // auth.log functions
+ obj.authLog = function (server, msg, args) {
+ if (typeof msg != 'string') return;
+ var str = msg;
+ if (args != null) {
+ if (typeof args.sessionid == 'string') { str += ', SessionID: ' + args.sessionid; }
+ if (typeof args.useragent == 'string') { const userAgentInfo = obj.webserver.getUserAgentInfo(args.useragent); str += ', Browser: ' + userAgentInfo.browserStr + ', OS: ' + userAgentInfo.osStr; }
+ }
+ obj.debug('authlog', str);
+ if (obj.syslogauth != null) { try { obj.syslogauth.log(obj.syslogauth.LOG_INFO, str); } catch (ex) { } }
+ if (obj.authlogfile != null) { // Write authlog to file
+ try {
+ const d = new Date(), month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][d.getMonth()];
+ str = month + ' ' + d.getDate() + ' ' + obj.common.zeroPad(d.getHours(), 2) + ':' + obj.common.zeroPad(d.getMinutes(), 2) + ':' + d.getSeconds() + ' meshcentral ' + server + '[' + process.pid + ']: ' + msg + ((obj.platform == 'win32') ? '\r\n' : '\n');
+ obj.fs.write(obj.authlogfile, str, function (err, written, string) { if (err) { console.error(err); } });
+ } catch (ex) { console.error(ex); }
+ }
+ }
+
+ // Return the path of a file into the meshcentral-data path
+ obj.getConfigFilePath = function (filename) {
+ if ((obj.config != null) && (obj.config.configfiles != null) && (obj.config.configfiles[filename] != null) && (typeof obj.config.configfiles[filename] == 'string')) {
+ //console.log('getConfigFilePath(\"' + filename + '\") = ' + obj.config.configfiles[filename]);
+ return obj.config.configfiles[filename];
+ }
+ //console.log('getConfigFilePath(\"' + filename + '\") = ' + obj.path.join(obj.datapath, filename));
+ return obj.path.join(obj.datapath, filename);
+ };
+
+ return obj;
+}
+
+// Resolve a list of names, call back with list of failed resolves.
+function checkResolveAll(names, func) {
+ const dns = require('dns'), state = { func: func, count: names.length, err: null };
+ for (var i in names) {
+ dns.lookup(names[i], { all: true }, function (err, records) {
+ if (err != null) { if (this.state.err == null) { this.state.err = [this.name]; } else { this.state.err.push(this.name); } }
+ if (--this.state.count == 0) { this.state.func(this.state.err); }
+ }.bind({ name: names[i], state: state }))
+ }
+}
+
+// Resolve a list of domains to IP addresses, return a flat array of IPs.
+async function resolveDomainsToIps(originalArray) {
+ if (!Array.isArray(originalArray)) { return undefined; }
+ const flatResult = [];
+ for (const item of originalArray) {
+ if (new require('ipcheck')(item).valid) {
+ flatResult.push(item);
+ continue;
+ }
+ try {
+ const results = await require('dns').promises.lookup(item, { all: true });
+ flatResult.push(...results.map(r => r.address));
+ } catch (err) {
+ console.log(`Could not resolve ${item}`);
+ }
+ }
+ if (flatResult.length == 0) { return undefined; }
+ return flatResult;
+}
+
+// Return the server configuration
+function getConfig(createSampleConfig) {
+ // Figure out the datapath location
+ var i, datapath = null;
+ const fs = require('fs'), path = require('path'), args = require('minimist')(process.argv.slice(2));
+ if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) {
+ datapath = path.join(__dirname, '../../meshcentral-data');
+ } else {
+ datapath = path.join(__dirname, '../meshcentral-data');
+ }
+ if (args.datapath) { datapath = args.datapath; }
+ try { fs.mkdirSync(datapath); } catch (ex) { }
+
+ // Read configuration file if present and change arguments.
+ var config = {}, configFilePath = path.join(datapath, 'config.json');
+ if (args.configfile) { configFilePath = common.joinPath(datapath, args.configfile); }
+ if (fs.existsSync(configFilePath)) {
+ // Load and validate the configuration file
+ try { config = require(configFilePath); } catch (ex) { console.log('ERROR: Unable to parse ' + configFilePath + '.'); return null; }
+ if (config.domains == null) { config.domains = {}; }
+ for (i in config.domains) { if ((i.split('/').length > 1) || (i.split(' ').length > 1)) { console.log("ERROR: Error in config.json, domain names can't have spaces or /."); return null; } }
+ } else {
+ if (createSampleConfig === true) {
+ // Copy the "sample-config.json" to give users a starting point
+ const sampleConfigPath = path.join(__dirname, 'sample-config.json');
+ if (fs.existsSync(sampleConfigPath)) { fs.createReadStream(sampleConfigPath).pipe(fs.createWriteStream(configFilePath)); }
+ }
+ }
+
+ // Set the command line arguments to the config file if they are not present
+ if (!config.settings) { config.settings = {}; }
+ for (i in args) { config.settings[i] = args[i]; }
+
+ // Lower case all keys in the config file
+ try {
+ require('./common.js').objKeysToLower(config, ['ldapoptions', 'defaultuserwebstate', 'forceduserwebstate', 'httpheaders', 'telegram/proxy']);
+ } catch (ex) {
+ console.log('CRITICAL ERROR: Unable to access the file \"./common.js\".\r\nCheck folder & file permissions.');
+ process.exit();
+ }
+
+ return config;
+}
+
+// Check if a list of modules are present and install any missing ones
+function InstallModules(modules, args, func) {
+ var missingModules = [];
+ if (modules.length > 0) {
+ const dependencies = require('./package.json').dependencies;
+ for (var i in modules) {
+ // Modules may contain a version tag (foobar@1.0.0), remove it so the module can be found using require
+ const moduleNameAndVersion = modules[i];
+ const moduleInfo = moduleNameAndVersion.split('@', 3);
+ var moduleName = null;
+ var moduleVersion = null;
+ if(moduleInfo.length == 1){ // normal package without version
+ moduleName = moduleInfo[0];
+ } else if (moduleInfo.length == 2) { // normal package with a version OR custom repo package with no version
+ moduleName = moduleInfo[0] === '' ? moduleNameAndVersion : moduleInfo[0];
+ moduleVersion = moduleInfo[0] === '' ? null : moduleInfo[1];
+ } else if (moduleInfo.length == 3) { // custom repo package and package with a version
+ moduleName = "@" + moduleInfo[1];
+ moduleVersion = moduleInfo[2];
+ }
+ try {
+ // Does the module need a specific version?
+ if (moduleVersion) {
+ var versionMatch = false;
+ var modulePath = null;
+ // This is the first way to test if a module is already installed.
+ try { versionMatch = (require(`${moduleName}/package.json`).version == moduleVersion) } catch (ex) {
+ if (ex.code == "ERR_PACKAGE_PATH_NOT_EXPORTED") { modulePath = ("" + ex).split(' ').at(-1); } else { throw new Error(); }
+ }
+ // If the module is not installed, but we get the ERR_PACKAGE_PATH_NOT_EXPORTED error, try a second way.
+ if ((versionMatch == false) && (modulePath != null)) {
+ if (JSON.parse(require('fs').readFileSync(modulePath, 'utf8')).version != moduleVersion) { throw new Error(); }
+ } else if (versionMatch == false) {
+ throw new Error();
+ }
+ } else {
+ // For all other modules, do the check here.
+ // Is the module in package.json? Install exact version.
+ if (typeof dependencies[moduleName] != null) { moduleVersion = dependencies[moduleName]; }
+ require(moduleName);
+ }
+ } catch (ex) {
+ missingModules.push(moduleNameAndVersion);
+ }
+ }
+
+ if (missingModules.length > 0) { if (args.debug) { console.log('Missing Modules: ' + missingModules.join(', ')); } InstallModuleEx(missingModules, args, func); } else { func(); }
+ }
+}
+
+// Install all missing modules at once. We will be running "npm install" once, with a full list of all modules we need, no matter if they area already installed or not,
+// this is to make sure NPM gives us exactly what we need. Also, we install the meshcentral with current version, so that NPM does not update it - which it will do if obmitted.
+function InstallModuleEx(modulenames, args, func) {
+ var names = modulenames.join(' ');
+ console.log('Installing modules', modulenames);
+ const child_process = require('child_process');
+ var parentpath = __dirname;
+ function getCurrentVersion() { try { return JSON.parse(require('fs').readFileSync(require('path').join(__dirname, 'package.json'), 'utf8')).version; } catch (ex) { } return null; } // Fetch server version
+ //const meshCentralVersion = getCurrentVersion();
+ //if ((meshCentralVersion != null) && (args.dev == null)) { names = 'meshcentral@' + getCurrentVersion() + ' ' + names; }
+
+ // Get the working directory
+ if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) { parentpath = require('path').join(__dirname, '../..'); }
+
+ if (args.debug) { console.log('NPM Command Line: ' + npmpath + ` install --save-exact --no-audit --omit=optional --no-fund ${names}`); }
+ // always use --save-exact - https://stackoverflow.com/a/64507176/1210734
+ child_process.exec(npmpath + ` install --save-exact --no-audit --no-optional --omit=optional ${names}`, { maxBuffer: 512000, timeout: 300000, cwd: parentpath }, function (error, stdout, stderr) {
+ if ((error != null) && (error != '')) {
+ var mcpath = __dirname;
+ if (mcpath.endsWith('\\node_modules\\meshcentral') || mcpath.endsWith('/node_modules/meshcentral')) { mcpath = require('path').join(mcpath, '..', '..'); }
+ console.log('ERROR: Unable to install required modules. MeshCentral may not have access to npm, or npm may not have suffisent rights to load the new module. To manualy install this module try:\r\n\r\n cd "' + mcpath + '"\r\n npm install --no-audit --no-optional --omit=optional ' + names + '\r\n node node_modules' + ((require('os').platform() == 'win32') ? '\\' : '/') + 'meshcentral');
+ process.exit();
+ return;
+ }
+ func();
+ return;
+ });
+}
+
+// Detect CTRL-C on Linux and stop nicely
+process.on('SIGINT', function () { if (meshserver != null) { meshserver.Stop(); meshserver = null; } console.log('Server Ctrl-C exit...'); process.exit(); });
+
+// Add a server warning, warnings will be shown to the administrator on the web application
+// TODO: migrate to obj.addServerWarning?
+const serverWarnings = [];
+function addServerWarning(msg, id, args, print) { serverWarnings.push({ msg: msg, id: id, args: args }); if (print !== false) { console.log("WARNING: " + msg); } }
+
+/*
+var ServerWarnings = {
+ 1: "",
+ 2: "Missing WebDAV parameters.",
+ 3: "Unrecognized configuration option \"{0}\".",
+ 4: "WebSocket compression is disabled, this feature is broken in NodeJS v11.11 to v12.15 and v13.2",
+ 5: "Unable to load Intel AMT TLS root certificate for default domain.",
+ 6: "Unable to load Intel AMT TLS root certificate for domain {0}.",
+ 7: "CIRA local FQDN's ignored when server in LAN-only or WAN-only mode.",
+ 8: "Can't have more than 4 CIRA local FQDN's. Ignoring value.",
+ 9: "Agent hash checking is being skipped, this is unsafe.",
+ 10: "Missing Let's Encrypt email address.",
+ 11: "Invalid Let's Encrypt host names.",
+ 12: "Invalid Let's Encrypt names, can't contain a *.",
+ 13: "Unable to setup Let's Encrypt module.",
+ 14: "Invalid Let's Encrypt names, unable to resolve: {0}",
+ 15: "Invalid Let's Encrypt email address, unable to resolve: {0}",
+ 16: "Unable to load CloudFlare trusted proxy IPv6 address list.",
+ 17: "SendGrid server has limited use in LAN mode.",
+ 18: "SMTP server has limited use in LAN mode.",
+ 19: "SMS gateway has limited use in LAN mode.",
+ 20: "Invalid \"LoginCookieEncryptionKey\" in config.json.",
+ 21: "Backup path can't be set within meshcentral-data folder, backup settings ignored.",
+ 22: "Failed to sign agent {0}: {1}",
+ 23: "Unable to load agent icon file: {0}.",
+ 24: "Unable to load agent logo file: {0}.",
+ 25: "This NodeJS version does not support OpenID.",
+ 26: "This NodeJS version does not support Discord.js.",
+ 27: "Firebase now requires a service account JSON file, Firebase disabled."
+};
+*/
+
+// Load the really basic modules
+var npmpath = 'npm';
+var meshserver = null;
+var childProcess = null;
+var previouslyInstalledModules = {};
+function mainStart() {
+ // Check the NodeJS is version 16 or better.
+ if (Number(process.version.match(/^v(\d+\.\d+)/)[1]) < 16) { console.log("MeshCentral requires Node v16 or above, current version is " + process.version + "."); return; }
+
+ // If running within the node_modules folder, move working directory to the parent of the node_modules folder.
+ if (__dirname.endsWith('\\node_modules\\meshcentral') || __dirname.endsWith('/node_modules/meshcentral')) { process.chdir(require('path').join(__dirname, '..', '..')); }
+
+ // Check for any missing modules.
+ InstallModules(['minimist'], {}, function () {
+ // Parse inbound arguments
+ const args = require('minimist')(process.argv.slice(2));
+
+ // Setup the NPM path
+ if (args.npmpath == null) {
+ try {
+ var xnodepath = process.argv[0];
+ var xnpmpath = require('path').join(require('path').dirname(process.argv[0]), 'npm');
+ if (require('fs').existsSync(xnodepath) && require('fs').existsSync(xnpmpath)) {
+ if (xnodepath.indexOf(' ') >= 0) { xnodepath = '"' + xnodepath + '"'; }
+ if (xnpmpath.indexOf(' ') >= 0) { xnpmpath = '"' + xnpmpath + '"'; }
+ if (require('os').platform() == 'win32') { npmpath = xnpmpath; } else { npmpath = (xnodepath + ' ' + xnpmpath); }
+ }
+ } catch (ex) { console.log(ex); }
+ } else {
+ npmpath = args.npmpath;
+ }
+
+ // Get the server configuration
+ var config = getConfig(false);
+ if (config == null) { process.exit(); }
+
+ // Lowercase the auth value if present
+ for (var i in config.domains) { if (typeof config.domains[i].auth == 'string') { config.domains[i].auth = config.domains[i].auth.toLowerCase(); } }
+
+ // Get the current node version
+ const verSplit = process.version.substring(1).split('.');
+ var nodeVersion = parseInt(verSplit[0]) + (parseInt(verSplit[1]) / 100);
+
+ // Check if RDP support if present
+ var mstsc = true;
+ try { require('./rdp') } catch (ex) { mstsc = false; }
+
+ // Check if Windows SSPI, LDAP, Passport and YubiKey OTP will be used
+ var sspi = false;
+ var ldap = false;
+ var passport = [];
+ var allsspi = true;
+ var yubikey = false;
+ var ssh = false;
+ var sessionRecording = false;
+ var domainCount = 0;
+ var wildleek = false;
+ var nodemailer = false;
+ var sendgrid = false;
+ var captcha = false;
+ if (require('os').platform() == 'win32') { for (var i in config.domains) { domainCount++; if (config.domains[i].auth == 'sspi') { sspi = true; } else { allsspi = false; } } } else { allsspi = false; }
+ if (domainCount == 0) { allsspi = false; }
+ for (var i in config.domains) {
+ if (i.startsWith('_')) continue;
+ if (((config.domains[i].smtp != null) && (config.domains[i].smtp.name != 'console')) || (config.domains[i].sendmail != null)) { nodemailer = true; }
+ if (config.domains[i].sendgrid != null) { sendgrid = true; }
+ if (config.domains[i].yubikey != null) { yubikey = true; }
+ if (config.domains[i].auth == 'ldap') { ldap = true; }
+ if (mstsc == false) { config.domains[i].mstsc = false; }
+ if (config.domains[i].ssh == true) { ssh = true; }
+ if ((typeof config.domains[i].authstrategies == 'object')) {
+ if (passport.indexOf('passport') == -1) { passport.push('passport','connect-flash'); } // Passport v0.6.0 requires a patch, see https://github.com/jaredhanson/passport/issues/904 and include connect-flash here to display errors
+ if ((typeof config.domains[i].authstrategies.twitter == 'object') && (typeof config.domains[i].authstrategies.twitter.clientid == 'string') && (typeof config.domains[i].authstrategies.twitter.clientsecret == 'string') && (passport.indexOf('passport-twitter') == -1)) { passport.push('passport-twitter'); }
+ if ((typeof config.domains[i].authstrategies.google == 'object') && (typeof config.domains[i].authstrategies.google.clientid == 'string') && (typeof config.domains[i].authstrategies.google.clientsecret == 'string') && (passport.indexOf('passport-google-oauth20') == -1)) { passport.push('passport-google-oauth20'); }
+ if ((typeof config.domains[i].authstrategies.github == 'object') && (typeof config.domains[i].authstrategies.github.clientid == 'string') && (typeof config.domains[i].authstrategies.github.clientsecret == 'string') && (passport.indexOf('passport-github2') == -1)) { passport.push('passport-github2'); }
+ if ((typeof config.domains[i].authstrategies.azure == 'object') && (typeof config.domains[i].authstrategies.azure.clientid == 'string') && (typeof config.domains[i].authstrategies.azure.clientsecret == 'string') && (typeof config.domains[i].authstrategies.azure.tenantid == 'string') && (passport.indexOf('passport-azure-oauth2') == -1)) { passport.push('passport-azure-oauth2'); passport.push('jwt-simple'); }
+ if ((typeof config.domains[i].authstrategies.oidc == 'object') && (passport.indexOf('openid-client@5.7.1') == -1)) {
+ if ((nodeVersion >= 17)
+ || ((Math.floor(nodeVersion) == 16) && (nodeVersion >= 16.13))
+ || ((Math.floor(nodeVersion) == 14) && (nodeVersion >= 14.15))
+ || ((Math.floor(nodeVersion) == 12) && (nodeVersion >= 12.19))) {
+ passport.push('openid-client@5.7.1');
+ } else {
+ addServerWarning('This NodeJS version does not support OpenID Connect on MeshCentral.', 25);
+ delete config.domains[i].authstrategies.oidc;
+ }
+ }
+ if ((typeof config.domains[i].authstrategies.saml == 'object') || (typeof config.domains[i].authstrategies.jumpcloud == 'object')) { passport.push('passport-saml'); }
+ }
+ if (config.domains[i].sessionrecording != null) { sessionRecording = true; }
+ if ((config.domains[i].passwordrequirements != null) && (config.domains[i].passwordrequirements.bancommonpasswords == true)) { wildleek = true; }
+ if ((config.domains[i].newaccountscaptcha != null) && (config.domains[i].newaccountscaptcha !== false)) { captcha = true; }
+ if ((typeof config.domains[i].duo2factor == 'object') && (passport.indexOf('@duosecurity/duo_universal') == -1)) { passport.push('@duosecurity/duo_universal@2.1.0'); }
+ }
+
+ // Build the list of required modules
+ // NOTE: ALL MODULES MUST HAVE A VERSION NUMBER AND THE VERSION MUST MATCH THAT USED IN Dockerfile
+ var modules = ['archiver@7.0.1', 'body-parser@1.20.3', 'cbor@5.2.0', 'compression@1.8.1', 'cookie-session@2.1.1', 'express@4.21.2', 'express-handlebars@7.1.3', 'express-ws@5.0.2', 'ipcheck@0.1.0', 'minimist@1.2.8', 'multiparty@4.2.3', '@seald-io/nedb@4.1.2', 'node-forge@1.3.1', 'ua-parser-js@1.0.40', 'ua-client-hints-js@0.1.2', 'ws@8.18.3', 'yauzl@2.10.0'];
+ if (require('os').platform() == 'win32') { modules.push('node-windows@0.1.14'); modules.push('loadavg-windows@1.1.1'); if (sspi == true) { modules.push('node-sspi@0.2.10'); } } // Add Windows modules
+ if (ldap == true) { modules.push('ldapauth-fork@5.0.5'); }
+ if (ssh == true) { modules.push('ssh2@1.17.0'); }
+ if (passport != null) { modules.push(...passport); }
+ if (captcha == true) { modules.push('svg-captcha@1.4.0'); }
+
+ if (sessionRecording == true) { modules.push('image-size@2.0.2'); } // Need to get the remote desktop JPEG sizes to index the recording file.
+ if (config.letsencrypt != null) { modules.push('acme-client@4.2.5'); } // Add acme-client module. We need to force v4.2.4 or higher since olver versions using SHA-1 which is no longer supported by Let's Encrypt.
+ if (config.settings.mqtt != null) { modules.push('aedes@0.51.3'); } // Add MQTT Modules
+ if (config.settings.mysql != null) { modules.push('mysql2@3.15.1'); } // Add MySQL.
+ //if (config.settings.mysql != null) { modules.push('@mysql/xdevapi@8.0.33'); } // Add MySQL, official driver (https://dev.mysql.com/doc/dev/connector-nodejs/8.0/)
+ if (config.settings.mongodb != null) { modules.push('mongodb@4.17.2'); modules.push('@mongodb-js/saslprep@1.3.1')} // Add MongoDB, official driver.
+ if (config.settings.postgres != null) { modules.push('pg@8.16.3') } // Add Postgres, official driver.
+ if (config.settings.mariadb != null) { modules.push('mariadb@3.4.5'); } // Add MariaDB, official driver.
+ if (config.settings.acebase != null) { modules.push('acebase@1.29.5'); } // Add AceBase, official driver.
+ if (config.settings.sqlite3 != null) { modules.push('sqlite3@5.1.7'); } // Add sqlite3, official driver.
+ if (config.settings.vault != null) { modules.push('node-vault@0.10.5'); } // Add official HashiCorp's Vault module.
+ const hasExistingProxy = process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy'];
+ 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
+ else if (config.settings.xmongodb != null) { modules.push('mongojs@3.1.0'); } // Add MongoJS, old driver.
+ if (nodemailer || ((config.smtp != null) && (config.smtp.name != 'console')) || (config.sendmail != null)) { modules.push('nodemailer@6.10.1'); } // Add SMTP support
+ if (sendgrid || (config.sendgrid != null)) { modules.push('@sendgrid/mail@8.1.6'); } // Add SendGrid support
+ if ((args.translate || args.dev) && (Number(process.version.match(/^v(\d+\.\d+)/)[1]) >= 16)) { modules.push('jsdom@22.1.0'); modules.push('esprima@4.0.1'); modules.push('html-minifier-terser@7.2.0'); } // Translation support
+ if (typeof config.settings.crowdsec == 'object') { modules.push('@crowdsec/express-bouncer@0.1.0'); } // Add CrowdSec bounser module (https://www.npmjs.com/package/@crowdsec/express-bouncer)
+ if (config.settings.prometheus != null) { modules.push('prom-client@15.1.3'); } // Add Prometheus Metrics support
+
+ if (typeof config.settings.autobackup == 'object') {
+ // Setup encrypted zip support if needed
+ if (config.settings.autobackup.zippassword) { modules.push('archiver-zip-encrypted@2.0.0'); }
+ // Enable Google Drive Support
+ if (typeof config.settings.autobackup.googledrive == 'object') { modules.push('googleapis@128.0.0'); }
+ // Enable WebDAV Support
+ if (typeof config.settings.autobackup.webdav == 'object') {
+ if ((typeof config.settings.autobackup.webdav.url != 'string') || (typeof config.settings.autobackup.webdav.username != 'string') || (typeof config.settings.autobackup.webdav.password != 'string')) { addServerWarning("Missing WebDAV parameters.", 2, null, !args.launch); } else { modules.push('webdav@5.8.0'); }
+ }
+ // Enable S3 Support
+ if (typeof config.settings.autobackup.s3 == 'object') { modules.push('minio@8.0.6'); }
+ }
+
+ // Setup common password blocking
+ if (wildleek == true) { modules.push('wildleek@2.0.0'); }
+
+ // Setup 2nd factor authentication
+ if (config.settings.no2factorauth !== true) {
+ // Setup YubiKey OTP if configured
+ if (yubikey == true) { modules.push('yub@0.11.1'); } // Add YubiKey OTP support (replaced yubikeyotp due to form-data issues)
+ if (allsspi == false) { modules.push('otplib@12.0.1'); } // Google Authenticator support (v10 supports older NodeJS versions).
+ }
+
+ // Desktop multiplexor support
+ if (config.settings.desktopmultiplex === true) { modules.push('image-size@2.0.2'); }
+
+ // SMS support
+ if (config.sms != null) {
+ if (config.sms.provider == 'twilio') { modules.push('twilio@4.23.0'); }
+ if (config.sms.provider == 'plivo') { modules.push('plivo@4.75.1'); }
+ if (config.sms.provider == 'telnyx') { modules.push('telnyx@1.25.5'); }
+ }
+
+ // Messaging support
+ if (config.messaging != null) {
+ if (config.messaging.telegram != null) { modules.push('telegram@2.26.22'); modules.push('input@1.0.1'); }
+ if (config.messaging.discord != null) { if (nodeVersion >= 17) { modules.push('discord.js@14.6.0'); } else { delete config.messaging.discord; addServerWarning('This NodeJS version does not support Discord.js.', 26); } }
+ if (config.messaging.xmpp != null) { modules.push('@xmpp/client@0.13.6'); }
+ if (config.messaging.pushover != null) { modules.push('node-pushover@1.0.0'); }
+ if (config.messaging.zulip != null) { modules.push('zulip@0.1.0'); }
+ }
+
+ // Setup web based push notifications
+ if ((typeof config.settings.webpush == 'object') && (typeof config.settings.webpush.email == 'string')) { modules.push('web-push@3.6.7'); }
+
+ // Firebase Support
+ if ((config.firebase != null) && (typeof config.firebase.serviceaccountfile == 'string')) { modules.push('firebase-admin@12.7.0'); }
+
+ // Syslog support
+ if ((require('os').platform() != 'win32') && (config.settings.syslog || config.settings.syslogjson)) { modules.push('modern-syslog@1.2.0'); }
+ if (config.settings.syslogtcp) { modules.push('syslog@0.1.1-1'); }
+
+ // Setup heapdump support if needed, useful for memory leak debugging
+ // https://www.arbazsiddiqui.me/a-practical-guide-to-memory-leaks-in-nodejs/
+ if (config.settings.heapdump === true) { modules.push('heapdump@0.3.15'); }
+
+ // Install any missing modules and launch the server
+ InstallModules(modules, args, function () {
+ if (require('os').platform() == 'win32') { try { require('node-windows'); } catch (ex) { console.log("Module node-windows can't be loaded. Restart MeshCentral."); process.exit(); return; } }
+ meshserver = CreateMeshCentralServer(config, args);
+ meshserver.Start();
+ });
+
+ // On exit, also terminate the child process if applicable
+ process.on('exit', function () { if (childProcess) { childProcess.kill(); childProcess = null; } });
+
+ // If our parent exits, we also exit
+ if (args.launch) {
+ process.stderr.on('end', function () { process.exit(); });
+ process.stdout.on('end', function () { process.exit(); });
+ process.stdin.on('end', function () { process.exit(); });
+ process.stdin.on('data', function (data) { });
+ }
+ });
+}
+
+if (require.main === module) {
+ mainStart(); // Called directly, launch normally.
+} else {
+ module.exports.mainStart = mainStart; // Required as a module, useful for winservice.js
+}
From 58a3e88a6c928fc89c0a5663f0155e1674906b9c Mon Sep 17 00:00:00 2001
From: stephannn <38951729+stephannn@users.noreply.github.com>
Date: Mon, 13 Oct 2025 15:30:38 +0200
Subject: [PATCH 5/8] Add files via upload
---
meshctrl.js | 6360 +++++++++++++++++++++++++--------------------------
1 file changed, 3179 insertions(+), 3181 deletions(-)
diff --git a/meshctrl.js b/meshctrl.js
index 76e86dd288..5722acce97 100644
--- a/meshctrl.js
+++ b/meshctrl.js
@@ -1,3181 +1,3179 @@
-#!/usr/bin/env node
-
-/**
-* @description MeshCentral command line tool
-* @author Ylian Saint-Hilaire
-* @copyright Intel Corporation 2018-2022
-* @license Apache-2.0
-* @version v0.0.1
-*/
-
-// Make sure we have the dependency modules
-try { require('minimist'); } catch (ex) { console.log('Missing module "minimist", type "npm install minimist" to install it.'); return; }
-try { require('ws'); } catch (ex) { console.log('Missing module "ws", type "npm install ws" to install it.'); return; }
-
-var settings = {};
-const crypto = require('crypto');
-const args = require('minimist')(process.argv.slice(2));
-const path = require('path');
-const possibleCommands = ['edituser', 'listusers', 'listusersessions', 'listdevicegroups', 'listdevices', 'listusersofdevicegroup', 'listevents', 'logintokens', 'serverinfo', 'userinfo', 'adduser', 'removeuser', 'adddevicegroup', 'removedevicegroup', 'editdevicegroup', 'broadcast', 'showevents', 'addusertodevicegroup', 'removeuserfromdevicegroup', 'addusertodevice', 'removeuserfromdevice', 'sendinviteemail', 'generateinvitelink', 'config', 'movetodevicegroup', 'deviceinfo', 'removedevice', 'editdevice', 'addlocaldevice', 'addamtdevice', 'addusergroup', 'listusergroups', 'removeusergroup', 'runcommand', 'shell', 'upload', 'download', 'deviceopenurl', 'devicemessage', 'devicetoast', 'addtousergroup', 'removefromusergroup', 'removeallusersfromusergroup', 'devicesharing', 'devicepower', 'indexagenterrorlog', 'agentdownload', 'report', 'grouptoast', 'groupmessage', 'webrelay'];
-if (args.proxy != null) { try { require('https-proxy-agent'); } catch (ex) { console.log('Missing module "https-proxy-agent", type "npm install https-proxy-agent" to install it.'); return; } }
-
-if (args['_'].length == 0) {
- console.log("MeshCtrl performs command line actions on a MeshCentral server.");
- console.log("Information at: https://meshcentral.com");
- console.log("No action specified, use MeshCtrl like this:\r\n\r\n meshctrl [action] [arguments]\r\n");
- console.log("Supported actions:");
- console.log(" Help [action] - Get help on an action.");
- console.log(" ServerInfo - Show server information.");
- console.log(" UserInfo - Show user information.");
- console.log(" ListUsers - List user accounts.");
- console.log(" ListUserSessions - List online users.");
- console.log(" ListUserGroups - List user groups.");
- console.log(" ListDevices - List devices.");
- console.log(" ListDeviceGroups - List device groups.");
- console.log(" ListUsersOfDeviceGroup - List the users in a device group.");
- console.log(" ListEvents - List server events.");
- console.log(" LoginTokens - List, create and remove login tokens.");
- console.log(" DeviceInfo - Show information about a device.");
- console.log(" AddLocalDevice - Add a local device.");
- console.log(" AddAmtDevice - Add a AMT device.");
- console.log(" EditDevice - Make changes to a device.");
- console.log(" RemoveDevice - Delete a device.");
- console.log(" Config - Perform operation on config.json file.");
- console.log(" AddUser - Create a new user account.");
- console.log(" EditUser - Change a user account.");
- console.log(" RemoveUser - Delete a user account.");
- console.log(" AddUserGroup - Create a new user group.");
- console.log(" RemoveUserGroup - Delete a user group.");
- console.log(" AddToUserGroup - Add a user, device or device group to a user group.");
- console.log(" RemoveFromUserGroup - Remove a user, device or device group from a user group.");
- console.log(" RemoveAllUsersFromUserGroup - Remove all users from a user group.");
- console.log(" AddDeviceGroup - Create a new device group.");
- console.log(" RemoveDeviceGroup - Delete a device group.");
- console.log(" EditDeviceGroup - Change a device group values.");
- console.log(" MoveToDeviceGroup - Move a device to a different device group.");
- console.log(" AddUserToDeviceGroup - Add a user to a device group.");
- console.log(" RemoveUserFromDeviceGroup - Remove a user from a device group.");
- console.log(" AddUserToDevice - Add a user to a device.");
- console.log(" RemoveUserFromDevice - Remove a user from a device.");
- console.log(" SendInviteEmail - Send an agent install invitation email.");
- console.log(" GenerateInviteLink - Create an invitation link.");
- console.log(" Broadcast - Display a message to all online users.");
- console.log(" ShowEvents - Display real-time server events in JSON format.");
- console.log(" RunCommand - Run a shell command on a remote device.");
- console.log(" Shell - Access command shell of a remote device.");
- console.log(" Upload - Upload a file to a remote device.");
- console.log(" Download - Download a file from a remote device.");
- console.log(" WebRelay - Creates a HTTP/HTTPS webrelay link for a remote device.");
- console.log(" DeviceOpenUrl - Open a URL on a remote device.");
- console.log(" DeviceMessage - Open a message box on a remote device.");
- console.log(" DeviceToast - Display a toast notification on a remote device.");
- console.log(" GroupMessage - Open a message box on remote devices in a specific device group.");
- console.log(" GroupToast - Display a toast notification on remote devices in a specific device group.");
- console.log(" DevicePower - Perform wake/sleep/reset/off operations on remote devices.");
- console.log(" DeviceSharing - View, add and remove sharing links for a given device.");
- console.log(" AgentDownload - Download an agent of a specific type for a device group.");
- console.log(" Report - Create and show a CSV report.");
- console.log("\r\nSupported login arguments:");
- console.log(" --url [wss://server] - Server url, wss://localhost:443 is default.");
- console.log(" - Use wss://localhost:443?key=xxx if login key is required.");
- console.log(" --loginuser [username] - Login username, admin is default.");
- console.log(" --loginpass [password] - Login password OR Leave blank to enter password at prompt");
- console.log(" --token [number] - 2nd factor authentication token.");
- console.log(" --loginkey [hex] - Server login key in hex.");
- console.log(" --loginkeyfile [file] - File containing server login key in hex.");
- console.log(" --logindomain [domainid] - Domain id, default is empty, only used with loginkey.");
- console.log(" --proxy [http://proxy:123] - Specify an HTTP proxy.");
- return;
-} else {
- settings.cmd = args['_'][0].toLowerCase();
- if ((possibleCommands.indexOf(settings.cmd) == -1) && (settings.cmd != 'help')) { console.log("Invalid command. Possible commands are: " + possibleCommands.join(', ') + '.'); return; }
- //console.log(settings.cmd);
-
- var ok = false;
- switch (settings.cmd) {
- case 'config': { performConfigOperations(args); return; }
- case 'indexagenterrorlog': { indexAgentErrorLog(); return; }
- case 'serverinfo': { ok = true; break; }
- case 'userinfo': { ok = true; break; }
- case 'listusers': { ok = true; break; }
- case 'listusersessions': { ok = true; break; }
- case 'listusergroups': { ok = true; break; }
- case 'listdevicegroups': { ok = true; break; }
- case 'listdevices': { ok = true; break; }
- case 'listevents': { ok = true; break; }
- case 'logintokens': { ok = true; break; }
- case 'listusersofdevicegroup':
- case 'deviceinfo':
- case 'removedevice':
- case 'editdevice': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else { ok = true; }
- break;
- }
- case 'addlocaldevice': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else if (args.devicename == null) { console.log(winRemoveSingleQuotes("Missing devicename, use --devicename [devicename]")); }
- else if (args.hostname == null) { console.log(winRemoveSingleQuotes("Missing hostname, use --hostname [hostname]")); }
- else { ok = true; }
- break;
- }
- case 'addamtdevice': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else if (args.devicename == null) { console.log(winRemoveSingleQuotes("Missing devicename, use --devicename [devicename]")); }
- else if (args.hostname == null) { console.log(winRemoveSingleQuotes("Missing hostname, use --hostname [hostname]")); }
- else if (args.user == null) { console.log(winRemoveSingleQuotes("Missing user, use --user [user]")); }
- else if (args.pass == null) { console.log(winRemoveSingleQuotes("Missing pass, use --pass [pass]")); }
- else { ok = true; }
- break;
- }
- case 'addusertodevicegroup': {
- if ((args.id == null) && (args.group == null)) { console.log(winRemoveSingleQuotes("Device group identifier missing, use --id '[groupid]' or --group [groupname]")); }
- else if (args.userid == null) { console.log("Add user to group missing useid, use --userid [userid]"); }
- else { ok = true; }
- break;
- }
- case 'removeuserfromdevicegroup': {
- if ((args.id == null) && (args.group == null)) { console.log(winRemoveSingleQuotes("Device group identifier missing, use --id '[groupid]' or --group [groupname]")); }
- else if (args.userid == null) { console.log("Remove user from group missing useid, use --userid [userid]"); }
- else { ok = true; }
- break;
- }
- case 'addusertodevice': {
- if (args.userid == null) { console.log("Add user to device missing userid, use --userid [userid]"); }
- else if (args.id == null) { console.log(winRemoveSingleQuotes("Add user to device missing device id, use --id '[deviceid]'")); }
- else { ok = true; }
- break;
- }
- case 'removeuserfromdevice': {
- if (args.userid == null) { console.log("Remove user from device missing userid, use --userid [userid]"); }
- else if (args.id == null) { console.log(winRemoveSingleQuotes("Remove user from device missing device id, use --id '[deviceid]'")); }
- else { ok = true; }
- break;
- }
- case 'adddevicegroup': {
- if (args.name == null) { console.log("Message group name, use --name [name]"); }
- else { ok = true; }
- break;
- }
- case 'editdevicegroup':
- case 'removedevicegroup': {
- if ((args.id == null) && (args.group == null)) { console.log(winRemoveSingleQuotes("Device group identifier missing, use --id '[groupid]' or --group [groupname]")); }
- else { ok = true; }
- break;
- }
- case 'movetodevicegroup': {
- if ((args.id == null) && (args.group == null)) { console.log(winRemoveSingleQuotes("Device group identifier missing, use --id '[groupid]' or --group [groupname]")); }
- else if (args.devid == null) { console.log(winRemoveSingleQuotes("Device identifier missing, use --devid '[deviceid]'")); }
- else { ok = true; }
- break;
- }
- case 'broadcast': {
- if (args.msg == null) { console.log("Message missing, use --msg [message]"); }
- else { ok = true; }
- break;
- }
- case 'showevents': {
- ok = true;
- break;
- }
- case 'adduser': {
- if (args.user == null) { console.log("New account name missing, use --user [name]"); }
- else if ((args.pass == null) && (args.randompass == null)) { console.log("New account password missing, use --pass [password] or --randompass"); }
- else { ok = true; }
- break;
- }
- case 'edituser': {
- if (args.userid == null) { console.log("Edit account user missing, use --userid [id]"); }
- else { ok = true; }
- break;
- }
- case 'removeuser': {
- if (args.userid == null) { console.log("Remove account userid missing, use --userid [id]"); }
- else { ok = true; }
- break;
- }
- case 'addusergroup': {
- if (args.name == null) { console.log("New user group name missing, use --name [name]"); }
- else { ok = true; }
- break;
- }
- case 'removeusergroup': {
- if (args.groupid == null) { console.log(winRemoveSingleQuotes("Remove user group id missing, use --groupid '[id]'")); }
- else { ok = true; }
- break;
- }
- case 'addtousergroup': {
- if (args.groupid == null) { console.log(winRemoveSingleQuotes("Group id missing, use --groupid '[id]'")); }
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing identifier to add, use --id [id]")); }
- else { ok = true; }
- break;
- }
- case 'removefromusergroup': {
- if (args.groupid == null) { console.log(winRemoveSingleQuotes("Group id missing, use --groupid '[id]'")); }
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing identifier to remove, use --id [id]")); }
- else { ok = true; }
- break;
- }
- case 'removeallusersfromusergroup': {
- if (args.groupid == null) { console.log(winRemoveSingleQuotes("Group id missing, use --groupid '[id]'")); }
- else { ok = true; }
- break;
- }
- case 'sendinviteemail': {
- if ((args.id == null) && (args.group == null)) { console.log("Device group identifier missing, use --id '[groupid]' or --group [groupname]"); }
- else if (args.email == null) { console.log("Device email is missing, use --email [email]"); }
- else { ok = true; }
- break;
- }
- case 'generateinvitelink': {
- if ((args.id == null) && (args.group == null)) { console.log("Device group identifier missing, use --id '[groupid]' or --group [groupname]"); }
- else if (args.hours == null) { console.log("Invitation validity period missing, use --hours [hours]"); }
- else { ok = true; }
- break;
- }
- case 'runcommand': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else if (args.run == null) { console.log("Missing run, use --run \"command\""); }
- else { ok = true; }
- break;
- }
- case 'shell': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else { ok = true; }
- break;
- }
- case 'devicepower': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else { ok = true; }
- break;
- }
- case 'devicesharing': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else if ((args.daily != null) && (args.weekly != null)) { console.log(winRemoveSingleQuotes("Can't specify both --daily and --weekly at the same time.")); }
- else { ok = true; }
- break;
- }
- case 'agentdownload': {
- if (args.type == null) { console.log(winRemoveSingleQuotes("Missing device type, use --type [agenttype]")); }
- else if ((parseInt(args.type) == null) || isNaN(parseInt(args.type)) || (parseInt(args.type) < 1) || (parseInt(args.type) > 11000)) { console.log(winRemoveSingleQuotes("Invalid agent type, must be a number.")); }
- else if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[meshid]'")); }
- else if ((typeof args.id != 'string') || (args.id.length != 64)) { console.log(winRemoveSingleQuotes("Invalid meshid.")); }
- else { ok = true; }
- break;
- }
- case 'upload': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else if (args.file == null) { console.log("Local file missing, use --file [file] specify the file to upload"); }
- else if (args.target == null) { console.log("Remote target path missing, use --target [path] to specify the remote location"); }
- else if (require('fs').existsSync(args.file) == false) { console.log("Local file does not exists, check --file"); }
- else { ok = true; }
- break;
- }
- case 'download': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else if (args.file == null) { console.log("Remote file missing, use --file [file] specify the remote file to download"); }
- else if (args.target == null) { console.log("Target path missing, use --target [path] to specify the local download location"); }
- else { ok = true; }
- break;
- }
- case 'webrelay': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else if (args.type == null) { console.log(winRemoveSingleQuotes("Missing protocol type, use --type [http,https]")); }
- else { ok = true; }
- break;
- }
- case 'deviceopenurl': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else if (args.openurl == null) { console.log("Remote URL, use --openurl [url] specify the link to open."); }
- else { ok = true; }
- break;
- }
- case 'devicemessage': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else if (args.msg == null) { console.log("Remote message, use --msg \"[message]\" specify a remote message."); }
- else { ok = true; }
- break;
- }
- case 'devicetoast': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
- else if (args.msg == null) { console.log("Remote message, use --msg \"[message]\" specify a remote message."); }
- else { ok = true; }
- break;
- }
- case 'groupmessage': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device group id, use --id '[devicegroupid]'")); }
- else if (args.msg == null) { console.log("Remote message, use --msg \"[message]\" specify a remote message."); }
- else { ok = true; }
- break;
- }
- case 'grouptoast': {
- if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device group id, use --id '[devicegroupid]'")); }
- else if (args.msg == null) { console.log("Remote message, use --msg \"[message]\" specify a remote message."); }
- else { ok = true; }
- break;
- }
- case 'report': {
- if (args.type == null) { console.log(winRemoveSingleQuotes("Missing report type, use --type '[reporttype]'")); }
- else { ok = true; }
- break;
- }
- case 'help': {
- if (args['_'].length < 2) {
- console.log("Get help on an action. Type:\r\n\r\n help [action]\r\n\r\nPossible actions are: " + possibleCommands.join(', ') + '.');
- } else {
- switch (args['_'][1].toLowerCase()) {
- case 'config': {
- displayConfigHelp();
- break;
- }
- case 'sendinviteemail': {
- console.log("Send invitation email with instructions on how to install the mesh agent for a specific device group. Example usage:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl SendInviteEmail --id 'groupid' --message \"msg\" --email user@sample.com"));
- console.log(winRemoveSingleQuotes(" MeshCtrl SendInviteEmail --group \"My Computers\" --name \"Jack\" --email user@sample.com"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [groupid] - Device group identifier (or --group).");
- } else {
- console.log(" --id '[groupid]' - Device group identifier (or --group).");
- }
- console.log(" --group [groupname] - Device group name (or --id).");
- console.log(" --email [email] - Email address.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --name (name) - Name of recipient to be included in the email.");
- console.log(" --message (msg) - Message to be included in the email.");
- break;
- }
- case 'generateinvitelink': {
- console.log("Generate a agent invitation URL for a given group. Example usage:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl GenerateInviteLink --id 'groupid' --hours 24"));
- console.log(" MeshCtrl GenerateInviteLink --group \"My Computers\" --hours 0");
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [groupid] - Device group identifier (or --group).");
- } else {
- console.log(" --id '[groupid]' - Device group identifier (or --group).");
- }
- console.log(" --group [groupname] - Device group name (or --id).");
- console.log(" --hours [hours] - Validity period in hours or 0 for infinite.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --flags [mode] - Mode flag for link type (0 = both, 1 = interactive only, 2 = background only)");
- break;
- }
- case 'showevents': {
- console.log("Show the server's event stream for this user account. Example usage:\r\n");
- console.log(" MeshCtrl ShowEvents");
- console.log(" MeshCtrl ShowEvents --filter nodeconnect");
- console.log(" MeshCtrl ShowEvents --filter uicustomevent,changenode");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --filter [actions] - Show only specified actions.");
- break;
- }
- case 'serverinfo': {
- console.log("Get information on the MeshCentral server, Example usages:\r\n");
- console.log(" MeshCtrl ServerInfo --loginuser myaccountname --loginpass mypassword");
- console.log(" MeshCtrl ServerInfo --loginuser myaccountname --loginkeyfile key.txt");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --json - Show result as JSON.");
- break;
- }
- case 'userinfo': {
- console.log("Get account information for the login account, Example usages:\r\n");
- console.log(" MeshCtrl UserInfo --loginuser myaccountname --loginpass mypassword");
- console.log(" MeshCtrl UserInfo --loginuser myaccountname --loginkeyfile key.txt");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --json - Show result as JSON.");
- break;
- }
- case 'listusers': {
- console.log("List the account on the MeshCentral server, Example usages:\r\n");
- console.log(" MeshCtrl ListUsers");
- console.log(" MeshCtrl ListUsers --json");
- console.log(" MeshCtrl ListUsers --nameexists \"bob\"");
- console.log(" MeshCtrl ListUsers --filter 2fa");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --idexists [id] - Return 1 if id exists, 0 if not.");
- console.log(" --nameexists [name] - Return id if name exists.");
- console.log(" --filter [filter1,...] - Filter user names: 2FA, NO2FA.");
- console.log(" --json - Show result as JSON.");
- break;
- }
- case 'listusersessions': {
- console.log("List active user sessions on the MeshCentral server, Example usages:\r\n");
- console.log(" MeshCtrl ListUserSessions");
- console.log(" MeshCtrl ListUserSessions --json");
- break;
- }
- case 'listusergroups': {
- console.log("List user groups on the MeshCentral server, Example usages:\r\n");
- console.log(" MeshCtrl ListUserGroups");
- console.log(" MeshCtrl ListUserGroups --json");
- break;
- }
- case 'listdevicegroups': {
- console.log("List the device groups for this account. Example usages:\r\n");
- console.log(" MeshCtrl ListDeviceGroups ");
- console.log(" MeshCtrl ListDeviceGroups --json");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --idexists [id] - Return 1 if id exists, 0 if not.");
- console.log(" --nameexists [name] - Return id if name exists.");
- console.log(" --emailexists [email] - Return id if email exists.");
- console.log(" --hex - Display meshid in hex format.");
- console.log(" --json - Show result as JSON.");
- break;
- }
- case 'listdevices': {
- console.log("List devices. Example usages:\r\n");
- console.log(" MeshCtrl ListDevices");
- console.log(winRemoveSingleQuotes(" MeshCtrl ListDevices -id '[groupid]' --json"));
- console.log("\r\nOptional arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [groupid] - Filter by group identifier (or --group).");
- } else {
- console.log(" --id '[groupid]' - Filter by group identifier (or --group).");
- }
- console.log(" --group [groupname] - Filter by group name (or --id).");
- console.log(" --count - Only return the device count.");
- console.log(" --json - Show result as JSON.");
- console.log(" --csv - Show result as comma separated values.");
- console.log(" --filter \"[filter]\" - Filter devices using a filter string.");
- console.log(" \"x\" - Devices with \"x\" in the name.");
- console.log(" \"user:x or u:x\" - Devices with \"x\" in the name of currently logged in user.");
- console.log(" \"ip:x\" - Devices \"x\" IP address.");
- console.log(" \"group:x or g:x\" - Devices with \"x\" in device group name.");
- console.log(" \"tag:x or t:x\" - Devices with \"x\" in device tag.");
- console.log(" \"atag:x or a:x\" - Devices with \"x\" in device agent tag.");
- console.log(" \"os:x\" - Devices with \"x\" in the device OS description.");
- console.log(" \"amt:x\" - Devices with Intel AMT provisioning state (0, 1, 2).");
- console.log(" \"desc:x\" - Devices with \"x\" in device description.");
- console.log(" \"wsc:ok\" - Devices with Windows Security Center ok.");
- console.log(" \"wsc:noav\" - Devices with Windows Security Center with anti-virus problem.");
- console.log(" \"wsc:noupdate\" - Devices with Windows Security Center with update problem.");
- console.log(" \"wsc:nofirewall\" - Devices with Windows Security Center with firewall problem.");
- console.log(" \"wsc:any\" - Devices with Windows Security Center with any problem.");
- console.log(" \"a and b\" - Match both conditions with precedence over OR. For example: \"lab and g:home\".");
- console.log(" \"a or b\" - Math one of the conditions, for example: \"lab or g:home\".");
- console.log(" --filterid [id,id...] - Show only results for devices with included id.");
- console.log(" --details - Show all device details.");
- break;
- }
- case 'listusersofdevicegroup': {
- console.log("List users that have permissions for a given device group. Example usage:\r\n");
- console.log(" MeshCtrl ListUserOfDeviceGroup ");
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [groupid] - Device group identifier.");
- } else {
- console.log(" --id '[groupid]' - Device group identifier.");
- }
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --json - Show result as JSON.");
- break;
- }
- case 'listevents': {
- console.log("List server events optionally filtered by user or device. Example usage:\r\n");
- console.log(" MeshCtrl ListEvents ");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --userid [name] - User account identifier.");
- console.log(" --id [deviceid] - The device identifier.");
- console.log(" --limit [number] - Maximum number of events to list.");
- console.log(" --raw - Output raw data in JSON format.");
- console.log(" --json - Give results in JSON format.");
- break;
- }
- case 'logintokens': {
- console.log("List account login tokens and allow addition and removal. Example usage:\r\n");
- console.log(" MeshCtrl LoginTokens ");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --remove [name] - Remove a login token.");
- console.log(" --add [name] - Add a login token.");
- console.log(" --expire [minutes] - When adding a token, minutes until expire.");
- console.log(" --json - Show login tokens in JSON format.");
- break;
- }
- case 'adduser': {
- console.log("Add a new user account. Example usages:\r\n");
- console.log(" MeshCtrl AddUser --user newaccountname --pass newpassword");
- console.log(" MeshCtrl AddUser --user newaccountname --randompass --rights full");
- console.log("\r\nRequired arguments:\r\n");
- console.log(" --user [name] - New account name.");
- console.log(" --pass [password] - New account password.");
- console.log(" --randompass - Create account with a random password.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --domain [domain] - Account domain, only for cross-domain admins.");
- console.log(" --email [email] - New account email address.");
- console.log(" --emailverified - New account email is verified.");
- console.log(" --resetpass - Request password reset on next login.");
- console.log(" --realname [name] - Set the real name for this account.");
- console.log(" --phone [number] - Set the account phone number.");
- console.log(" --rights [none|full|a,b,c] - Comma separated list of server permissions. Possible values:");
- console.log(" manageusers,serverbackup,serverrestore,serverupdate,fileaccess,locked,nonewgroups,notools,usergroups,recordings,locksettings,allevents,nonewdevices");
- break;
- }
- case 'edituser': {
- console.log("Edit a user account, Example usages:\r\n");
- console.log(" MeshCtrl EditUser --userid user --rights locked,locksettings");
- console.log(" MeshCtrl EditUser --userid user --realname Jones");
- console.log("\r\nRequired arguments:\r\n");
- console.log(" --userid [name] - User account identifier.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --domain [domain] - Account domain, only for cross-domain admins.");
- console.log(" --email [email] - Account email address.");
- console.log(" --emailverified - Account email is verified.");
- console.log(" --resetpass - Request password reset on next login.");
- console.log(" --realname [name] - Set the real name for this account.");
- console.log(" --phone [number] - Set the account phone number.");
- console.log(" --rights [none|full|a,b,c] - Comma separated list of server permissions. Possible values:");
- console.log(" manageusers,serverbackup,serverrestore,serverupdate,fileaccess,locked,nonewgroups,notools,usergroups,recordings,locksettings,allevents,nonewdevices");
- break;
- }
- case 'removeuser': {
- console.log("Delete a user account, Example usages:\r\n");
- console.log(" MeshCtrl RemoveUser --userid accountid");
- console.log("\r\nRequired arguments:\r\n");
- console.log(" --userid [id] - Account identifier.");
- break;
- }
- case 'addusergroup': {
- console.log("Create a new user group, Example usages:\r\n");
- console.log(" MeshCtrl AddUserGroup --name \"Test Group\"");
- console.log("\r\nRequired arguments:\r\n");
- console.log(" --name [name] - Name of the user group.");
- break;
- }
- case 'removeusergroup': {
- console.log("Remove a user group, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl RemoveUserGroup --groupid 'ugrp//abcdf'"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --groupid [groupid] - User group identifier.");
- } else {
- console.log(" --groupid '[groupid]' - User group identifier.");
- }
- break;
- }
- case 'addtousergroup': {
- console.log("Add a user, device or device group to a user group, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl AddToUserGroup --id 'user//abcdef' --groupid 'ugrp//abcdf'"));
- console.log(winRemoveSingleQuotes(" MeshCtrl AddToUserGroup --id 'node//abcdef' --groupid 'ugrp//abcdf' --rights [rights]"));
- console.log(winRemoveSingleQuotes(" MeshCtrl AddToUserGroup --id 'mesh//abcdef' --groupid 'ugrp//abcdf' --rights [rights]"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [id] - Identifier to add.");
- console.log(" --groupid [groupid] - User group identifier.");
- } else {
- console.log(" --id '[id]' - Identifier to add.");
- console.log(" --groupid '[groupid]' - User group identifier.");
- }
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --rights [number] - Rights granted for adding device or device group.");
- console.log(" - 4294967295 for full admin or the sum of the following numbers.");
- console.log(" 1 = Edit Device Group 2 = Manage Users ");
- console.log(" 4 = Manage Computers 8 = Remote Control ");
- console.log(" 16 = Agent Console 32 = Server Files ");
- console.log(" 64 = Wake Device 128 = Set Notes ");
- console.log(" 256 = Remote View Only 512 = No Terminal ");
- console.log(" 1024 = No Files 2048 = No Intel AMT ");
- console.log(" 4096 = Desktop Limited Input 8192 = Limit Events ");
- console.log(" 16384 = Chat / Notify 32768 = Uninstall Agent ");
- console.log(" 65536 = No Remote Desktop 131072 = Remote Commands ");
- console.log(" 262144 = Reset / Power off ");
- break;
- }
- case 'removefromusergroup': {
- console.log("Remove a user, device or device group from a user group, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl RemoveUserFromUserGroup --userid 'user//abcdef' --groupid 'ugrp//abcdf'"));
- console.log(winRemoveSingleQuotes(" MeshCtrl RemoveUserFromUserGroup --userid 'node//abcdef' --groupid 'ugrp//abcdf'"));
- console.log(winRemoveSingleQuotes(" MeshCtrl RemoveUserFromUserGroup --userid 'mesh//abcdef' --groupid 'ugrp//abcdf'"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [userid] - Identifier to remove.");
- console.log(" --groupid [groupid] - User group identifier.");
- } else {
- console.log(" --id '[userid]' - Identifier to remove.");
- console.log(" --groupid '[groupid]' - User group identifier.");
- }
- break;
- }
- case 'removeallusersfromusergroup': {
- console.log("Remove all users from a user group, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl RemoveAllUsersFromUserGroup --groupid 'ugrp//abcdf'"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --groupid [groupid] - User group identifier.");
- } else {
- console.log(" --groupid '[groupid]' - User group identifier.");
- }
- break;
- }
- case 'adddevicegroup': {
- console.log("Add a device group, Example usages:\r\n");
- console.log(" MeshCtrl AddDeviceGroup --name newgroupname");
- console.log(" MeshCtrl AddDeviceGroup --name newgroupname --desc description --amtonly");
- console.log(" MeshCtrl AddDeviceGroup --name newgroupname --features 1 --consent 7");
- console.log("\r\nRequired arguments:\r\n");
- console.log(" --name [name] - Name of the new group.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --desc [description] - New group description.");
- console.log(" --amtonly - New group is agent-less, Intel AMT only.");
- console.log(" --agentless - New group is agent-less only.");
- console.log(" --features [number] - Set device group features, sum of numbers below.");
- console.log(" 1 = Auto-Remove 2 = Hostname Sync");
- console.log(" 4 = Record Sessions");
- console.log(" --consent [number] - Set device group user consent, sum of numbers below.");
- console.log(" 1 = Desktop notify user 2 = Terminal notify user ");
- console.log(" 4 = Files notify user 8 = Desktop prompt user ");
- console.log(" 16 = Terminal prompt user 32 = Files prompt user ");
- console.log(" 64 = Desktop Toolbar ");
- break;
- }
- case 'removedevicegroup': {
- console.log("Remove a device group, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl RemoveDeviceGroup --id 'groupid'"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [groupid] - Device group identifier (or --group).");
- } else {
- console.log(" --id '[groupid]' - Device group identifier (or --group).");
- }
- console.log(" --group [groupname] - Device group name (or --id).");
- break;
- }
- case 'editdevicegroup': {
- console.log("Edit a device group, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl EditDeviceGroup --id 'groupid' --name \"New Name\""));
- console.log(winRemoveSingleQuotes(" MeshCtrl EditDeviceGroup --id 'groupid' --desc \"Description\" --consent 63"));
- console.log(winRemoveSingleQuotes(" MeshCtrl EditDeviceGroup --id 'groupid' --invitecodes \"code1,code2\" --backgroundonly"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [groupid] - Device group identifier (or --group).");
- } else {
- console.log(" --id '[groupid]' - Device group identifier (or --group).");
- }
- console.log(" --group [groupname] - Device group name (or --id).");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --name [name] - Set new device group name.");
- console.log(" --desc [description] - Set new device group description, blank to clear.");
- console.log(" --flags [number] - Set device group flags, sum of the values below, 0 for none.");
- console.log(" 1 = Auto remove device on disconnect.");
- console.log(" 2 = Sync hostname.");
- console.log(" --consent [number] - Set device group consent options, sum of the values below, 0 for none.");
- console.log(" 1 = Desktop notify user.");
- console.log(" 2 = Terminal notify user.");
- console.log(" 4 = Files notify user.");
- console.log(" 8 = Desktop prompt for user consent.");
- console.log(" 16 = Terminal prompt for user consent.");
- console.log(" 32 = Files prompt for user consent.");
- console.log(" 64 = Desktop show connection toolbar.");
- console.log(" --invitecodes [aa,bb] - Comma separated list of invite codes, blank to clear.");
- console.log(" --backgroundonly - When used with invitecodes, set agent to only install in background.");
- console.log(" --interactiveonly - When used with invitecodes, set agent to only run on demand.");
- break;
- }
- case 'movetodevicegroup': {
- console.log("Move a device to a new device group, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl MoveToDeviceGroup --devid 'deviceid' --id 'groupid'"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [groupid] - Device group identifier (or --group).");
- } else {
- console.log(" --id '[groupid]' - Device group identifier (or --group).");
- }
- console.log(" --group [groupname] - Device group name (or --id).");
- if (process.platform == 'win32') {
- console.log(" --devid [deviceid] - Device identifier.");
- } else {
- console.log(" --devid '[deviceid]' - Device identifier.");
- }
- break;
- }
- case 'addusertodevicegroup': {
- console.log("Add a user to a device group, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl AddUserToDeviceGroup --id 'groupid' --userid userid --fullrights"));
- console.log(" MeshCtrl AddUserToDeviceGroup --group groupname --userid userid --editgroup --manageusers");
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [groupid] - Device group identifier (or --group).");
- } else {
- console.log(" --id '[groupid]' - Device group identifier (or --group).");
- }
- console.log(" --group [groupname] - Device group name (or --id).");
- console.log(" --userid [userid] - The user identifier.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --fullrights - Allow full rights over this device group.");
- console.log(" --editgroup - Allow the user to edit group information.");
- console.log(" --manageusers - Allow the user to add/remove users.");
- console.log(" --managedevices - Allow the user to edit device information.");
- console.log(" --remotecontrol - Allow device remote control operations.");
- console.log(" --agentconsole - Allow agent console operations.");
- console.log(" --serverfiles - Allow access to group server files.");
- console.log(" --wakedevices - Allow device wake operation.");
- console.log(" --notes - Allow editing of device notes.");
- console.log(" --desktopviewonly - Restrict user to view-only remote desktop.");
- console.log(" --limiteddesktop - Limit remote desktop keys.");
- console.log(" --noterminal - Hide the terminal tab from this user.");
- console.log(" --nofiles - Hide the files tab from this user.");
- console.log(" --noamt - Hide the Intel AMT tab from this user.");
- console.log(" --limitedevents - User can only see his own events.");
- console.log(" --chatnotify - Allow chat and notification options.");
- console.log(" --uninstall - Allow remote uninstall of the agent.");
- if (args.limiteddesktop) { meshrights |= 4096; }
- if (args.limitedevents) { meshrights |= 8192; }
- if (args.chatnotify) { meshrights |= 16384; }
- if (args.uninstall) { meshrights |= 32768; }
-
- break;
- }
- case 'removeuserfromdevicegroup': {
- console.log("Remove a user from a device group, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl RemoveuserFromDeviceGroup --id 'groupid' --userid userid"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [groupid] - Device group identifier (or --group).");
- } else {
- console.log(" --id '[groupid]' - Device group identifier (or --group).");
- }
- console.log(" --group [groupname] - Device group name (or --id).");
- console.log(" --userid [userid] - The user identifier.");
- break;
- }
- case 'addusertodevice': {
- console.log("Add a user to a device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl AddUserToDevice --id 'deviceid' --userid userid --fullrights"));
- console.log(winRemoveSingleQuotes(" MeshCtrl AddUserToDevice --id 'deviceid' --userid userid --remotecontrol"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log(" --userid [userid] - The user identifier.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --fullrights - Allow full rights over this device.");
- console.log(" --remotecontrol - Allow device remote control operations.");
- console.log(" --agentconsole - Allow agent console operations.");
- console.log(" --serverfiles - Allow access to group server files.");
- console.log(" --wakedevices - Allow device wake operation.");
- console.log(" --notes - Allow editing of device notes.");
- console.log(" --desktopviewonly - Restrict user to view-only remote desktop.");
- console.log(" --limiteddesktop - Limit remote desktop keys.");
- console.log(" --noterminal - Hide the terminal tab from this user.");
- console.log(" --nofiles - Hide the files tab from this user.");
- console.log(" --noamt - Hide the Intel AMT tab from this user.");
- console.log(" --limitedevents - User can only see his own events.");
- console.log(" --chatnotify - Allow chat and notification options.");
- console.log(" --uninstall - Allow remote uninstall of the agent.");
- break;
- }
- case 'removeuserfromdevice': {
- console.log("Remove a user from a device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl RemoveuserFromDeviceGroup --id 'deviceid' --userid userid"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log(" --userid [userid] - The user identifier.");
- break;
- }
- case 'broadcast': {
- console.log("Display a message to one or all logged in users, Example usages:\r\n");
- console.log(" MeshCtrl Broadcast --msg \"This is a test\"");
- console.log("\r\nRequired arguments:\r\n");
- console.log(" --msg [message] - Message to display.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --user [userid] - Send the message to the specified user.");
- break;
- }
- case 'deviceinfo': {
- console.log("Display information about a device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceInfo --id 'deviceid'"));
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceInfo --id 'deviceid' --json"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --raw - Output raw data in JSON format.");
- console.log(" --json - Give results in JSON format.");
- break;
- }
- case 'removedevice': {
- console.log("Delete a device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl RemoveDevice --id 'deviceid'"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- break;
- }
- case 'addlocaldevice': {
- console.log("Add a Local Device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl AddLocalDevice --id 'meshid' --devicename 'devicename' --hostname 'hostname'"));
- console.log(winRemoveSingleQuotes(" MeshCtrl AddLocalDevice --id 'meshid' --devicename 'devicename' --hostname 'hostname' --type 6"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [meshid] - The mesh identifier.");
- console.log(" --devicename [devicename] - The device name.");
- console.log(" --hostname [hostname] - The devices hostname or ip address.");
- } else {
- console.log(" --id '[meshid]' - The mesh identifier.");
- console.log(" --devicename '[devicename]' - The device name.");
- console.log(" --hostname '[hostname]' - The devices hostname or ip address.");
- }
-
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --type [TypeNumber] - With the following choices:");
- console.log(" type 4 - Default, Windows (RDP)");
- console.log(" type 6 - Linux (SSH/SCP/VNC)");
- console.log(" type 29 - macOS (SSH/SCP/VNC)");
- break;
- }
- case 'addamtdevice': {
- console.log("Add an Intel AMT Device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl AddAmtDevice --id 'meshid' --devicename 'devicename' --hostname 'hostname --user 'admin' --pass 'admin'"));
- console.log(winRemoveSingleQuotes(" MeshCtrl AddAmtDevice --id 'meshid' --devicename 'devicename' --hostname 'hostname --user 'admin' --pass 'admin' --notls"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [meshid] - The mesh identifier.");
- console.log(" --devicename [devicename] - The device name.");
- console.log(" --hostname [hostname] - The devices hostname or ip address.");
- console.log(" --user [user] - The devices AMT username.");
- console.log(" --pass [pass] - The devices AMT password.");
- console.log("")
- } else {
- console.log(" --id '[meshid]' - The mesh identifier.");
- console.log(" --devicename '[devicename]' - The device name.");
- console.log(" --hostname '[hostname]' - The devices hostname or ip address.");
- console.log(" --user '[user]' - The devices AMT username.");
- console.log(" --pass '[pass]' - The devices AMT password.");
- }
- console.log("\r\nOptional arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --notls - Use No TLS Security.");
- } else {
- console.log(" --notls - Use No TLS Security.");
- }
- break;
- }
- case 'editdevice': {
- console.log("Change information about a device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl EditDevice --id 'deviceid' --name 'device1'"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log("\r\nOptional arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --name [name] - Change device name.");
- console.log(" --desc [description] - Change device description.");
- console.log(" --tags [tag1,tags2] - Change device tags.");
- } else {
- console.log(" --name '[name]' - Change device name.");
- console.log(" --desc '[description]' - Change device description.");
- console.log(" --tags '[tag1,tags2]' - Change device tags.");
- }
- console.log(" --icon [number] - Change the device icon (1 to 8).");
- console.log(" --consent [flags] - Sum of the following numbers:");
- console.log(" 1 = Desktop notify 2 = Terminal notify");
- console.log(" 4 = Files notify 8 = Desktop prompt");
- console.log(" 16 = Terminal prompt 32 = Files prompt");
- console.log(" 64 = Desktop privacy bar");
- break;
- }
- case 'runcommand': {
- console.log("Run a shell command on a remote device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl RunCommand --id 'deviceid' --run \"command\""));
- console.log(winRemoveSingleQuotes(" MeshCtrl RunCommand --id 'deviceid' --run \"command\" --powershell"));
- console.log(winRemoveSingleQuotes(" MeshCtrl RunCommand --id 'deviceid' --run \"command\" --reply"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log(" --run \"[command]\" - Shell command to execute on the remote device.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --powershell - Run in Windows PowerShell.");
- console.log(" --runasuser - Attempt to run the command as logged in user.");
- console.log(" --runasuseronly - Only run the command as the logged in user.");
- console.log(" --reply - Return with the output from running the command.");
- break;
- }
- case 'shell': {
- console.log("Access a command shell on a remote device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl Shell --id 'deviceid'"));
- console.log(winRemoveSingleQuotes(" MeshCtrl Shell --id 'deviceid' --powershell"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --powershell - Run a Windows PowerShell.");
- break;
- }
- case 'devicepower': {
- console.log("Perform power operations on remote devices, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl DevicePower --wake --id 'deviceid'"));
- console.log(winRemoveSingleQuotes(" MeshCtrl DevicePower --sleep --id 'deviceid'"));
- console.log(winRemoveSingleQuotes(" MeshCtrl DevicePower --reset --id 'deviceid'"));
- console.log(winRemoveSingleQuotes(" MeshCtrl DevicePower --off --id 'deviceid1,deviceid2'"));
- console.log("\r\nNote that some power operations may take up to a minute to execute.\r\n");
- console.log("Required arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid1,deviceid2] - Device identifiers.");
- } else {
- console.log(" --id '[deviceid1,deviceid2]' - Device identifiers.");
- }
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --wake - Attempt to wake up the remote device.");
- console.log(" --reset - Attempt to remote the remote device.");
- console.log(" --sleep - Attempt to place the remote device in low power mode.");
- console.log(" --off - Attempt to power off the remote device.");
- console.log(" --amtoff - Attempt to power off the remote device using Intel AMT.");
- console.log(" --amton - Attempt to power on the remote device using Intel AMT.");
- console.log(" --amtreset - Attempt to reset the remote device using Intel AMT.");
- break;
- }
- case 'devicesharing': {
- var tzoffset = (new Date()).getTimezoneOffset() * 60000; // Offset in milliseconds
- var localISOTime = (new Date(Date.now() - tzoffset)).toISOString().slice(0, -5);
- console.log("List sharing links for a specified device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid'"));
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --remove abcdef"));
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --add Guest --start " + localISOTime + " --duration 30"));
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --add Guest --start " + localISOTime + " --duration 30 --daily"));
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --add Guest --type desktop,terminal --consent prompt"));
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --add Guest --type http --port 80"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --remove [shareid] - Remove a device sharing link.");
- console.log(" --add [guestname] - Add a device sharing link.");
- console.log(" --type [desktop,terminal,files,http,https] - Type of sharing to add, can be combined. default is desktop.");
- console.log(" --viewonly - Make desktop sharing view only.");
- console.log(" --consent [notify,prompt,none] - Consent flags, default is notify.");
- console.log(" --start [yyyy-mm-ddThh:mm:ss] - Start time, default is now.");
- console.log(" --end [yyyy-mm-ddThh:mm:ss] - End time.");
- console.log(" --duration [minutes] - Duration of the share, default is 60 minutes.");
- console.log(" --daily - Add recurring daily device share.");
- console.log(" --weekly - Add recurring weekly device share.");
- console.log(" --port [portnumber] - Set alternative port for http or https, default is 80 for http and 443 for https.");
- break;
- }
- case 'agentdownload': {
- console.log("Download an agent of a specific type for a given device group, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl AgentDownload --id 'groupid' --type 3"));
- console.log(winRemoveSingleQuotes(" MeshCtrl AgentDownload --id 'groupid' --type 3 --installflags 1"));
- console.log("\r\nRequired arguments:\r\n");
- console.log(" --type [ArchitectureNumber] - Agent architecture number.");
- if (process.platform == 'win32') {
- console.log(" --id [groupid] - The device group identifier.");
- } else {
- console.log(" --id '[groupid]' - The device group identifier.");
- }
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --installflags [InstallFlagsNumber] - With the following choices:");
- console.log(" installflags 0 - Default, Interactive & Background, offers connect button & install/uninstall");
- console.log(" installflags 1 - Interactive only, offers only connect button, not install/uninstall");
- console.log(" installflags 2 - Background only, offers only install/uninstall, not connect");
- break;
- }
- case 'upload': {
- console.log("Upload a local file to a remote device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl Upload --id 'deviceid' --file sample.txt --target c:\\"));
- console.log(winRemoveSingleQuotes(" MeshCtrl Upload --id 'deviceid' --file sample.txt --target /tmp"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log(" --file [localfile] - The local file to upload.");
- console.log(" --target [remotepath] - The remote path to upload the file to.");
- break;
- }
- case 'download': {
- console.log("Download a file from a remote device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl Download --id 'deviceid' --file C:\\sample.txt --target c:\\temp"));
- console.log(winRemoveSingleQuotes(" MeshCtrl Download --id 'deviceid' --file /tmp/sample.txt --target /tmp"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log(" --file [remotefile] - The remote file to download.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --target [localpath] - The local path to download the file to.");
- break;
- }
- case 'webrelay': {
- console.log("Generate a webrelay URL to access a HTTP/HTTPS service on a remote device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl WebRelay --id 'deviceid' --type http --port 80"));
- console.log(winRemoveSingleQuotes(" MeshCtrl WebRelay --id 'deviceid' --type https --port 443"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log(" --type [http,https] - Type of relay from remote device, http or https.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --port [portnumber] - Set alternative port for http or https, default is 80 for http and 443 for https.");
- break;
- }
- case 'deviceopenurl': {
- console.log("Open a web page on a remote device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceOpenUrl --id 'deviceid' --openurl http://meshcentral.com"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log(" --openurl [url] - Link to the web page.");
- break;
- }
- case 'devicemessage': {
- console.log("Display a message on the remote device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceMessage --id 'deviceid' --msg \"message\""));
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceMessage --id 'deviceid' --msg \"message\" --title \"title\""));
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceMessage --id 'deviceid' --msg \"message\" --title \"title\" --timeout 120000"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log(" --msg [message] - The message to display.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --title [title] - Messagebox title, default is \"MeshCentral\".");
- console.log(" --timeout [miliseconds] - After timeout messagebox vanishes, 0 keeps messagebox open until closed manually, default is 120000 (2 Minutes).");
- break;
- }
- case 'devicetoast': {
- console.log("Display a toast message on the remote device, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceToast --id 'deviceid' --msg \"message\""));
- console.log(winRemoveSingleQuotes(" MeshCtrl DeviceToast --id 'deviceid' --msg \"message\" --title \"title\""));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [deviceid] - The device identifier.");
- } else {
- console.log(" --id '[deviceid]' - The device identifier.");
- }
- console.log(" --msg [message] - The message to display.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --title [title] - Toast title, default is \"MeshCentral\".");
- break;
- }
- case 'groupmessage': {
- console.log("Open a message box on remote devices in a specific device group, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl GroupMessage --id 'devicegroupid' --msg \"message\""));
- console.log(winRemoveSingleQuotes(" MeshCtrl GroupMessage --id 'devicegroupid' --msg \"message\" --title \"title\""));
- console.log(winRemoveSingleQuotes(" MeshCtrl GroupMessage --id 'devicegroupid' --msg \"message\" --title \"title\" --timeout 120000"));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [devicegroupid] - The device identifier.");
- } else {
- console.log(" --id '[devicegroupid]' - The device identifier.");
- }
- console.log(" --msg [message] - The message to display.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --title [title] - Messagebox title, default is \"MeshCentral\".");
- console.log(" --timeout [miliseconds] - After timeout messagebox vanishes, 0 keeps messagebox open until closed manually, default is 120000 (2 Minutes).");
- break;
- }
- case 'grouptoast': {
- console.log("Display a toast notification on remote devices in a specific device group, Example usages:\r\n");
- console.log(winRemoveSingleQuotes(" MeshCtrl GroupToast --id 'devicegroupid' --msg \"message\""));
- console.log(winRemoveSingleQuotes(" MeshCtrl GroupToast --id 'devicegroupid' --msg \"message\" --title \"title\""));
- console.log("\r\nRequired arguments:\r\n");
- if (process.platform == 'win32') {
- console.log(" --id [devicegroupid] - The device identifier.");
- } else {
- console.log(" --id '[devicegroupid]' - The device identifier.");
- }
- console.log(" --msg [message] - The message to display.");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --title [title] - Toast title, default is \"MeshCentral\".");
- break;
- }
- case 'report': {
- console.log("Generate a CSV report, Example usages:\r\n");
- console.log(" MeshCtrl Report --type sessions --devicegroup mesh//...");
- console.log(" MeshCtrl Report --type traffic --json");
- console.log(" MeshCtrl Report --type logins --groupby day");
- console.log(" MeshCtrl Report --type db");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --start [yyyy-mm-ddThh:mm:ss] - Filter the results starting at that date. Defaults to last 24h and last week when used with --groupby day. Usable with sessions, traffic and logins");
- console.log(" --end [yyyy-mm-ddThh:mm:ss] - Filter the results ending at that date. Defaults to now. Usable with sessions, traffic and logins");
- console.log(" --groupby [name] - How to group results. Options: user, day, device. Defaults to user. User and day usable in sessions and logins, device usable in sessions.");
- console.log(" --devicegroup [devicegroupid] - Filter the results by device group. Usable in sessions");
- console.log(" --showtraffic - Add traffic data in sessions report");
- break;
- }
- default: {
- console.log("Get help on an action. Type:\r\n\r\n help [action]\r\n\r\nPossible actions are: " + possibleCommands.join(', ') + '.');
- }
- }
- }
- break;
- }
- }
-
- if (ok) {
- if(args.loginpass===true){
- const readline = require('readline');
- const rl = readline.createInterface({
- input: process.stdin,
- output: process.stdout,
- terminal: false
- });
- process.stdout.write('Enter your password: ');
- const stdin = process.openStdin();
- stdin.setRawMode(true); // Set raw mode to prevent echoing of characters
- stdin.resume();
- args.loginpass = '';
- process.stdin.on('data', (char) => {
- char = char + '';
- switch (char) {
- case '\n':
- case '\r':
- case '\u0004': // They've finished entering their password
- stdin.setRawMode(false);
- stdin.pause();
- process.stdout.clearLine(); process.stdout.cursorTo(0);
- rl.close();
- serverConnect();
- break;
- case '\u0003': // Ctrl+C
- process.stdout.write('\n');
- process.exit();
- break;
- default: // Mask the password with "*"
- args.loginpass += char;
- process.stdout.clearLine(); process.stdout.cursorTo(0);
- process.stdout.write('Enter your password: ' + '*'.repeat(args.loginpass.length));
- break;
- }
- });
- }else{
- serverConnect();
- }
- }
-}
-
-function displayConfigHelp() {
- console.log("Perform operations on the config.json file. Example usage:\r\n");
- console.log(" MeshCtrl config --show");
- console.log("\r\nOptional arguments:\r\n");
- console.log(" --show - Display the config.json file.");
- console.log(" --listdomains - Display non-default domains.");
- console.log(" --adddomain [domain] - Add a domain.");
- console.log(" --removedomain [domain] - Remove a domain.");
- console.log(" --settodomain [domain] - Set values to the domain.");
- console.log(" --removefromdomain [domain] - Remove values from the domain.");
- console.log("\r\nWith adddomain, removedomain, settodomain and removefromdomain you can add the key and value pair. For example:\r\n");
- console.log(" --adddomain \"MyDomain\" --title \"My Server Name\" --newAccounts false");
- console.log(" --settodomain \"MyDomain\" --title \"My Server Name\"");
- console.log(" --removefromdomain \"MyDomain\" --title");
-}
-
-function performConfigOperations(args) {
- var domainValues = ['title', 'title2', 'titlepicture', 'trustedcert', 'welcomepicture', 'welcometext', 'userquota', 'meshquota', 'newaccounts', 'usernameisemail', 'newaccountemaildomains', 'newaccountspass', 'newaccountsrights', 'geolocation', 'lockagentdownload', 'userconsentflags', 'Usersessionidletimeout', 'auth', 'ldapoptions', 'ldapusername', 'ldapuserbinarykey', 'ldapuseremail', 'footer', 'certurl', 'loginKey', 'userallowedip', 'agentallowedip', 'agentnoproxy', 'agentconfig', 'orphanagentuser', 'httpheaders', 'yubikey', 'passwordrequirements', 'limits', 'amtacmactivation', 'redirects', 'sessionrecording', 'hide', 'preventduplicatedevices'];
- var domainObjectValues = ['ldapoptions', 'httpheaders', 'yubikey', 'passwordrequirements', 'limits', 'amtacmactivation', 'redirects', 'sessionrecording'];
- var domainArrayValues = ['newaccountemaildomains', 'newaccountsrights', 'loginkey', 'agentconfig'];
- var configChange = false;
- var fs = require('fs');
- var path = require('path');
- var configFile = 'config.json';
- var didSomething = 0;
- if (fs.existsSync(configFile) == false) { configFile = path.join('meshcentral-data', 'config.json'); }
- if (fs.existsSync(configFile) == false) { configFile = path.join(__dirname, 'config.json'); }
- if (fs.existsSync(configFile) == false) { configFile = path.join(__dirname, 'meshcentral-data', 'config.json'); }
- if (fs.existsSync(configFile) == false) { configFile = path.join(__dirname, '..', 'meshcentral-data', 'config.json'); }
- if (fs.existsSync(configFile) == false) { configFile = path.join(__dirname, '..', '..', 'meshcentral-data', 'config.json'); }
- if (fs.existsSync(configFile) == false) { console.log("Unable to find config.json."); return; }
- var config = null;
- try { config = fs.readFileSync(configFile).toString('utf8'); } catch (ex) { console.log("Error: Unable to read config.json"); return; }
- try { config = JSON.parse(fs.readFileSync(configFile)); } catch (e) { console.log('ERROR: Unable to parse ' + configFile + '.'); return null; }
- console.log("!!!READ CONFIG");
- console.log(JSON.parse(fs.readFileSync(configFile)));
- if (args.adddomain != null) {
- didSomething++;
- if (config.domains == null) { config.domains = {}; }
- if (config.domains[args.adddomain] != null) { console.log("Error: Domain \"" + args.adddomain + "\" already exists"); }
- else {
- configChange = true;
- config.domains[args.adddomain] = {};
- for (var i in args) {
- if (domainValues.indexOf(i.toLowerCase()) >= 0) {
- if (args[i] == 'true') { args[i] = true; } else if (args[i] == 'false') { args[i] = false; } else if (parseInt(args[i]) == args[i]) { args[i] = parseInt(args[i]); }
- config.domains[args.adddomain][i] = args[i];
- configChange = true;
- }
- }
- }
- }
- if (args.removedomain != null) {
- didSomething++;
- if (config.domains == null) { config.domains = {}; }
- if (config.domains[args.removedomain] == null) { console.log("Error: Domain \"" + args.removedomain + "\" does not exist"); }
- else { delete config.domains[args.removedomain]; configChange = true; }
- }
- if (args.settodomain != null) {
- didSomething++;
- if (config.domains == null) { config.domains = {}; }
- if (args.settodomain == true) { args.settodomain = ''; }
- if (config.domains[args.settodomain] == null) { console.log("Error: Domain \"" + args.settodomain + "\" does not exist"); }
- else {
- for (var i in args) {
- if ((i == '_') || (i == 'settodomain')) continue;
- if (domainValues.indexOf(i.toLowerCase()) >= 0) {
- var isObj = (domainObjectValues.indexOf(i.toLowerCase()) >= 0);
- var isArr = (domainArrayValues.indexOf(i.toLowerCase()) >= 0);
- if ((isObj == false) && (isArr == false)) {
- // Simple value set
- if (args[i] == '') { delete config.domains[args.settodomain][i]; configChange = true; } else {
- if (args[i] == 'true') { args[i] = true; } else if (args[i] == 'false') { args[i] = false; } else if (parseInt(args[i]) == args[i]) { args[i] = parseInt(args[i]); }
- config.domains[args.settodomain][i] = args[i];
- configChange = true;
- }
- } else if (isObj || isArr) {
- // Set an object/array value
- if (args[i] == '') { delete config.domains[args.settodomain][i]; configChange = true; } else {
- var x = null;
- try { x = JSON.parse(args[i]); } catch (ex) { }
- if ((x == null) || (typeof x != 'object')) { console.log("Unable to parse JSON for " + i + "."); } else {
- if (isArr && Array.isArray(x) == false) {
- console.log("Value " + i + " must be an array.");
- } else if (!isArr && Array.isArray(x) == true) {
- console.log("Value " + i + " must be an object.");
- } else {
- config.domains[args.settodomain][i] = x;
- configChange = true;
- }
- }
- }
- }
- } else {
- console.log('Invalid configuration value: ' + i);
- }
- }
- }
- }
- if (args.removefromdomain != null) {
- didSomething++;
- if (config.domains == null) { config.domains = {}; }
- if (config.domains[args.removefromdomain] == null) { console.log("Error: Domain \"" + args.removefromdomain + "\" does not exist"); }
- else { for (var i in args) { if (domainValues.indexOf(i.toLowerCase()) >= 0) { delete config.domains[args.removefromdomain][i]; configChange = true; } } }
- }
- if (configChange) {
- try { fs.writeFileSync(configFile, JSON.stringify(config, null, 2)); } catch (ex) { console.log("Error: Unable to read config.json"); return; }
- }
- if (args.show == 1) {
- console.log(JSON.stringify(config, null, 2)); return;
- } else if (args.listdomains == 1) {
- if (config.domains == null) {
- console.log('No domains found.'); return;
- } else {
- // Show the list of active domains, skip the default one.
- for (var i in config.domains) { if ((i != '') && (i[0] != '_')) { console.log(i); } } return;
- }
- } else {
- if (didSomething == 0) {
- displayConfigHelp();
- } else {
- console.log("Done.");
- }
- }
-}
-
-function onVerifyServer(clientName, certs) { return null; }
-
-function serverConnect() {
- const WebSocket = require('ws');
-
- var url = 'wss://localhost/control.ashx';
- if (args.url) {
- url = args.url;
- if (url.length < 5) { console.log("Invalid url."); process.exit(); return; }
- if ((url.startsWith('wss://') == false) && (url.startsWith('ws://') == false)) { console.log("Invalid url."); process.exit(); return; }
- var i = url.indexOf('?key='), loginKey = null;
- if (i >= 0) { loginKey = url.substring(i + 5); url = url.substring(0, i); }
- if (url.endsWith('/') == false) { url += '/'; }
- url += 'control.ashx';
- if (loginKey != null) { url += '?key=' + loginKey; }
- }
-
- // TODO: checkServerIdentity does not work???
- var options = { rejectUnauthorized: false, checkServerIdentity: onVerifyServer }
-
- // Setup the HTTP proxy if needed
- if (args.proxy != null) {
- const HttpsProxyAgent = require('https-proxy-agent');
- options.agent = new HttpsProxyAgent(require('url').parse(args.proxy));
- }
-
- // Password authentication
- if (args.loginpass != null) {
- var username = 'admin';
- if (args.loginuser != null) { username = args.loginuser; }
- var token = '';
- if (args.token != null) { token = ',' + Buffer.from('' + args.token).toString('base64'); }
- options.headers = { 'x-meshauth': Buffer.from('' + username).toString('base64') + ',' + Buffer.from('' + args.loginpass).toString('base64') + token }
- }
-
- // Cookie authentication
- var ckey = null, loginCookie = null;
- if (args.loginkey != null) {
- // User key passed in as argument hex
- if (args.loginkey.length != 160) { loginCookie = args.loginkey; }
- ckey = Buffer.from(args.loginkey, 'hex');
- if (ckey.length != 80) { ckey = null; loginCookie = args.loginkey; }
- } else if (args.loginkeyfile != null) {
- // Load key from hex file
- var fs = require('fs');
- try {
- var keydata = fs.readFileSync(args.loginkeyfile, 'utf8').split(' ').join('').split('\r').join('').split('\n').join('');
- ckey = Buffer.from(keydata, 'hex');
- if (ckey.length != 80) { ckey = null; loginCookie = args.loginkey; }
- } catch (ex) { console.log(ex.message); process.exit(); return; }
- }
-
- settings.xxurl = url;
- if (ckey != null) {
- var domainid = '', username = 'admin';
- if (args.logindomain != null) { domainid = args.logindomain; }
- if (args.loginuser != null) { username = args.loginuser; }
- url += (url.indexOf('?key=') >= 0 ? '&auth=' : '?auth=') + encodeCookie({ userid: 'user/' + domainid + '/' + username, domainid: domainid }, ckey);
- } else {
- if (args.logindomain != null) { console.log("--logindomain can only be used along with --loginkey."); process.exit(); return; }
- if (loginCookie != null) { url += (url.indexOf('?key=') >= 0 ? '&auth=' : '?auth=') + loginCookie; }
- }
-
- const ws = new WebSocket(url, options);
- //console.log('Connecting to ' + url);
-
- ws.on('open', function open() {
- //console.log('Connected.');
- switch (settings.cmd) {
- case 'serverinfo': { break; }
- case 'userinfo': { break; }
- case 'listusers': { ws.send(JSON.stringify({ action: 'users', responseid: 'meshctrl' })); break; }
- case 'listusersessions': { ws.send(JSON.stringify({ action: 'wssessioncount', responseid: 'meshctrl' })); break; }
- case 'removeallusersfromusergroup':
- case 'listusergroups': { ws.send(JSON.stringify({ action: 'usergroups', responseid: 'meshctrl' })); break; }
- case 'listdevicegroups': { ws.send(JSON.stringify({ action: 'meshes', responseid: 'meshctrl' })); break; }
- case 'listusersofdevicegroup': { ws.send(JSON.stringify({ action: 'meshes', responseid: 'meshctrl' })); break; }
- case 'listdevices': {
- if (args.details) {
- // Get list of devices with lots of details
- ws.send(JSON.stringify({ action: 'getDeviceDetails', type: (args.csv) ? 'csv' : 'json' }));
- } else if (args.group) {
- ws.send(JSON.stringify({ action: 'nodes', meshname: args.group, responseid: 'meshctrl' }));
- } else if (args.id) {
- ws.send(JSON.stringify({ action: 'nodes', meshid: args.id, responseid: 'meshctrl' }));
- } else {
- ws.send(JSON.stringify({ action: 'meshes' }));
- ws.send(JSON.stringify({ action: 'nodes', responseid: 'meshctrl' }));
- }
- break;
- }
- case 'listevents': {
- limit = null;
- if (args.limit) { limit = parseInt(args.limit); }
- if ((typeof limit != 'number') || (limit < 1)) { limit = null; }
-
- var cmd = null;
- if (args.userid) {
- cmd = { action: 'events', user: args.userid, responseid: 'meshctrl' };
- } else if (args.id) {
- cmd = { action: 'events', nodeid: args.id, responseid: 'meshctrl' };
- } else {
- cmd = { action: 'events', responseid: 'meshctrl' };
- }
- if (typeof limit == 'number') { cmd.limit = limit; }
- ws.send(JSON.stringify(cmd));
- break;
- }
- case 'logintokens': {
- if (args.add) {
- var cmd = { action: 'createLoginToken', name: args.add, expire: 0, responseid: 'meshctrl' };
- if (args.expire) { cmd.expire = parseInt(args.expire); }
- ws.send(JSON.stringify(cmd));
- } else {
- var cmd = { action: 'loginTokens', responseid: 'meshctrl' };
- if (args.remove) { cmd.remove = [args.remove]; }
- ws.send(JSON.stringify(cmd));
- }
- break;
- }
- case 'adduser': {
- var siteadmin = getSiteAdminRights(args);
- if (args.randompass) { args.pass = getRandomAmtPassword(); }
- var op = { action: 'adduser', username: args.user, pass: args.pass, responseid: 'meshctrl' };
- if (args.email) { op.email = args.email; if (args.emailverified) { op.emailVerified = true; } }
- if (args.resetpass) { op.resetNextLogin = true; }
- if (siteadmin != -1) { op.siteadmin = siteadmin; }
- if (args.domain) { op.domain = args.domain; }
- if (args.phone === true) { op.phone = ''; }
- if (typeof args.phone == 'string') { op.phone = args.phone; }
- if (typeof args.realname == 'string') { op.realname = args.realname; }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'edituser': {
- var userid = args.userid;
- if ((args.domain != null) && (userid.indexOf('/') < 0)) { userid = 'user/' + args.domain + '/' + userid; }
- var siteadmin = getSiteAdminRights(args);
- var op = { action: 'edituser', userid: userid, responseid: 'meshctrl' };
- if (args.email) { op.email = args.email; if (args.emailverified) { op.emailVerified = true; } }
- if (args.resetpass) { op.resetNextLogin = true; }
- if (siteadmin != -1) { op.siteadmin = siteadmin; }
- if (args.domain) { op.domain = args.domain; }
- if (args.phone === true) { op.phone = ''; }
- if (typeof args.phone == 'string') { op.phone = args.phone; }
- if (typeof args.realname == 'string') { op.realname = args.realname; }
- if (args.realname === true) { op.realname = ''; }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'removeuser': {
- var userid = args.userid;
- if ((args.domain != null) && (userid.indexOf('/') < 0)) { userid = 'user/' + args.domain + '/' + userid; }
- ws.send(JSON.stringify({ action: 'deleteuser', userid: userid, responseid: 'meshctrl' }));
- break;
- }
- case 'addusergroup': {
- var op = { action: 'createusergroup', name: args.name, desc: args.desc, responseid: 'meshctrl' };
- if (args.domain) { op.domain = args.domain; }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'removeusergroup': {
- var ugrpid = args.groupid;
- if ((args.domain != null) && (userid.indexOf('/') < 0)) { ugrpid = 'ugrp/' + args.domain + '/' + ugrpid; }
- ws.send(JSON.stringify({ action: 'deleteusergroup', ugrpid: ugrpid, responseid: 'meshctrl' }));
- break;
- }
- case 'addtousergroup': {
- var ugrpid = args.groupid;
- if ((args.domain != null) && (userid.indexOf('/') < 0)) { ugrpid = 'ugrp/' + args.domain + '/' + ugrpid; }
-
- // Add a user to a user group
- if (args.userid != null) {
- var userid = args.userid;
- if ((args.domain != null) && (userid.indexOf('/') < 0)) { userid = 'user/' + args.domain + '/' + userid; }
- ws.send(JSON.stringify({ action: 'addusertousergroup', ugrpid: ugrpid, usernames: [userid.split('/')[2]], responseid: 'meshctrl' }));
- break;
- }
-
- if ((args.id != null) && (args.id.startsWith('user/'))) {
- ws.send(JSON.stringify({ action: 'addusertousergroup', ugrpid: ugrpid, usernames: [args.id.split('/')[2]], responseid: 'meshctrl' }));
- break;
- }
-
- var rights = 0;
- if (args.rights != null) { rights = parseInt(args.rights); }
-
- // Add a device group to a user group
- if (args.meshid != null) {
- var meshid = args.meshid;
- if ((args.domain != null) && (userid.indexOf('/') < 0)) { meshid = 'mesh/' + args.domain + '/' + meshid; }
- ws.send(JSON.stringify({ action: 'addmeshuser', meshid: meshid, userid: ugrpid, meshadmin: rights, responseid: 'meshctrl' }));
- break;
- }
-
- if ((args.id != null) && (args.id.startsWith('mesh/'))) {
- ws.send(JSON.stringify({ action: 'addmeshuser', meshid: args.id, userid: ugrpid, meshadmin: rights, responseid: 'meshctrl' }));
- break;
- }
-
- // Add a device to a user group
- if (args.nodeid != null) {
- var nodeid = args.nodeid;
- if ((args.domain != null) && (userid.indexOf('/') < 0)) { nodeid = 'node/' + args.domain + '/' + nodeid; }
- ws.send(JSON.stringify({ action: 'adddeviceuser', nodeid: nodeid, userids: [ugrpid], rights: rights, responseid: 'meshctrl' }));
- break;
- }
-
- if ((args.id != null) && (args.id.startsWith('node/'))) {
- ws.send(JSON.stringify({ action: 'adddeviceuser', nodeid: args.id, userids: [ugrpid], rights: rights, responseid: 'meshctrl' }));
- break;
- }
-
- break;
- }
- case 'removefromusergroup': {
- var ugrpid = args.groupid;
- if ((args.domain != null) && (userid.indexOf('/') < 0)) { ugrpid = 'ugrp/' + args.domain + '/' + ugrpid; }
-
- // Remove a user from a user group
- if (args.userid != null) {
- var userid = args.userid;
- if ((args.domain != null) && (userid.indexOf('/') < 0)) { userid = 'user/' + args.domain + '/' + userid; }
- ws.send(JSON.stringify({ action: 'removeuserfromusergroup', ugrpid: ugrpid, userid: userid, responseid: 'meshctrl' }));
- break;
- }
-
- if ((args.id != null) && (args.id.startsWith('user/'))) {
- ws.send(JSON.stringify({ action: 'removeuserfromusergroup', ugrpid: ugrpid, userid: args.id, responseid: 'meshctrl' }));
- break;
- }
-
- // Remove a device group from a user group
- if (args.meshid != null) {
- var meshid = args.meshid;
- if ((args.domain != null) && (userid.indexOf('/') < 0)) { meshid = 'mesh/' + args.domain + '/' + meshid; }
- ws.send(JSON.stringify({ action: 'removemeshuser', meshid: meshid, userid: ugrpid, responseid: 'meshctrl' }));
- break;
- }
-
- if ((args.id != null) && (args.id.startsWith('mesh/'))) {
- ws.send(JSON.stringify({ action: 'removemeshuser', meshid: args.id, userid: ugrpid, responseid: 'meshctrl' }));
- break;
- }
-
- // Remove a device from a user group
- if (args.nodeid != null) {
- var nodeid = args.nodeid;
- if ((args.domain != null) && (userid.indexOf('/') < 0)) { nodeid = 'node/' + args.domain + '/' + nodeid; }
- ws.send(JSON.stringify({ action: 'adddeviceuser', nodeid: nodeid, userids: [ugrpid], rights: 0, responseid: 'meshctrl', remove: true }));
- break;
- }
-
- if ((args.id != null) && (args.id.startsWith('node/'))) {
- ws.send(JSON.stringify({ action: 'adddeviceuser', nodeid: args.id, userids: [ugrpid], rights: 0, responseid: 'meshctrl', remove: true }));
- break;
- }
-
- break;
- }
- case 'adddevicegroup': {
- var op = { action: 'createmesh', meshname: args.name, meshtype: 2, responseid: 'meshctrl' };
- if (args.desc) { op.desc = args.desc; }
- if (args.amtonly) { op.meshtype = 1; }
- if (args.agentless) { op.meshtype = 3; }
- if (args.features) { op.flags = parseInt(args.features); }
- if (args.consent) { op.consent = parseInt(args.consent); }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'removedevicegroup': {
- var op = { action: 'deletemesh', responseid: 'meshctrl' };
- if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'addamtdevice': {
- var op = { action: 'addamtdevice', amttls: 1, responseid: 'meshctrl' };
- if (args.id) { op.meshid = args.id; }
- if ((typeof args.devicename == 'string') && (args.devicename != '')) { op.devicename = args.devicename; }
- if ((typeof args.hostname == 'string') && (args.hostname != '')) { op.hostname = args.hostname; }
- if ((typeof args.user == 'string') && (args.user != '')) { op.amtusername = args.user; }
- if ((typeof args.pass == 'string') && (args.pass != '')) { op.amtpassword = args.pass; }
- if (args.notls) { op.amttls = 0; }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'addlocaldevice': {
- var op = { action: 'addlocaldevice', type: 4, responseid: 'meshctrl' };
- if (args.id) { op.meshid = args.id; }
- if ((typeof args.devicename == 'string') && (args.devicename != '')) { op.devicename = args.devicename; }
- if ((typeof args.hostname == 'string') && (args.hostname != '')) { op.hostname = args.hostname; }
- if (args.type) {
- if ((typeof parseInt(args.type) != 'number') || isNaN(parseInt(args.type))) { console.log("Invalid type."); process.exit(1); return; }
- op.type = args.type;
- }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'editdevicegroup': {
- var op = { action: 'editmesh', responseid: 'meshctrl' };
- if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshidname = args.group; }
- if ((typeof args.name == 'string') && (args.name != '')) { op.meshname = args.name; }
- if (args.desc === true) { op.desc = ""; } else if (typeof args.desc == 'string') { op.desc = args.desc; }
- if (args.invitecodes === true) { op.invite = "*"; } else if (typeof args.invitecodes == 'string') {
- var invitecodes = args.invitecodes.split(','), invitecodes2 = [];
- for (var i in invitecodes) { if (invitecodes[i].length > 0) { invitecodes2.push(invitecodes[i]); } }
- if (invitecodes2.length > 0) {
- op.invite = { codes: invitecodes2, flags: 0 };
- if (args.backgroundonly === true) { op.invite.flags = 2; }
- else if (args.interactiveonly === true) { op.invite.flags = 1; }
- }
- }
- if (args.flags != null) {
- var flags = parseInt(args.flags);
- if (typeof flags == 'number') { op.flags = flags; }
- }
- if (args.consent != null) {
- var consent = parseInt(args.consent);
- if (typeof consent == 'number') { op.consent = consent; }
- }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'movetodevicegroup': {
- var op = { action: 'changeDeviceMesh', responseid: 'meshctrl', nodeids: [args.devid] };
- if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'addusertodevicegroup': {
- var meshrights = 0;
- if (args.fullrights) { meshrights = 0xFFFFFFFF; }
- if (args.editgroup) { meshrights |= 1; }
- if (args.manageusers) { meshrights |= 2; }
- if (args.managedevices) { meshrights |= 4; }
- if (args.remotecontrol) { meshrights |= 8; }
- if (args.agentconsole) { meshrights |= 16; }
- if (args.serverfiles) { meshrights |= 32; }
- if (args.wakedevices) { meshrights |= 64; }
- if (args.notes) { meshrights |= 128; }
- if (args.desktopviewonly) { meshrights |= 256; }
- if (args.noterminal) { meshrights |= 512; }
- if (args.nofiles) { meshrights |= 1024; }
- if (args.noamt) { meshrights |= 2048; }
- if (args.limiteddesktop) { meshrights |= 4096; }
- if (args.limitedevents) { meshrights |= 8192; }
- if (args.chatnotify) { meshrights |= 16384; }
- if (args.uninstall) { meshrights |= 32768; }
- var op = { action: 'addmeshuser', usernames: [args.userid], meshadmin: meshrights, responseid: 'meshctrl' };
- if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'removeuserfromdevicegroup': {
- var op = { action: 'removemeshuser', userid: args.userid, responseid: 'meshctrl' };
- if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'addusertodevice': {
- var meshrights = 0;
- if (args.fullrights) { meshrights = (8 + 16 + 32 + 64 + 128 + 16384 + 32768); }
- if (args.remotecontrol) { meshrights |= 8; }
- if (args.agentconsole) { meshrights |= 16; }
- if (args.serverfiles) { meshrights |= 32; }
- if (args.wakedevices) { meshrights |= 64; }
- if (args.notes) { meshrights |= 128; }
- if (args.desktopviewonly) { meshrights |= 256; }
- if (args.noterminal) { meshrights |= 512; }
- if (args.nofiles) { meshrights |= 1024; }
- if (args.noamt) { meshrights |= 2048; }
- if (args.limiteddesktop) { meshrights |= 4096; }
- if (args.limitedevents) { meshrights |= 8192; }
- if (args.chatnotify) { meshrights |= 16384; }
- if (args.uninstall) { meshrights |= 32768; }
- var op = { action: 'adddeviceuser', nodeid: args.id, usernames: [args.userid], rights: meshrights, responseid: 'meshctrl' };
- ws.send(JSON.stringify(op));
- break;
- }
- case 'removeuserfromdevice': {
- var op = { action: 'adddeviceuser', nodeid: args.id, usernames: [args.userid], rights: 0, remove: true, responseid: 'meshctrl' };
- ws.send(JSON.stringify(op));
- break;
- }
- case 'sendinviteemail': {
- var op = { action: 'inviteAgent', email: args.email, name: '', os: '0', responseid: 'meshctrl' }
- if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
- if (args.name) { op.name = args.name; }
- if (args.message) { op.msg = args.message; }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'generateinvitelink': {
- var op = { action: 'createInviteLink', expire: args.hours, flags: 0, responseid: 'meshctrl' }
- if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
- if (args.flags) { op.flags = args.flags; }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'broadcast': {
- var op = { action: 'userbroadcast', msg: args.msg, responseid: 'meshctrl' };
- if (args.user) { op.userid = args.user; }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'showevents': {
- console.log('Connected. Press ctrl-c to end.');
- break;
- }
- case 'deviceinfo': {
- settings.deviceinfocount = 4;
- ws.send(JSON.stringify({ action: 'nodes' }));
- ws.send(JSON.stringify({ action: 'getnetworkinfo', nodeid: args.id, responseid: 'meshctrl' }));
- ws.send(JSON.stringify({ action: 'lastconnect', nodeid: args.id, responseid: 'meshctrl' }));
- ws.send(JSON.stringify({ action: 'getsysinfo', nodeid: args.id, nodeinfo: true, responseid: 'meshctrl' }));
- break;
- }
- case 'removedevice': {
- var op = { action: 'removedevices', nodeids: [ args.id ], responseid: 'meshctrl' };
- ws.send(JSON.stringify(op));
- break;
- }
- case 'editdevice': {
- var op = { action: 'changedevice', nodeid: args.id, responseid: 'meshctrl' };
- if (typeof args.name == 'string') { op.name = args.name; }
- if (typeof args.name == 'number') { op.name = '' + args.name; }
- if (args.desc) { if (args.desc === true) { op.desc = ''; } else if (typeof args.desc == 'string') { op.desc = args.desc; } else if (typeof args.desc == 'number') { op.desc = '' + args.desc; } }
- if (args.tags) { if (args.tags === true) { op.tags = ''; } else if (typeof args.tags == 'string') { op.tags = args.tags.split(','); } else if (typeof args.tags == 'number') { op.tags = '' + args.tags; } }
- if (args.icon) { op.icon = parseInt(args.icon); if ((typeof op.icon != 'number') || isNaN(op.icon) || (op.icon < 1) || (op.icon > 8)) { console.log("Icon must be between 1 and 8."); process.exit(1); return; } }
- if (args.consent) { op.consent = parseInt(args.consent); if ((typeof op.consent != 'number') || isNaN(op.consent) || (op.consent < 1)) { console.log("Invalid consent flags."); process.exit(1); return; } }
- ws.send(JSON.stringify(op));
- break;
- }
- case 'runcommand': {
- var runAsUser = 0;
- if (args.runasuser) { runAsUser = 1; } else if (args.runasuseronly) { runAsUser = 2; }
- var reply = false;
- if (args.reply) { reply = true; }
- ws.send(JSON.stringify({ action: 'runcommands', nodeids: [args.id], type: ((args.powershell) ? 2 : 0), cmds: args.run, responseid: 'meshctrl', runAsUser: runAsUser, reply: reply }));
- break;
- }
- case 'shell':
- case 'upload':
- case 'download': {
- ws.send("{\"action\":\"authcookie\"}");
- break;
- }
- case 'devicepower': {
- var nodes = args.id.split(',');
- if (args.wake) {
- // Wake operation
- ws.send(JSON.stringify({ action: 'wakedevices', nodeids: nodes, responseid: 'meshctrl' }));
- } else if (args.off) {
- // Power off operation
- ws.send(JSON.stringify({ action: 'poweraction', nodeids: nodes, actiontype: 2, responseid: 'meshctrl' }));
- } else if (args.reset) {
- // Reset operation
- ws.send(JSON.stringify({ action: 'poweraction', nodeids: nodes, actiontype: 3, responseid: 'meshctrl' }));
- } else if (args.sleep) {
- // Sleep operation
- ws.send(JSON.stringify({ action: 'poweraction', nodeids: nodes, actiontype: 4, responseid: 'meshctrl' }));
- } else if (args.amton) {
- // Intel AMT Power on operation
- ws.send(JSON.stringify({ action: 'poweraction', nodeids: nodes, actiontype: 302, responseid: 'meshctrl' }));
- } else if (args.amtoff) {
- // Intel AMT Power off operation
- ws.send(JSON.stringify({ action: 'poweraction', nodeids: nodes, actiontype: 308, responseid: 'meshctrl' }));
- } else if (args.amtreset) {
- // Intel AMT Power reset operation
- ws.send(JSON.stringify({ action: 'poweraction', nodeids: nodes, actiontype: 310, responseid: 'meshctrl' }));
- } else {
- console.log('No power operation specified.');
- process.exit(1);
- }
- break;
- }
- case 'agentdownload': {
- // Download an agent
- var u = settings.xxurl.replace('wss://', 'https://').replace('/control.ashx', '/meshagents');
- if (u.indexOf('?') > 0) { u += '&'; } else { u += '?'; }
- u += 'id=' + args.type + '&meshid=' + args.id;
- if (args.installflags) {
- if ((typeof parseInt(args.installflags) != 'number') || isNaN(parseInt(args.installflags)) || (parseInt(args.installflags) < 0) || (parseInt(args.installflags) > 2)) { console.log("Invalid Installflags."); process.exit(1); return; }
- u += '&installflags=' + args.installflags;
- }
- const options = { rejectUnauthorized: false, checkServerIdentity: onVerifyServer }
- const fs = require('fs');
- const https = require('https');
- var downloadSize = 0;
- const req = https.request(u, options, function (res) {
- if (res.statusCode != 200) {
- console.log('Download error, statusCode: ' + res.statusCode);
- process.exit(1);
- } else {
- // Agent the agent filename
- var agentFileName = 'meshagent';
- if ((res.headers) && (res.headers['content-disposition'] != null)) {
- var i = res.headers['content-disposition'].indexOf('filename=\"');
- if (i >= 0) {
- agentFileName = res.headers['content-disposition'].substring(i + 10);
- i = agentFileName.indexOf('\"');
- if (i >= 0) { agentFileName = agentFileName.substring(0, i); }
- }
- }
- // Check if this file already exists
- if (fs.existsSync(agentFileName)) { console.log('File \"' + agentFileName + '\" already exists.'); process.exit(1); }
- var fd = fs.openSync(agentFileName, 'w'); // Open the file for writing
- res.on('data', function (d) {
- downloadSize += d.length;
- fs.writeSync(fd, d); // Save to file
- });
- res.on('end', function (d) {
- fs.closeSync(fd); // Close file
- console.log('Downloaded ' + downloadSize + ' byte(s) to \"' + agentFileName + '\"');
- process.exit(1);
- });
- }
- })
- req.on('error', function (error) { console.error(error); process.exit(1); })
- req.end()
- break;
- }
- case 'webrelay': {
- var protocol = null;
- if (args.type != null) {
- if (args.type == 'http') {
- protocol = 1;
- } else if (args.type == 'https') {
- protocol = 2;
- } else {
- console.log("Unknown protocol type: " + args.type); process.exit(1);
- }
- }
- var port = null;
- if (typeof args.port == 'number') {
- if ((args.port < 1) || (args.port > 65535)) { console.log("Port number must be between 1 and 65535."); process.exit(1); }
- port = args.port;
- } else if (protocol == 1) {
- port = 80;
- } else if (protocol == 2) {
- port = 443;
- }
- ws.send(JSON.stringify({ action: 'webrelay', nodeid: args.id, port: port, appid: protocol, responseid: 'meshctrl' }));
- break;
- }
- case 'devicesharing': {
- if (args.add) {
- if (args.add.length == 0) { console.log("Invalid guest name."); process.exit(1); }
-
- // Sharing type, desktop or terminal
- var p = 0;
- if (args.type != null) {
- var shareTypes = args.type.toLowerCase().split(',');
- for (var i in shareTypes) { if ((shareTypes[i] != 'terminal') && (shareTypes[i] != 'desktop') && (shareTypes[i] != 'files') && (shareTypes[i] != 'http') && (shareTypes[i] != 'https')) { console.log("Unknown sharing type: " + shareTypes[i]); process.exit(1); } }
- if (shareTypes.indexOf('terminal') >= 0) { p |= 1; }
- if (shareTypes.indexOf('desktop') >= 0) { p |= 2; }
- if (shareTypes.indexOf('files') >= 0) { p |= 4; }
- if (shareTypes.indexOf('http') >= 0) { p |= 8; }
- if (shareTypes.indexOf('https') >= 0) { p |= 16; }
- }
- if (p == 0) { p = 2; } // Desktop
-
- // Sharing view only
- var viewOnly = false;
- if (args.viewonly) { viewOnly = true; }
-
- // User consent
- var consent = 0;
- if (args.consent == null) {
- if ((p & 1) != 0) { consent = 0x0002; } // Terminal notify
- if ((p & 2) != 0) { consent = 0x0001; } // Desktop notify
- if ((p & 4) != 0) { consent = 0x0004; } // Files notify
- } else {
- if (typeof args.consent == 'string') {
- var flagStrs = args.consent.split(',');
- for (var i in flagStrs) {
- var flagStr = flagStrs[i].toLowerCase();
- if (flagStr == 'none') { consent = 0; }
- else if (flagStr == 'notify') {
- if ((p & 1) != 0) { consent |= 0x0002; } // Terminal notify
- if ((p & 2) != 0) { consent |= 0x0001; } // Desktop notify
- if ((p & 4) != 0) { consent |= 0x0004; } // Files notify
- } else if (flagStr == 'prompt') {
- if ((p & 1) != 0) { consent |= 0x0010; } // Terminal prompt
- if ((p & 2) != 0) { consent |= 0x0008; } // Desktop prompt
- if ((p & 4) != 0) { consent |= 0x0020; } // Files prompt
- } else if (flagStr == 'bar') {
- if ((p & 2) != 0) { consent |= 0x0040; } // Desktop toolbar
- } else { console.log("Unknown consent type."); process.exit(1); return; }
- }
- }
- }
-
- var port = null;
- // Set Port Number if http or https
- if ((p & 8) || (p & 16)) {
- if (typeof args.port == 'number') {
- if ((args.port < 1) || (args.port > 65535)) { console.log("Port number must be between 1 and 65535."); process.exit(1); }
- port = args.port;
- } else if ((p & 8)) {
- port = 80;
- } else if ((p & 16)) {
- port = 443;
- }
- }
-
- // Start and end time
- var start = null, end = null;
- if (args.start) { start = Math.floor(Date.parse(args.start) / 1000); end = start + (60 * 60); }
- if (args.end) { if (start == null) { start = Math.floor(Date.now() / 1000) } end = Math.floor(Date.parse(args.end) / 1000); if (end <= start) { console.log("End time must be ahead of start time."); process.exit(1); return; } }
- if (args.duration) { if (start == null) { start = Math.floor(Date.now() / 1000) } end = start + parseInt(args.duration * 60); }
-
- // Recurring
- var recurring = 0;
- if (args.daily) { recurring = 1; } else if (args.weekly) { recurring = 2; }
- if (recurring > 0) {
- if (args.end != null) { console.log("End time can't be specified for recurring shares, use --duration only."); process.exit(1); return; }
- if (args.duration == null) { args.duration = 60; } else { args.duration = parseInt(args.duration); }
- if (start == null) { start = Math.floor(Date.now() / 1000) }
- if ((typeof args.duration != 'number') || (args.duration < 1)) { console.log("Invalid duration value."); process.exit(1); return; }
-
- // Recurring sharing
- ws.send(JSON.stringify({ action: 'createDeviceShareLink', nodeid: args.id, guestname: args.add, p: p, consent: consent, start: start, expire: args.duration, recurring: recurring, viewOnly: viewOnly, port: port, responseid: 'meshctrl' }));
- } else {
- if ((start == null) && (end == null)) {
- // Unlimited sharing
- ws.send(JSON.stringify({ action: 'createDeviceShareLink', nodeid: args.id, guestname: args.add, p: p, consent: consent, expire: 0, viewOnly: viewOnly, port: port, responseid: 'meshctrl' }));
- } else {
- // Time limited sharing
- ws.send(JSON.stringify({ action: 'createDeviceShareLink', nodeid: args.id, guestname: args.add, p: p, consent: consent, start: start, end: end, viewOnly: viewOnly, port: port, responseid: 'meshctrl' }));
- }
- }
- } else if (args.remove) {
- ws.send(JSON.stringify({ action: 'removeDeviceShare', nodeid: args.id, publicid: args.remove, responseid: 'meshctrl' }));
- } else {
- ws.send(JSON.stringify({ action: 'deviceShares', nodeid: args.id, responseid: 'meshctrl' }));
- }
- break;
- }
- case 'deviceopenurl': {
- ws.send(JSON.stringify({ action: 'msg', type: 'openUrl', nodeid: args.id, url: args.openurl, responseid: 'meshctrl' }));
- break;
- }
- case 'devicemessage': {
- ws.send(JSON.stringify({ action: 'msg', type: 'messagebox', nodeid: args.id, title: args.title ? args.title : "MeshCentral", msg: args.msg, timeout: args.timeout ? args.timeout : 120000, responseid: 'meshctrl' }));
- break;
- }
- case 'devicetoast': {
- ws.send(JSON.stringify({ action: 'toast', nodeids: [args.id], title: args.title ? args.title : "MeshCentral", msg: args.msg, responseid: 'meshctrl' }));
- break;
- }
- case 'groupmessage': {
- ws.send(JSON.stringify({ action: 'nodes', meshid: args.id, responseid: 'meshctrl' }));
- break;
- }
- case 'grouptoast': {
- ws.send(JSON.stringify({ action: 'nodes', meshid: args.id, responseid: 'meshctrl' }));
- break;
- }
- case 'report': {
- var reporttype = 1;
- switch(args.type) {
- case 'traffic':
- reporttype = 2;
- break;
- case 'logins':
- reporttype = 3;
- break;
- case 'db':
- reporttype = 4;
- break;
- }
-
- var reportgroupby = 1;
- if(args.groupby){
- reportgroupby = args.groupby === 'device' ? 2 : args.groupby === 'day' ? 3: 1;
- }
-
- var start = null, end = null;
- if (args.start) {
- start = Math.floor(Date.parse(args.start) / 1000);
- } else {
- start = reportgroupby === 3 ? Math.round(new Date().getTime() / 1000) - (168 * 3600) : Math.round(new Date().getTime() / 1000) - (24 * 3600);
- }
- if (args.end) {
- end = Math.floor(Date.parse(args.end) / 1000);
- } else {
- end = Math.round(new Date().getTime() / 1000);
- }
- if (end <= start) { console.log("End time must be ahead of start time."); process.exit(1); return; }
-
- ws.send(JSON.stringify({ action: 'report', type: reporttype, groupBy: reportgroupby, devGroup: args.devicegroup || null, start, end, tz: Intl.DateTimeFormat().resolvedOptions().timeZone, tf: new Date().getTimezoneOffset(), showTraffic: args.hasOwnProperty('showtraffic'), l: 'en', responseid: 'meshctrl' }));
- break;
- }
- }
- });
-
- function getSiteAdminRights(args) {
- var siteadmin = -1;
- if (typeof args.rights == 'number') {
- siteadmin = args.rights;
- } else if (typeof args.rights == 'string') {
- siteadmin = 0;
- var srights = args.rights.toLowerCase().split(',');
- if (srights.indexOf('full') != -1) { siteadmin = 0xFFFFFFFF; }
- if (srights.indexOf('none') != -1) { siteadmin = 0x00000000; }
- if (srights.indexOf('backup') != -1 || srights.indexOf('serverbackup') != -1) { siteadmin |= 0x00000001; }
- if (srights.indexOf('manageusers') != -1) { siteadmin |= 0x00000002; }
- if (srights.indexOf('restore') != -1 || srights.indexOf('serverrestore') != -1) { siteadmin |= 0x00000004; }
- if (srights.indexOf('fileaccess') != -1) { siteadmin |= 0x00000008; }
- if (srights.indexOf('update') != -1 || srights.indexOf('serverupdate') != -1) { siteadmin |= 0x00000010; }
- if (srights.indexOf('locked') != -1) { siteadmin |= 0x00000020; }
- if (srights.indexOf('nonewgroups') != -1) { siteadmin |= 0x00000040; }
- if (srights.indexOf('notools') != -1) { siteadmin |= 0x00000080; }
- if (srights.indexOf('usergroups') != -1) { siteadmin |= 0x00000100; }
- if (srights.indexOf('recordings') != -1) { siteadmin |= 0x00000200; }
- if (srights.indexOf('locksettings') != -1) { siteadmin |= 0x00000400; }
- if (srights.indexOf('allevents') != -1) { siteadmin |= 0x00000800; }
- if (srights.indexOf('nonewdevices') != -1) { siteadmin |= 0x00001000; }
- }
-
- if (args.siteadmin) { siteadmin = 0xFFFFFFFF; }
- if (args.manageusers) { if (siteadmin == -1) { siteadmin = 0; } siteadmin |= 2; }
- if (args.fileaccess) { if (siteadmin == -1) { siteadmin = 0; } siteadmin |= 8; }
- if (args.serverupdate) { if (siteadmin == -1) { siteadmin = 0; } siteadmin |= 16; }
- if (args.locked) { if (siteadmin == -1) { siteadmin = 0; } siteadmin |= 32; }
- if (args.nonewgroups) { if (siteadmin == -1) { siteadmin = 0; } siteadmin |= 64; }
- if (args.notools) { if (siteadmin == -1) { siteadmin = 0; } siteadmin |= 128; }
- return siteadmin;
- }
-
- ws.on('close', function () { process.exit(); });
- ws.on('error', function (err) {
- if (err.code == 'ENOTFOUND') { console.log('Unable to resolve ' + url); }
- else if (err.code == 'ECONNREFUSED') { console.log('Unable to connect to ' + url); }
- else { console.log('Unable to connect to ' + url); }
- process.exit();
- });
-
- ws.on('message', function incoming(rawdata) {
- var data = null;
- try { data = JSON.parse(rawdata); } catch (ex) { }
- if (data == null) { console.log('Unable to parse data: ' + rawdata); }
- if (settings.cmd == 'showevents') {
- if (args.filter == null) {
- // Display all events
- console.log(JSON.stringify(data, null, 2));
- } else {
- // Display select events
- var filters = args.filter.split(',');
- if (typeof data.event == 'object') {
- if (filters.indexOf(data.event.action) >= 0) { console.log(JSON.stringify(data, null, 2) + '\r\n'); }
- } else {
- if (filters.indexOf(data.action) >= 0) { console.log(JSON.stringify(data, null, 2) + '\r\n'); }
- }
- }
- return;
- }
- switch (data.action) {
- case 'serverinfo': { // SERVERINFO
- settings.currentDomain = data.serverinfo.domain;
- if (settings.cmd == 'serverinfo') {
- if (args.json) {
- console.log(JSON.stringify(data.serverinfo, ' ', 2));
- } else {
- for (var i in data.serverinfo) { console.log(i + ':', data.serverinfo[i]); }
- }
- process.exit();
- }
- break;
- }
- case 'events': {
- if (settings.cmd == 'listevents') {
- if (args.raw) {
- // RAW JSON
- console.log(JSON.stringify(data.events));
- } else if (args.json) {
- // Formatted JSON
- console.log(JSON.stringify(data.events, null, 2));
- } else {
- if ((args.id == null) && (args.userid == null)) {
- // CSV format
- console.log("time,type,action,nodeid,userid,msg");
- for (var i in data.events) {
- var x = [];
- x.push(data.events[i].time);
- x.push(data.events[i].etype);
- x.push(data.events[i].action);
- x.push(data.events[i].nodeid);
- x.push(data.events[i].userid);
- x.push(data.events[i].msg);
- console.log(csvFormatArray(x));
- }
- } else if (args.id != null) {
- // CSV format
- console.log("time,type,action,userid,msg");
- for (var i in data.events) {
- var x = [];
- x.push(data.events[i].time);
- x.push(data.events[i].etype);
- x.push(data.events[i].action);
- x.push(data.events[i].userid);
- x.push(data.events[i].msg);
- console.log(csvFormatArray(x));
- }
- } else if (args.userid != null) {
- // CSV format
- console.log("time,type,action,nodeid,msg");
- for (var i in data.events) {
- var x = [];
- x.push(data.events[i].time);
- x.push(data.events[i].etype);
- x.push(data.events[i].action);
- x.push(data.events[i].nodeid);
- x.push(data.events[i].msg);
- console.log(csvFormatArray(x));
- }
- }
- }
- process.exit();
- }
- break;
- }
- case 'authcookie': { // SHELL, UPLOAD, DOWNLOAD
- if ((settings.cmd == 'shell') || (settings.cmd == 'upload') || (settings.cmd == 'download')) {
- var protocol = 1; // Terminal
- if ((settings.cmd == 'upload') || (settings.cmd == 'download')) { protocol = 5; } // Files
- if ((args.id.split('/') != 3) && (settings.currentDomain != null)) { args.id = 'node/' + settings.currentDomain + '/' + args.id; }
- var id = getRandomHex(6);
- ws.send(JSON.stringify({ action: 'msg', nodeid: args.id, type: 'tunnel', usage: 1, value: '*/meshrelay.ashx?p=' + protocol + '&nodeid=' + args.id + '&id=' + id + '&rauth=' + data.rcookie, responseid: 'meshctrl' }));
- connectTunnel(url.replace('/control.ashx', '/meshrelay.ashx?browser=1&p=' + protocol + '&nodeid=' + encodeURIComponent(args.id) + '&id=' + id + '&auth=' + data.cookie));
- }
- break;
- }
- case 'deviceShares': { // DEVICESHARING
- if (data.result != null) {
- console.log(data.result);
- } else {
- if ((data.deviceShares == null) || (data.deviceShares.length == 0)) {
- console.log('No device sharing links for this device.');
- } else {
- if (args.json) {
- console.log(data.deviceShares);
- } else {
- for (var i in data.deviceShares) {
- var share = data.deviceShares[i];
- var shareType = [];
- if ((share.p & 1) != 0) { shareType.push("Terminal"); }
- if ((share.p & 2) != 0) { if (share.viewOnly) { shareType.push("View Only Desktop"); } else { shareType.push("Desktop"); } }
- if ((share.p & 4) != 0) { shareType.push("Files"); }
- shareType = shareType.join(' + ');
- if (shareType == '') { shareType = "Unknown"; }
- var consent = [];
- if ((share.consent & 0x0001) != 0) { consent.push("Desktop Notify"); }
- if ((share.consent & 0x0008) != 0) { consent.push("Desktop Prompt"); }
- if ((share.consent & 0x0040) != 0) { consent.push("Desktop Connection Toolbar"); }
- if ((share.consent & 0x0002) != 0) { consent.push("Terminal Notify"); }
- if ((share.consent & 0x0010) != 0) { consent.push("Terminal Prompt"); }
- if ((share.consent & 0x0004) != 0) { consent.push("Files Notify"); }
- if ((share.consent & 0x0020) != 0) { consent.push("Files Prompt"); }
- console.log('----------');
- console.log('Identifier: ' + share.publicid);
- console.log('Type: ' + shareType);
- console.log('UserId: ' + share.userid);
- console.log('Guest Name: ' + share.guestName);
- console.log('User Consent: ' + consent.join(', '));
- if (share.startTime) { console.log('Start Time: ' + new Date(share.startTime).toLocaleString()); }
- if (share.expireTime) { console.log('Expire Time: ' + new Date(share.expireTime).toLocaleString()); }
- if (share.duration) { console.log('Duration: ' + share.duration + ' minute' + ((share.duration > 1) ? 's' : '')); }
- if (share.recurring == 1) { console.log('Recurring: ' + 'Daily'); }
- if (share.recurring == 2) { console.log('Recurring: ' + 'Weekly'); }
- console.log('URL: ' + share.url);
- }
- }
- }
- }
- process.exit();
- break;
- }
- case 'userinfo': { // USERINFO
- if (settings.cmd == 'userinfo') {
- if (args.json) {
- console.log(JSON.stringify(data.userinfo, ' ', 2));
- } else {
- for (var i in data.userinfo) { console.log(i + ':', data.userinfo[i]); }
- }
- process.exit();
- }
- break;
- }
- case 'getsysinfo': { // DEVICEINFO
- if (settings.cmd == 'deviceinfo') {
- settings.sysinfo = (data.result) ? null : data;
- if (--settings.deviceinfocount == 0) { displayDeviceInfo(settings.sysinfo, settings.lastconnect, settings.networking, settings.nodes); process.exit(); }
- }
- break;
- }
- case 'lastconnect': {
- if (settings.cmd == 'deviceinfo') {
- settings.lastconnect = (data.result) ? null : data;
- if (--settings.deviceinfocount == 0) { displayDeviceInfo(settings.sysinfo, settings.lastconnect, settings.networking, settings.nodes); process.exit(); }
- }
- break;
- }
- case 'getnetworkinfo': {
- if (settings.cmd == 'deviceinfo') {
- settings.networking = (data.result) ? null : data;
- if (--settings.deviceinfocount == 0) { displayDeviceInfo(settings.sysinfo, settings.lastconnect, settings.networking, settings.nodes); process.exit(); }
- }
- break;
- }
- case 'msg': // SHELL
- case 'toast': // TOAST
- case 'adduser': // ADDUSER
- case 'edituser': // EDITUSER
- case 'addamtdevice': // ADDAMTDEVICE
- case 'addlocaldevice': // ADDLOCALDEVICE
- case 'removedevices': // REMOVEDEVICE
- case 'changedevice': // EDITDEVICE
- case 'deleteuser': // REMOVEUSER
- case 'createmesh': // ADDDEVICEGROUP
- case 'deletemesh': // REMOVEDEVICEGROUP
- case 'editmesh': // EDITDEVICEGROUP
- case 'wakedevices':
- case 'changeDeviceMesh':
- case 'addmeshuser': //
- case 'removemeshuser': //
- case 'wakedevices': //
- case 'inviteAgent': //
- case 'adddeviceuser': //
- case 'createusergroup': //
- case 'deleteusergroup': //
- case 'runcommands':
- case 'poweraction':
- case 'addusertousergroup':
- case 'removeuserfromusergroup':
- case 'removeDeviceShare':
- case 'userbroadcast': { // BROADCAST
- if (((settings.cmd == 'shell') || (settings.cmd == 'upload') || (settings.cmd == 'download')) && (data.result == 'OK')) return;
- if ((data.type == 'runcommands') && (settings.cmd != 'runcommand')) return;
- if ((settings.multiresponse != null) && (settings.multiresponse > 1)) { settings.multiresponse--; break; }
- if (data.responseid == 'meshctrl') {
- if (data.meshid) { console.log(data.result, data.meshid); }
- else if (data.userid) { console.log(data.result, data.userid); }
- else console.log(data.result);
- process.exit();
- }
- break;
- }
- case 'createDeviceShareLink':
- case 'webrelay':
- if (data.result == 'OK') {
- if (data.publicid) { console.log('ID: ' + data.publicid); }
- console.log('URL: ' + data.url);
- } else {
- console.log(data.result);
- }
- process.exit();
- break;
- case 'createInviteLink':
- if (data.responseid == 'meshctrl') {
- if (data.url) { console.log(data.url); }
- else console.log(data.result);
- process.exit();
- }
- break;
- case 'wssessioncount': { // LIST USER SESSIONS
- if (args.json) {
- console.log(JSON.stringify(data.wssessions, ' ', 2));
- } else {
- for (var i in data.wssessions) { console.log(i + ', ' + ((data.wssessions[i] > 1) ? (data.wssessions[i] + ' sessions.') : ("1 session."))); }
- }
- process.exit();
- break;
- }
- case 'usergroups': { // LIST USER GROUPS
- if (settings.cmd == 'listusergroups') {
- if (args.json) {
- console.log(JSON.stringify(data.ugroups, ' ', 2));
- } else {
- for (var i in data.ugroups) {
- var x = i + ', ' + data.ugroups[i].name;
- if (data.ugroups[i].desc && (data.ugroups[i].desc != '')) { x += ', ' + data.ugroups[i].desc; }
- console.log(x);
- var mesh = [], user = [], node = [];
- if (data.ugroups[i].links != null) { for (var j in data.ugroups[i].links) { if (j.startsWith('mesh/')) { mesh.push(j); } if (j.startsWith('user/')) { user.push(j); } if (j.startsWith('node/')) { node.push(j); } } }
- console.log(' Users:');
- if (user.length > 0) { for (var j in user) { console.log(' ' + user[j]); } } else { console.log(' (None)'); }
- console.log(' Device Groups:');
- if (mesh.length > 0) { for (var j in mesh) { console.log(' ' + mesh[j] + ', ' + data.ugroups[i].links[mesh[j]].rights); } } else { console.log(' (None)'); }
- console.log(' Devices:');
- if (node.length > 0) { for (var j in node) { console.log(' ' + node[j] + ', ' + data.ugroups[i].links[node[j]].rights); } } else { console.log(' (None)'); }
- }
- }
- process.exit();
- } else if (settings.cmd == 'removeallusersfromusergroup') {
- var ugrpid = args.groupid, exit = false;
- if ((args.domain != null) && (userid.indexOf('/') < 0)) { ugrpid = 'ugrp/' + args.domain + '/' + ugrpid; }
- var ugroup = data.ugroups[ugrpid];
- if (ugroup == null) {
- console.log('User group not found.');
- exit = true;
- } else {
- var usercount = 0;
- if (ugroup.links) {
- for (var i in ugroup.links) {
- if (i.startsWith('user/')) {
- usercount++;
- ws.send(JSON.stringify({ action: 'removeuserfromusergroup', ugrpid: ugrpid, userid: i, responseid: 'meshctrl' }));
- console.log('Removing ' + i);
- }
- }
- }
- if (usercount == 0) { console.log('No users in this user group.'); exit = true; } else { settings.multiresponse = usercount; }
- }
- if (exit) { process.exit(); }
- }
- break;
- }
- case 'users': { // LISTUSERS
- if (data.result) { console.log(data.result); process.exit(); return; }
- if (args.filter) {
- // Filter the list of users
- var filters = args.filter.toLowerCase().split(',');
- var filteredusers = [];
- for (var i in data.users) {
- var ok = false;
- if ((filters.indexOf('2fa') >= 0) && ((data.users[i].otphkeys != null) || (data.users[i].otpkeys != null) || (data.users[i].otpsecret != null))) { ok = true; }
- if ((filters.indexOf('no2fa') >= 0) && ((data.users[i].otphkeys == null) && (data.users[i].otpkeys == null) && (data.users[i].otpsecret == null))) { ok = true; }
- if (ok == true) { filteredusers.push(data.users[i]); }
- }
- data.users = filteredusers;
- }
- if (args.json) {
- console.log(JSON.stringify(data.users, ' ', 2));
- } else {
- if (args.idexists) { for (var i in data.users) { const u = data.users[i]; if ((u._id == args.idexists) || (u._id.split('/')[2] == args.idexists)) { console.log('1'); process.exit(); return; } } console.log('0'); process.exit(); return; }
- if (args.nameexists) { for (var i in data.users) { const u = data.users[i]; if (u.name == args.nameexists) { console.log(u._id); process.exit(); return; } } process.exit(); return; }
-
- console.log('id, name, email\r\n---------------');
- for (var i in data.users) {
- const u = data.users[i];
- var t = "\"" + u._id.split('/')[2] + "\", \"" + u.name + "\"";
- if (u.email != null) { t += ", \"" + u.email + "\""; }
- console.log(t);
- }
- }
- process.exit();
- break;
- }
- case 'nodes': {
- if (settings.cmd == 'deviceinfo') {
- settings.nodes = (data.result) ? null : data;
- if (--settings.deviceinfocount == 0) { displayDeviceInfo(settings.sysinfo, settings.lastconnect, settings.networking, settings.nodes); process.exit(); }
- }
- if ((settings.cmd == 'listdevices') && (data.responseid == 'meshctrl')) {
- if ((data.result != null) && (data.result != 'ok')) {
- console.log(data.result);
- } else {
- // Filter devices based on device id.
- if (args.filterid) {
- var filteridSplit = args.filterid.split(','), filters = [];
- for (var i in filteridSplit) {
- var f = filteridSplit[i].trim();
- var g = f.split('/'); // If there is any / in the id, just grab the last part.
- if (g.length > 0) { f = g[g.length - 1]; }
- if (f != '') { filters.push(f); }
- }
- if (filters.length > 0) {
- for (var mid in data.nodes) {
- var filteredNodes = [];
- for (var nid in data.nodes[mid]) {
- var n = data.nodes[mid][nid], match = false;
- for (var f in filters) { if (n._id.indexOf(filters[f]) >= 0) { match = true; } }
- if (match) { filteredNodes.push(n); }
- }
- data.nodes[mid] = filteredNodes;
- }
- }
- }
-
- // Filter devices based on filter string
- if (args.filter != null) {
- for (var meshid in data.nodes) {
- for (var d in data.nodes[meshid]) { data.nodes[meshid][d].meshid = meshid; }
- data.nodes[meshid] = parseSearchOrInput(data.nodes[meshid], args.filter.toString().toLowerCase());
- }
- }
-
- if (args.csv) {
- // Return a flat list
- var nodecount = 0;
- for (var i in data.nodes) {
- var devicesInMesh = data.nodes[i];
- for (var j in devicesInMesh) {
- var n = devicesInMesh[j];
- nodecount++;
- if (settings.xmeshes && settings.xmeshes[i]) {
- console.log('\"' + settings.xmeshes[i]._id.split('/')[2] + '\",\"' + settings.xmeshes[i].name.split('\"').join('') + '\",\"' + n._id.split('/')[2] + '\",\"' + n.name.split('\"').join('') + '\",' + (n.icon ? n.icon : 0) + ',' + (n.conn ? n.conn : 0) + ',' + (n.pwr ? n.pwr : 0));
- } else {
- console.log('\"\",\"\",\"' + n._id.split('/')[2] + '\",\"' + n.name.split('\"').join('') + '\",' + (n.icon ? n.icon : 0) + ',' + (n.conn ? n.conn : 0) + ',' + (n.pwr ? n.pwr : 0));
- }
- }
- }
- if (nodecount == 0) { console.log('None'); }
- } else if (args.count) {
- // Return how many devices are in this group
- var nodes = [];
- for (var i in data.nodes) { var devicesInMesh = data.nodes[i]; for (var j in devicesInMesh) { nodes.push(devicesInMesh[j]); } }
- console.log(nodes.length);
- } else if (args.json) {
- // Return all devices in JSON format
- var nodes = [];
-
- for (var i in data.nodes) {
- const devicesInMesh = data.nodes[i];
- for (var j in devicesInMesh) {
- devicesInMesh[j].meshid = i; // Add device group id
- if (settings.xmeshes && settings.xmeshes[i] && settings.xmeshes[i].name) { devicesInMesh[j].groupname = settings.xmeshes[i].name; } // Add device group name
- nodes.push(devicesInMesh[j]);
- }
- }
- console.log(JSON.stringify(nodes, ' ', 2));
- } else {
- // Display the list of nodes in text format
- var nodecount = 0;
- for (var i in data.nodes) {
- var devicesInMesh = data.nodes[i];
- if (devicesInMesh.length > 0) {
- if (settings.xmeshes && settings.xmeshes[i] && settings.xmeshes[i].name) { console.log('\r\nDevice group: \"' + settings.xmeshes[i].name.split('\"').join('') + '\"'); }
- console.log('id, name, icon, conn, pwr\r\n-------------------------');
- for (var j in devicesInMesh) {
- var n = devicesInMesh[j];
- nodecount++;
- console.log('\"' + n._id.split('/')[2] + '\", \"' + n.name.split('\"').join('') + '\", ' + (n.icon ? n.icon : 0) + ', ' + (n.conn ? n.conn : 0) + ', ' + (n.pwr ? n.pwr : 0));
- }
- }
- }
- if (nodecount == 0) { console.log('None'); }
- }
- }
- process.exit();
- }
- if ((settings.cmd == 'groupmessage') && (data.responseid == 'meshctrl')) {
- if ((data.nodes != null)) {
- for (var i in data.nodes) {
- for (let index = 0; index < data.nodes[i].length; index++) {
- const element = data.nodes[i][index];
- ws.send(JSON.stringify({ action: 'msg', type: 'messagebox', nodeid: element._id, title: args.title ? args.title : "MeshCentral", msg: args.msg, timeout: args.timeout ? args.timeout : 120000 }));
- }
- }
- }
-
- setTimeout(function(){ console.log('ok'); process.exit(); }, 1000);
- }
- if ((settings.cmd == 'grouptoast') && (data.responseid == 'meshctrl')) {
- if (data.nodes != null) {
- for (var i in data.nodes) {
- var nodes = [];
- for (let index = 0; index < data.nodes[i].length; index++) {
- const element = data.nodes[i][index];
- nodes.push(element._id);
- }
- ws.send(JSON.stringify({ action: 'toast', nodeids: nodes, title: args.title ? args.title : "MeshCentral", msg: args.msg, responseid: 'meshctrl' }));
- }
- }
- }
- break;
- }
- case 'meshes': { // LISTDEVICEGROUPS
- if (settings.cmd == 'listdevices') {
- // Store the list of device groups for later use
- settings.xmeshes = {}
- for (var i in data.meshes) { settings.xmeshes[data.meshes[i]._id] = data.meshes[i]; }
- } else if (settings.cmd == 'listdevicegroups') {
- if (args.json) {
- // If asked, add the MeshID hex encoding to the JSON.
- if (args.hex) { for (var i in data.meshes) { data.meshes[i]._idhex = '0x' + Buffer.from(data.meshes[i]._id.split('/')[2].replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase(); } }
- console.log(JSON.stringify(data.meshes, ' ', 2));
- } else {
- if (args.idexists) { for (var i in data.meshes) { const u = data.meshes[i]; if ((u._id == args.idexists) || (u._id.split('/')[2] == args.idexists)) { console.log('1'); process.exit(); return; } } console.log('0'); process.exit(); return; }
- if (args.nameexists) { for (var i in data.meshes) { const u = data.meshes[i]; if (u.name == args.nameexists) { console.log(u._id); process.exit(); return; } } process.exit(); return; }
-
- console.log('id, name\r\n---------------');
- for (var i in data.meshes) {
- const m = data.meshes[i];
- var mid = m._id.split('/')[2];
- if (args.hex) { mid = '0x' + Buffer.from(mid.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase(); }
- var t = "\"" + mid + "\", \"" + m.name + "\"";
- console.log(t);
- }
- }
- process.exit();
- } else if (settings.cmd == 'listusersofdevicegroup') {
- for (var i in data.meshes) {
- const m = data.meshes[i];
- var mid = m._id.split('/')[2];
- if (mid == args.id) {
- if (args.json) {
- console.log(JSON.stringify(m.links, ' ', 2));
- } else {
- console.log('userid, rights\r\n---------------');
- for (var l in m.links) {
- var rights = m.links[l].rights;
- var rightsstr = [];
- if (rights == 4294967295) { rightsstr = ['FullAdministrator']; } else {
- if (rights & 1) { rightsstr.push('EditMesh'); }
- if (rights & 2) { rightsstr.push('ManageUsers'); }
- if (rights & 4) { rightsstr.push('ManageComputers'); }
- if (rights & 8) { rightsstr.push('RemoteControl'); }
- if (rights & 16) { rightsstr.push('AgentConsole'); }
- if (rights & 32) { rightsstr.push('ServerFiles'); }
- if (rights & 64) { rightsstr.push('WakeDevice'); }
- if (rights & 128) { rightsstr.push('SetNotes'); }
- if (rights & 256) { rightsstr.push('RemoteViewOnly'); }
- if (rights & 512) { rightsstr.push('NoTerminal'); }
- if (rights & 1024) { rightsstr.push('NoFiles'); }
- if (rights & 2048) { rightsstr.push('NoAMT'); }
- if (rights & 4096) { rightsstr.push('DesktopLimitedInput'); }
- }
- console.log(l.split('/')[2] + ', ' + rightsstr.join(', '));
- }
- }
- process.exit();
- return;
- }
- }
- console.log('Group id not found');
- process.exit();
- }
- break;
- }
- case 'close': {
- if (data.cause == 'noauth') {
- if (data.msg == 'tokenrequired') {
- console.log('Authentication token required, use --token [number].');
- } else if (data.msg == 'nokey') {
- console.log('URL key is invalid or missing, please specify ?key=xxx in url');
- } else {
- if ((args.loginkeyfile != null) || (args.loginkey != null)) {
- console.log('Invalid login, check the login key and that this computer has the correct time.');
- } else {
- console.log('Invalid login.');
- }
- }
- }
- process.exit();
- break;
- }
- case 'createLoginToken': {
- if (data.result != null) {
- console.log(data.result);
- process.exit();
- } else {
- if (args.json) {
- console.log(data);
- } else {
- console.log("New login token created.");
- if (data.name) { console.log("Token name: " + data.name); }
- if (data.created) { console.log("Created: " + new Date(data.created).toLocaleString()); }
- if (data.expire) { console.log("Expire: " + new Date(data.expire).toLocaleString()); }
- if (data.tokenUser) { console.log("Username: " + data.tokenUser); }
- if (data.tokenPass) { console.log("Password: " + data.tokenPass); }
- }
- }
- process.exit();
- break;
- }
- case 'loginTokens': {
- if (args.json) {
- console.log(data.loginTokens);
- } else {
- console.log("Name Username Expire");
- console.log("-------------------------------------------------------------------------------------");
- if (data.loginTokens.length == 0) {
- console.log("No login tokens");
- } else {
- for (var i in data.loginTokens) {
- var t = data.loginTokens[i];
- var e = (t.expire == 0) ? "Unlimited" : new Date(t.expire).toLocaleString();
- console.log(padString(t.name, 28) + padString(t.tokenUser, 28) + e);
- }
- }
- }
- process.exit();
- break;
- }
- case 'getDeviceDetails': {
- console.log(data.data);
- process.exit();
- }
- case 'report': {
- console.log('group,' + data.data.columns.flatMap(c => c.id).join(','));
- Object.keys(data.data.groups).forEach(gk => {
- data.data.groups[gk].entries.forEach(e => {
- console.log(gk + ',' + Object.values(e).join(','));
- });
- });
- process.exit();
- }
- default: { break; }
- }
- //console.log('Data', data);
- //setTimeout(function timeout() { ws.send(Date.now()); }, 500);
- });
-}
-
-// String padding function
-
-function padString(str, pad) {
- var xpad = ' ';
- if (str.length >= pad) return str; return str + xpad.substring(0, pad - str.length)
-}
-
-function parseSearchAndInput(nodes, x) {
- var s = x.split(' ' + "and" + ' '), r = null;
- for (var i in s) {
- var r2 = getDevicesThatMatchFilter(nodes, s[i]);
- if (r == null) { r = r2; } else { var r3 = []; for (var j in r2) { if (r.indexOf(r2[j]) >= 0) { r3.push(r2[j]); } } r = r3; }
- }
- return r;
-}
-
-function parseSearchOrInput(nodes, x) {
- var s = x.split(' ' + "or" + ' '), r = null;
- for (var i in s) { var r2 = parseSearchAndInput(nodes, s[i]); if (r == null) { r = r2; } else { for (var j in r2) { if (r.indexOf(r2[j] >= 0)) { r.push(r2[j]); } } } }
- return r;
-}
-
-function getDevicesThatMatchFilter(nodes, x) {
- var r = [];
- var userSearch = null, ipSearch = null, groupSearch = null, tagSearch = null, agentTagSearch = null, wscSearch = null, osSearch = null, amtSearch = null, descSearch = null;
- if (x.startsWith("user:".toLowerCase())) { userSearch = x.substring("user:".length); }
- else if (x.startsWith("u:".toLowerCase())) { userSearch = x.substring("u:".length); }
- else if (x.startsWith("ip:".toLowerCase())) { ipSearch = x.substring("ip:".length); }
- else if (x.startsWith("group:".toLowerCase())) { groupSearch = x.substring("group:".length); }
- else if (x.startsWith("g:".toLowerCase())) { groupSearch = x.substring("g:".length); }
- else if (x.startsWith("tag:".toLowerCase())) { tagSearch = x.substring("tag:".length); }
- else if (x.startsWith("t:".toLowerCase())) { tagSearch = x.substring("t:".length); }
- else if (x.startsWith("atag:".toLowerCase())) { agentTagSearch = x.substring("atag:".length); }
- else if (x.startsWith("a:".toLowerCase())) { agentTagSearch = x.substring("a:".length); }
- else if (x.startsWith("os:".toLowerCase())) { osSearch = x.substring("os:".length); }
- else if (x.startsWith("amt:".toLowerCase())) { amtSearch = x.substring("amt:".length); }
- else if (x.startsWith("desc:".toLowerCase())) { descSearch = x.substring("desc:".length); }
- else if (x == 'wsc:ok') { wscSearch = 1; }
- else if (x == 'wsc:noav') { wscSearch = 2; }
- else if (x == 'wsc:noupdate') { wscSearch = 3; }
- else if (x == 'wsc:nofirewall') { wscSearch = 4; }
- else if (x == 'wsc:any') { wscSearch = 5; }
-
- if (x == '') {
- // No search
- for (var d in nodes) { r.push(nodes[d]); }
- } else if (ipSearch != null) {
- // IP address search
- for (var d in nodes) { if ((nodes[d].ip != null) && (nodes[d].ip.indexOf(ipSearch) >= 0)) { r.push(nodes[d]); } }
- } else if (groupSearch != null) {
- // Group filter
- if (settings.xmeshes) { for (var d in nodes) { if (settings.xmeshes[nodes[d].meshid] && settings.xmeshes[nodes[d].meshid].name.toLowerCase().indexOf(groupSearch) >= 0) { r.push(nodes[d]); } } }
- } else if (tagSearch != null) {
- // Tag filter
- for (var d in nodes) {
- if ((nodes[d].tags == null) && (tagSearch == '')) { r.push(nodes[d]); }
- else if (nodes[d].tags != null) { for (var j in nodes[d].tags) { if (nodes[d].tags[j].toLowerCase() == tagSearch) { r.push(nodes[d]); break; } } }
- }
- } else if (agentTagSearch != null) {
- // Agent Tag filter
- for (var d in nodes) {
- if ((((nodes[d].agent != null) && (nodes[d].agent.tag == null)) && (agentTagSearch == '')) || ((nodes[d].agent != null) && (nodes[d].agent.tag != null) && (nodes[d].agent.tag.toLowerCase().indexOf(agentTagSearch) >= 0))) { r.push(nodes[d]); };
- }
- } else if (userSearch != null) {
- // User search
- for (var d in nodes) {
- if (nodes[d].users && nodes[d].users.length > 0) { for (var i in nodes[d].users) { if (nodes[d].users[i].toLowerCase().indexOf(userSearch) >= 0) { r.push(nodes[d]); } } }
- }
- } else if (osSearch != null) {
- // OS search
- for (var d in nodes) { if ((nodes[d].osdesc != null) && (nodes[d].osdesc.toLowerCase().indexOf(osSearch) >= 0)) { r.push(nodes[d]); }; }
- } else if (amtSearch != null) {
- // Intel AMT search
- for (var d in nodes) { if ((nodes[d].intelamt != null) && ((amtSearch == '') || (nodes[d].intelamt.state == amtSearch))) { r.push(nodes[d]); } }
- } else if (descSearch != null) {
- // Device description search
- for (var d in nodes) { if ((nodes[d].desc != null) && (nodes[d].desc != '') && ((descSearch == '') || (nodes[d].desc.toLowerCase().indexOf(descSearch) >= 0))) { r.push(nodes[d]); } }
- } else if (wscSearch != null) {
- // Windows Security Center
- for (var d in nodes) {
- if (nodes[d].wsc) {
- if ((wscSearch == 1) && (nodes[d].wsc.antiVirus == 'OK') && (nodes[d].wsc.autoUpdate == 'OK') && (nodes[d].wsc.firewall == 'OK')) { r.push(nodes[d]); }
- else if (((wscSearch == 2) || (wscSearch == 5)) && (nodes[d].wsc.antiVirus != 'OK')) { r.push(nodes[d]); }
- else if (((wscSearch == 3) || (wscSearch == 5)) && (nodes[d].wsc.autoUpdate != 'OK')) { r.push(nodes[d]); }
- else if (((wscSearch == 4) || (wscSearch == 5)) && (nodes[d].wsc.firewall != 'OK')) { r.push(nodes[d]); }
- }
- }
- } else if (x == '*') {
- // Star filter
- for (var d in nodes) { if (stars[nodes[d]._id] == 1) { r.push(nodes[d]); } }
- } else {
- // Device name search
- try {
- var rs = x.split(/\s+/).join('|'), rx = new RegExp(rs); // In some cases (like +), this can throw an exception.
- for (var d in nodes) {
- //if (showRealNames) {
- //if (nodes[d].rnamel != null && rx.test(nodes[d].rnamel.toLowerCase())) { r.push(nodes[d]); }
- //} else {
- if (rx.test(nodes[d].name.toLowerCase())) { r.push(nodes[d]); }
- //}
- }
- } catch (ex) { for (var d in nodes) { r.push(nodes[d]); } }
- }
-
- return r;
-}
-
-
-// Connect tunnel to a remote agent
-function connectTunnel(url) {
- // Setup WebSocket options
- var options = { rejectUnauthorized: false, checkServerIdentity: onVerifyServer }
-
- // Setup the HTTP proxy if needed
- if (args.proxy != null) { const HttpsProxyAgent = require('https-proxy-agent'); options.agent = new HttpsProxyAgent(require('url').parse(args.proxy)); }
-
- // Connect the WebSocket
- console.log('Connecting...');
- const WebSocket = require('ws');
- settings.tunnelwsstate = 0;
- settings.tunnelws = new WebSocket(url, options);
- settings.tunnelws.on('open', function () { console.log('Waiting for Agent...'); }); // Wait for agent connection
- settings.tunnelws.on('close', function () { console.log('Connection Closed.'); process.exit(); });
- settings.tunnelws.on('error', function (err) { console.log(err); process.exit(); });
-
- if (settings.cmd == 'shell') {
- // This code does all of the work for a shell command
- settings.tunnelws.on('message', function (rawdata) {
- var data = rawdata.toString();
- if (settings.tunnelwsstate == 1) {
- // If the incoming text looks exactly like a control command, ignore it.
- if ((typeof data == 'string') && (data.startsWith('{"ctrlChannel":"102938","type":"'))) {
- var ctrlCmd = null;
- try { ctrlCmd = JSON.parse(data); } catch (ex) { }
- if ((ctrlCmd != null) && (ctrlCmd.ctrlChannel == '102938') && (ctrlCmd.type != null)) return; // This is a control command, like ping/pong. Ignore it.
- }
- process.stdout.write(data);
- } else if (settings.tunnelwsstate == 0) {
- if (data == 'c') { console.log('Connected.'); } else if (data == 'cr') { console.log('Connected, session is being recorded.'); } else return;
- // Send terminal size
- var termSize = null;
- if (typeof process.stdout.getWindowSize == 'function') { termSize = process.stdout.getWindowSize(); }
- if (termSize != null) { settings.tunnelws.send(JSON.stringify({ ctrlChannel: '102938', type: 'options', cols: termSize[0], rows: termSize[1] })); }
- settings.tunnelwsstate = 1;
- settings.tunnelws.send('1'); // Terminal
- process.stdin.setEncoding('utf8');
- process.stdin.setRawMode(true);
- process.stdout.setEncoding('utf8');
- process.stdin.unpipe(process.stdout);
- process.stdout.unpipe(process.stdin);
- process.stdin.on('data', function (data) { settings.tunnelws.send(Buffer.from(data)); });
- //process.stdin.on('readable', function () { var chunk; while ((chunk = process.stdin.read()) !== null) { settings.tunnelws.send(Buffer.from(chunk)); } });
- process.stdin.on('end', function () { process.exit(); });
- process.stdout.on('resize', function () {
- var termSize = null;
- if (typeof process.stdout.getWindowSize == 'function') { termSize = process.stdout.getWindowSize(); }
- if (termSize != null) { settings.tunnelws.send(JSON.stringify({ ctrlChannel: '102938', type: 'termsize', cols: termSize[0], rows: termSize[1] })); }
- });
- }
- });
- } else if (settings.cmd == 'upload') {
- // This code does all of the work for a file upload
- // node meshctrl upload --id oL4Y6Eg0qjnpHFrp1AxfxnBPenbDGnDSkC@HSOnAheIyd51pKhqSCUgJZakzwfKl --file readme.md --target c:\
- settings.tunnelws.on('message', function (rawdata) {
- if (settings.tunnelwsstate == 1) {
- var cmd = null;
- try { cmd = JSON.parse(rawdata.toString()); } catch (ex) { return; }
- if (cmd.reqid == 'up') {
- if ((cmd.action == 'uploadack') || (cmd.action == 'uploadstart')) {
- settings.inFlight--;
- if (settings.uploadFile == null) { if (settings.inFlight == 0) { process.exit(); } return; } // If the file is closed and there is no more in-flight data, exit.
- var loops = (cmd.action == 'uploadstart') ? 16 : 1; // If this is the first data to be sent, hot start now. We are going to have 16 blocks of data in-flight.
- for (var i = 0; i < loops; i++) {
- if (settings.uploadFile == null) continue;
- var buf = Buffer.alloc(65565);
- var len = require('fs').readSync(settings.uploadFile, buf, 1, 65564, settings.uploadPtr);
- var start = 1;
- settings.uploadPtr += len;
- if (len > 0) {
- if ((buf[1] == 0) || (buf[1] == 123)) { start = 0; buf[0] = 0; len++; } // If the buffer starts with 0 or 123, we must add an extra 0 at the start of the buffer
- settings.inFlight++;
- settings.tunnelws.send(buf.slice(start, start + len));
- } else {
- console.log('Upload done, ' + settings.uploadPtr + ' bytes sent.');
- if (settings.uploadFile != null) { require('fs').closeSync(settings.uploadFile); delete settings.uploadFile; }
- if (settings.inFlight == 0) { process.exit(); return; } // File is closed, if there is no more in-flight data, exit.
- }
- }
-
- } else if (cmd.action == 'uploaderror') {
- if (settings.uploadFile != null) { require('fs').closeSync(settings.uploadFile); }
- console.log('Upload error.');
- process.exit();
- }
- }
- } else if (settings.tunnelwsstate == 0) {
- var data = rawdata.toString();
- if (data == 'c') { console.log('Connected.'); } else if (data == 'cr') { console.log('Connected, session is being recorded.'); } else return;
- settings.tunnelwsstate = 1;
- settings.tunnelws.send('5'); // Files
- settings.uploadSize = require('fs').statSync(args.file).size;
- settings.uploadFile = require('fs').openSync(args.file, 'r');
- settings.uploadPtr = 0;
- settings.inFlight = 1;
- console.log('Uploading...');
- settings.tunnelws.send(JSON.stringify({ action: 'upload', reqid: 'up', path: args.target, name: require('path').basename(args.file), size: settings.uploadSize }));
- }
- });
- } else if (settings.cmd == 'download') {
- // This code does all of the work for a file download
- // node meshctrl download --id oL4Y6Eg0qjnpHFrp1AxfxnBPenbDGnDSkC@HSOnAheIyd51pKhqSCUgJZakzwfKl --file c:\temp\MC-8Languages.png --target c:\temp\bob.png
- settings.tunnelws.on('message', function (rawdata) {
- if (settings.tunnelwsstate == 1) {
- if ((rawdata.length > 0) && (rawdata.toString()[0] != '{')) {
- // This is binary data, this test is ok because 4 first bytes is a control value.
- if ((rawdata.length > 4) && (settings.downloadFile != null)) { settings.downloadSize += (rawdata.length - 4); require('fs').writeSync(settings.downloadFile, rawdata, 4, rawdata.length - 4); }
- if ((rawdata[3] & 1) != 0) { // Check end flag
- // File is done, close everything.
- if (settings.downloadFile != null) { require('fs').closeSync(settings.downloadFile); }
- console.log('Download completed, ' + settings.downloadSize + ' bytes written.');
- process.exit();
- } else {
- settings.tunnelws.send(JSON.stringify({ action: 'download', sub: 'ack', id: args.file })); // Send the ACK
- }
- } else {
- // This is text data
- var cmd = null;
- try { cmd = JSON.parse(rawdata.toString()); } catch (ex) { return; }
- if (cmd.action == 'download') {
- if (cmd.id != args.file) return;
- if (cmd.sub == 'start') {
- if ((args.target.endsWith('\\')) || (args.target.endsWith('/'))) { args.target += path.parse(args.file).name; }
- try { settings.downloadFile = require('fs').openSync(args.target, 'w'); } catch (ex) { console.log("Unable to create file: " + args.target); process.exit(); return; }
- settings.downloadSize = 0;
- settings.tunnelws.send(JSON.stringify({ action: 'download', sub: 'startack', id: args.file }));
- console.log('Download started: ' + args.target);
- } else if (cmd.sub == 'cancel') {
- if (settings.downloadFile != null) { require('fs').closeSync(settings.downloadFile); }
- console.log('Download canceled.');
- process.exit();
- }
- }
- }
- } else if (settings.tunnelwsstate == 0) {
- var data = rawdata.toString();
- if (data == 'c') { console.log('Connected.'); } else if (data == 'cr') { console.log('Connected, session is being recorded.'); } else return;
- settings.tunnelwsstate = 1;
- settings.tunnelws.send('5'); // Files
- settings.tunnelws.send(JSON.stringify({ action: 'download', sub: 'start', id: args.file, path: args.file }));
- }
- });
- }
-}
-
-// Encode an object as a cookie using a key using AES-GCM. (key must be 32 bytes or more)
-function encodeCookie(o, key) {
- try {
- if (key == null) { return null; }
- o.time = Math.floor(Date.now() / 1000); // Add the cookie creation time
- const iv = Buffer.from(crypto.randomBytes(12), 'binary'), cipher = crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv);
- const crypted = Buffer.concat([cipher.update(JSON.stringify(o), 'utf8'), cipher.final()]);
- return Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
- } catch (e) { return null; }
-}
-
-// Generate a random Intel AMT password
-function checkAmtPassword(p) { return (p.length > 7) && (/\d/.test(p)) && (/[a-z]/.test(p)) && (/[A-Z]/.test(p)) && (/\W/.test(p)); }
-function getRandomAmtPassword() { var p; do { p = Buffer.from(crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); } while (checkAmtPassword(p) == false); return p; }
-function getRandomHex(count) { return Buffer.from(crypto.randomBytes(count), 'binary').toString('hex'); }
-function format(format) { var args = Array.prototype.slice.call(arguments, 1); return format.replace(/{(\d+)}/g, function (match, number) { return typeof args[number] != 'undefined' ? args[number] : match; }); };
-function winRemoveSingleQuotes(str) { if (process.platform != 'win32') return str; else return str.split('\'').join(''); }
-
-function csvFormatArray(x) {
- var y = [];
- for (var i in x) { if ((x[i] == null) || (x[i] == '')) { y.push(''); } else { y.push('"' + x[i].split('"').join('') + '"'); } }
- return y.join(',');
-}
-
-function displayDeviceInfo(sysinfo, lastconnect, network, nodes) {
- //console.log('displayDeviceInfo', sysinfo, lastconnect, network, nodes);
-
- // Fetch the node information
- var node = null;;
- if (sysinfo != null && (sysinfo.node != null)) {
- // Node information came with system information
- node = sysinfo.node;
- } else {
- // This device does not have system information, get node information from the nodes list.
- for (var m in nodes.nodes) {
- for (var n in nodes.nodes[m]) {
- if (nodes.nodes[m][n]._id.indexOf(args.id) >= 0) { node = nodes.nodes[m][n]; }
- }
- }
- }
- if ((sysinfo == null && lastconnect == null && network == null) || (node == null)) {
- console.log("Invalid device id");
- process.exit(); return;
- }
-
- var info = {};
-
- //if (network != null) { sysinfo.netif = network.netif; }
- if (lastconnect != null) { node.lastconnect = lastconnect.time; node.lastaddr = lastconnect.addr; }
- if (args.raw) { console.log(JSON.stringify(sysinfo, ' ', 2)); return; }
-
- // General
- var output = {}, outputCount = 0;
- if (node.name) { output["Server Name"] = node.name; outputCount++; }
- if (node.rname) { output["Computer Name"] = node.rname; outputCount++; }
- if (node.host != null) { output["Hostname"] = node.host; outputCount++; }
- if (node.ip != null) { output["IP Address"] = node.ip; outputCount++; }
- if (node.desc != null) { output["Description"] = node.desc; outputCount++; }
- if (node.icon != null) { output["Icon"] = node.icon; outputCount++; }
- if (node.tags) { output["Tags"] = node.tags; outputCount++; }
- if (node.av) {
- var av = [];
- for (var i in node.av) {
- if (typeof node.av[i]['product'] == 'string') {
- var n = node.av[i]['product'];
- if (node.av[i]['updated'] === true) { n += ', updated'; }
- if (node.av[i]['updated'] === false) { n += ', not updated'; }
- if (node.av[i]['enabled'] === true) { n += ', enabled'; }
- if (node.av[i]['enabled'] === false) { n += ', disabled'; }
- av.push(n);
- }
- }
- output["AntiVirus"] = av; outputCount++;
- }
- if (typeof node.wsc == 'object') {
- output["WindowsSecurityCenter"] = node.wsc; outputCount++;
- }
- if (outputCount > 0) { info["General"] = output; }
-
- // Operating System
- var hardware = null;
- if ((sysinfo != null) && (sysinfo.hardware != null)) { hardware = sysinfo.hardware; }
- if ((hardware && hardware.windows && hardware.windows.osinfo) || node.osdesc) {
- var output = {}, outputCount = 0;
- if (node.rname) { output["Name"] = node.rname; outputCount++; }
- if (node.osdesc) { output["Version"] = node.osdesc; outputCount++; }
- if (hardware && hardware.windows && hardware.windows.osinfo) { var m = hardware.windows.osinfo; if (m.OSArchitecture) { output["Architecture"] = m.OSArchitecture; outputCount++; } }
- if (outputCount > 0) { info["Operating System"] = output; }
- }
-
- // MeshAgent
- if (node.agent) {
- var output = {}, outputCount = 0;
- var agentsStr = ["Unknown", "Windows 32bit console", "Windows 64bit console", "Windows 32bit service", "Windows 64bit service", "Linux 32bit", "Linux 64bit", "MIPS", "XENx86", "Android", "Linux ARM", "macOS x86-32bit", "Android x86", "PogoPlug ARM", "Android", "Linux Poky x86-32bit", "macOS x86-64bit", "ChromeOS", "Linux Poky x86-64bit", "Linux NoKVM x86-32bit", "Linux NoKVM x86-64bit", "Windows MinCore console", "Windows MinCore service", "NodeJS", "ARM-Linaro", "ARMv6l / ARMv7l", "ARMv8 64bit", "ARMv6l / ARMv7l / NoKVM", "MIPS24KC (OpenWRT)", "Apple Silicon", "FreeBSD x86-64", "Unknown", "Linux ARM 64 bit (glibc/2.24 NOKVM)", "Alpine Linux x86 64 Bit (MUSL)", "Assistant (Windows)", "Armada370 - ARM32/HF (libc/2.26)", "OpenWRT x86-64", "OpenBSD x86-64", "Unknown", "Unknown", "MIPSEL24KC (OpenWRT)", "ARMADA/CORTEX-A53/MUSL (OpenWRT)", "Windows ARM 64bit console", "Windows ARM 64bit service", "ARMVIRT32 (OpenWRT)", "RISC-V x86-64"];
- if ((node.agent != null) && (node.agent.id != null) && (node.agent.ver != null)) {
- var str = '';
- if (node.agent.id <= agentsStr.length) { str = agentsStr[node.agent.id]; } else { str = agentsStr[0]; }
- if (node.agent.ver != 0) { str += ' v' + node.agent.ver; }
- output["Mesh Agent"] = str; outputCount++;
- }
- if ((node.conn & 1) != 0) {
- output["Last agent connection"] = "Connected now"; outputCount++;
- } else {
- if (node.lastconnect) { output["Last agent connection"] = new Date(node.lastconnect).toLocaleString(); outputCount++; }
- }
- if (node.lastaddr) {
- var splitip = node.lastaddr.split(':');
- if (splitip.length > 2) {
- output["Last agent address"] = node.lastaddr; outputCount++; // IPv6
- } else {
- output["Last agent address"] = splitip[0]; outputCount++; // IPv4
- }
- }
- if ((node.agent != null) && (node.agent.tag != null)) {
- output["Tag"] = node.agent.tag; outputCount++;
- }
- if (outputCount > 0) { info["Mesh Agent"] = output; }
- }
-
- // Networking
- if (network.netif != null) {
- var output = {}, outputCount = 0, minfo = {};
- for (var i in network.netif) {
- var m = network.netif[i], moutput = {}, moutputCount = 0;
- if (m.desc) { moutput["Description"] = m.desc; moutputCount++; }
- if (m.mac) {
- if (m.gatewaymac) {
- moutput["MAC Layer"] = format("MAC: {0}, Gateway: {1}", m.mac, m.gatewaymac); moutputCount++;
- } else {
- moutput["MAC Layer"] = format("MAC: {0}", m.mac); moutputCount++;
- }
- }
- if (m.v4addr && (m.v4addr != '0.0.0.0')) {
- if (m.v4gateway && m.v4mask) {
- moutput["IPv4 Layer"] = format("IP: {0}, Mask: {1}, Gateway: {2}", m.v4addr, m.v4mask, m.v4gateway); moutputCount++;
- } else {
- moutput["IPv4 Layer"] = format("IP: {0}", m.v4addr); moutputCount++;
- }
- }
- if (moutputCount > 0) { minfo[m.name + (m.dnssuffix ? (', ' + m.dnssuffix) : '')] = moutput; info["Networking"] = minfo; }
- }
- }
-
- if (network.netif2 != null) {
- var minfo = {};
- for (var i in network.netif2) {
- var m = network.netif2[i], moutput = {}, moutputCount = 0;
-
- if (Array.isArray(m) == false ||
- m.length < 1 ||
- m[0] == null ||
- ((typeof m[0].mac == 'string') && (m[0].mac.startsWith('00:00:00:00')))
- )
- continue;
-
- var ifTitle = '' + i;
- if (m[0].fqdn != null && m[0].fqdn != '') ifTitle += ', ' + m[0].fqdn;
-
- if (typeof m[0].mac == 'string') {
- if (m[0].gatewaymac) {
- moutput['MAC Layer'] = format("MAC: {0}, Gateway: {1}", m[0].mac, m[0].gatewaymac);
- } else {
- moutput['MAC Layer'] = format("MAC: {0}", m[0].mac);
- }
- moutputCount++;
- }
-
- moutput['IPv4 Layer'] = '';
- moutput['IPv6 Layer'] = '';
- for (var j = 0; j < m.length; j++) {
- var iplayer = m[j];
- if (iplayer.family == 'IPv4' || iplayer.family == 'IPv6') {
- if (iplayer.gateway && iplayer.netmask) {
- moutput[iplayer.family + ' Layer'] += format("IP: {0}, Mask: {1}, Gateway: {2} ", iplayer.address, iplayer.netmask, iplayer.gateway);
- moutputCount++;
- } else {
- if (iplayer.address) {
- moutput[iplayer.family + ' Layer'] += format("IP: {0} ", iplayer.address);
- moutputCount++;
- }
- }
- }
- }
- if (moutput['IPv4 Layer'] == '') delete moutput['IPv4 Layer'];
- if (moutput['IPv6 Layer'] == '') delete moutput['IPv6 Layer'];
- if (moutputCount > 0) {
- minfo[ifTitle] = moutput;
- info["Networking"] = minfo;
- }
- }
- }
-
- // Intel AMT
- if (node.intelamt != null) {
- var output = {}, outputCount = 0;
- output["Version"] = (node.intelamt.ver) ? ('v' + node.intelamt.ver) : ('' + "Unknown" + ''); outputCount++;
- var provisioningStates = { 0: "Not Activated (Pre)", 1: "Not Activated (In)", 2: "Activated" };
- var provisioningMode = '';
- if ((node.intelamt.state == 2) && node.intelamt.flags) { if (node.intelamt.flags & 2) { provisioningMode = (', ' + "Client Control Mode (CCM)"); } else if (node.intelamt.flags & 4) { provisioningMode = (', ' + "Admin Control Mode (ACM)"); } }
- output["Provisioning State"] = ((node.intelamt.state) ? (provisioningStates[node.intelamt.state]) : ('' + "Unknown" + '')) + provisioningMode; outputCount++;
- output["Security"] = (node.intelamt.tls == 1) ? "Secured using TLS" : "TLS is not setup"; outputCount++;
- output["Admin Credentials"] = (node.intelamt.user == null || node.intelamt.user == '') ? "Not Known" : "Known"; outputCount++;
- if (outputCount > 0) { info["Intel Active Management Technology (Intel AMT)"] = output; }
- }
-
- if (hardware != null) {
- if (hardware.identifiers) {
- var output = {}, outputCount = 0, ident = hardware.identifiers;
- // BIOS
- if (ident.bios_vendor) { output["Vendor"] = ident.bios_vendor; outputCount++; }
- if (ident.bios_version) { output["Version"] = ident.bios_version; outputCount++; }
- if (outputCount > 0) { info["BIOS"] = output; }
- output = {}, outputCount = 0;
-
- // Motherboard
- if (ident.board_vendor) { output["Vendor"] = ident.board_vendor; outputCount++; }
- if (ident.board_name) { output["Name"] = ident.board_name; outputCount++; }
- if (ident.board_serial && (ident.board_serial != '')) { output["Serial"] = ident.board_serial; outputCount++; }
- if (ident.board_version) { output["Version"] = ident.board_version; }
- if (ident.product_uuid) { output["Identifier"] = ident.product_uuid; }
- if (ident.cpu_name) { output["CPU"] = ident.cpu_name; }
- if (ident.gpu_name) { for (var i in ident.gpu_name) { output["GPU" + (parseInt(i) + 1)] = ident.gpu_name[i]; } }
- if (outputCount > 0) { info["Motherboard"] = output; }
- }
-
- // Memory
- if (hardware.windows) {
- if (hardware.windows.memory) {
- var output = {}, outputCount = 0, minfo = {};
- hardware.windows.memory.sort(function (a, b) { if (a.BankLabel > b.BankLabel) return 1; if (a.BankLabel < b.BankLabel) return -1; return 0; });
- for (var i in hardware.windows.memory) {
- var m = hardware.windows.memory[i], moutput = {}, moutputCount = 0;
- if (m.Capacity) { moutput["Capacity/Speed"] = (m.Capacity / 1024 / 1024) + " Mb, " + m.Speed + " Mhz"; moutputCount++; }
- if (m.PartNumber) { moutput["Part Number"] = ((m.Manufacturer && m.Manufacturer != 'Undefined') ? (m.Manufacturer + ', ') : '') + m.PartNumber; moutputCount++; }
- if (moutputCount > 0) { minfo[m.BankLabel] = moutput; info["Memory"] = minfo; }
- }
- }
- }
-
- // Storage
- if (hardware.identifiers && ident.storage_devices) {
- var output = {}, outputCount = 0, minfo = {};
- // Sort Storage
- ident.storage_devices.sort(function (a, b) { if (a.Caption > b.Caption) return 1; if (a.Caption < b.Caption) return -1; return 0; });
- for (var i in ident.storage_devices) {
- var m = ident.storage_devices[i], moutput = {};
- if (m.Size) {
- if (m.Model && (m.Model != m.Caption)) { moutput["Model"] = m.Model; outputCount++; }
- if ((typeof m.Size == 'string') && (parseInt(m.Size) == m.Size)) { m.Size = parseInt(m.Size); }
- if (typeof m.Size == 'number') { moutput["Capacity"] = Math.floor(m.Size / 1024 / 1024) + 'Mb'; outputCount++; }
- if (typeof m.Size == 'string') { moutput["Capacity"] = m.Size; outputCount++; }
- if (moutputCount > 0) { minfo[m.Caption] = moutput; info["Storage"] = minfo; }
- }
- }
- }
- }
-
- // Display everything
- if (args.json) {
- console.log(JSON.stringify(info, ' ', 2));
- } else {
- for (var i in info) {
- console.log('--- ' + i + ' ---');
- for (var j in info[i]) {
- if ((typeof info[i][j] == 'string') || (typeof info[i][j] == 'number')) {
- console.log(' ' + j + ': ' + info[i][j]);
- } else {
- console.log(' ' + j + ':');
- for (var k in info[i][j]) {
- console.log(' ' + k + ': ' + info[i][j][k]);
- }
- }
- }
- }
- }
-}
-
-// Read the Mesh Agent error log and index it.
-function indexAgentErrorLog() {
- // Index the messages
- const lines = require('fs').readFileSync('../meshcentral-data/agenterrorlogs.txt', { encoding: 'utf8', flag: 'r' }).split('\r\n');
- var errorIndex = {}; // "msg" --> [ { lineNumber, elemenetNumber } ]
- for (var i = 0; i < lines.length; i++) {
- const line = lines[i];
- if (line.length > 88) {
- var nodeid = line.substring(0, 70);
- var fetchTime = parseInt(line.substring(72, 85));
- var data = JSON.parse(line.substring(87));
- if ((data != null) && (data.action == 'errorlog') && (Array.isArray(data.log))) {
- for (var j = 0; j < data.log.length; j++) {
- var entry = data.log[j];
- if ((entry != null) && (typeof entry.t == 'number') && (typeof entry.m == 'string')) {
- const msg = entry.m;
- if (errorIndex[msg] == null) { errorIndex[msg] = []; }
- errorIndex[msg].push({ l: i, e: j });
- }
- }
- }
- }
- }
-
- // Sort the messages by frequency
- var errorIndexCount = []; // [ { m: "msg", c: count } ]
- for (var i in errorIndex) { errorIndexCount.push({ m: i, c: errorIndex[i].length }); }
- errorIndexCount = errorIndexCount.sort(function (a, b) { return b.c - a.c })
-
- // Display the results
- for (var i = 0; i < errorIndexCount.length; i++) {
- const m = errorIndexCount[i].m;
- if ((m.indexOf('STUCK') >= 0) || (m.indexOf('FATAL') >= 0)) { console.log(errorIndexCount[i].c, m); }
- }
-}
+#!/usr/bin/env node
+
+/**
+* @description MeshCentral command line tool
+* @author Ylian Saint-Hilaire
+* @copyright Intel Corporation 2018-2022
+* @license Apache-2.0
+* @version v0.0.1
+*/
+
+// Make sure we have the dependency modules
+try { require('minimist'); } catch (ex) { console.log('Missing module "minimist", type "npm install minimist" to install it.'); return; }
+try { require('ws'); } catch (ex) { console.log('Missing module "ws", type "npm install ws" to install it.'); return; }
+
+var settings = {};
+const crypto = require('crypto');
+const args = require('minimist')(process.argv.slice(2));
+const path = require('path');
+const possibleCommands = ['edituser', 'listusers', 'listusersessions', 'listdevicegroups', 'listdevices', 'listusersofdevicegroup', 'listevents', 'logintokens', 'serverinfo', 'userinfo', 'adduser', 'removeuser', 'adddevicegroup', 'removedevicegroup', 'editdevicegroup', 'broadcast', 'showevents', 'addusertodevicegroup', 'removeuserfromdevicegroup', 'addusertodevice', 'removeuserfromdevice', 'sendinviteemail', 'generateinvitelink', 'config', 'movetodevicegroup', 'deviceinfo', 'removedevice', 'editdevice', 'addlocaldevice', 'addamtdevice', 'addusergroup', 'listusergroups', 'removeusergroup', 'runcommand', 'shell', 'upload', 'download', 'deviceopenurl', 'devicemessage', 'devicetoast', 'addtousergroup', 'removefromusergroup', 'removeallusersfromusergroup', 'devicesharing', 'devicepower', 'indexagenterrorlog', 'agentdownload', 'report', 'grouptoast', 'groupmessage', 'webrelay'];
+if (args.proxy != null) { try { require('https-proxy-agent'); } catch (ex) { console.log('Missing module "https-proxy-agent", type "npm install https-proxy-agent" to install it.'); return; } }
+
+if (args['_'].length == 0) {
+ console.log("MeshCtrl performs command line actions on a MeshCentral server.");
+ console.log("Information at: https://meshcentral.com");
+ console.log("No action specified, use MeshCtrl like this:\r\n\r\n meshctrl [action] [arguments]\r\n");
+ console.log("Supported actions:");
+ console.log(" Help [action] - Get help on an action.");
+ console.log(" ServerInfo - Show server information.");
+ console.log(" UserInfo - Show user information.");
+ console.log(" ListUsers - List user accounts.");
+ console.log(" ListUserSessions - List online users.");
+ console.log(" ListUserGroups - List user groups.");
+ console.log(" ListDevices - List devices.");
+ console.log(" ListDeviceGroups - List device groups.");
+ console.log(" ListUsersOfDeviceGroup - List the users in a device group.");
+ console.log(" ListEvents - List server events.");
+ console.log(" LoginTokens - List, create and remove login tokens.");
+ console.log(" DeviceInfo - Show information about a device.");
+ console.log(" AddLocalDevice - Add a local device.");
+ console.log(" AddAmtDevice - Add a AMT device.");
+ console.log(" EditDevice - Make changes to a device.");
+ console.log(" RemoveDevice - Delete a device.");
+ console.log(" Config - Perform operation on config.json file.");
+ console.log(" AddUser - Create a new user account.");
+ console.log(" EditUser - Change a user account.");
+ console.log(" RemoveUser - Delete a user account.");
+ console.log(" AddUserGroup - Create a new user group.");
+ console.log(" RemoveUserGroup - Delete a user group.");
+ console.log(" AddToUserGroup - Add a user, device or device group to a user group.");
+ console.log(" RemoveFromUserGroup - Remove a user, device or device group from a user group.");
+ console.log(" RemoveAllUsersFromUserGroup - Remove all users from a user group.");
+ console.log(" AddDeviceGroup - Create a new device group.");
+ console.log(" RemoveDeviceGroup - Delete a device group.");
+ console.log(" EditDeviceGroup - Change a device group values.");
+ console.log(" MoveToDeviceGroup - Move a device to a different device group.");
+ console.log(" AddUserToDeviceGroup - Add a user to a device group.");
+ console.log(" RemoveUserFromDeviceGroup - Remove a user from a device group.");
+ console.log(" AddUserToDevice - Add a user to a device.");
+ console.log(" RemoveUserFromDevice - Remove a user from a device.");
+ console.log(" SendInviteEmail - Send an agent install invitation email.");
+ console.log(" GenerateInviteLink - Create an invitation link.");
+ console.log(" Broadcast - Display a message to all online users.");
+ console.log(" ShowEvents - Display real-time server events in JSON format.");
+ console.log(" RunCommand - Run a shell command on a remote device.");
+ console.log(" Shell - Access command shell of a remote device.");
+ console.log(" Upload - Upload a file to a remote device.");
+ console.log(" Download - Download a file from a remote device.");
+ console.log(" WebRelay - Creates a HTTP/HTTPS webrelay link for a remote device.");
+ console.log(" DeviceOpenUrl - Open a URL on a remote device.");
+ console.log(" DeviceMessage - Open a message box on a remote device.");
+ console.log(" DeviceToast - Display a toast notification on a remote device.");
+ console.log(" GroupMessage - Open a message box on remote devices in a specific device group.");
+ console.log(" GroupToast - Display a toast notification on remote devices in a specific device group.");
+ console.log(" DevicePower - Perform wake/sleep/reset/off operations on remote devices.");
+ console.log(" DeviceSharing - View, add and remove sharing links for a given device.");
+ console.log(" AgentDownload - Download an agent of a specific type for a device group.");
+ console.log(" Report - Create and show a CSV report.");
+ console.log("\r\nSupported login arguments:");
+ console.log(" --url [wss://server] - Server url, wss://localhost:443 is default.");
+ console.log(" - Use wss://localhost:443?key=xxx if login key is required.");
+ console.log(" --loginuser [username] - Login username, admin is default.");
+ console.log(" --loginpass [password] - Login password OR Leave blank to enter password at prompt");
+ console.log(" --token [number] - 2nd factor authentication token.");
+ console.log(" --loginkey [hex] - Server login key in hex.");
+ console.log(" --loginkeyfile [file] - File containing server login key in hex.");
+ console.log(" --logindomain [domainid] - Domain id, default is empty, only used with loginkey.");
+ console.log(" --proxy [http://proxy:123] - Specify an HTTP proxy.");
+ return;
+} else {
+ settings.cmd = args['_'][0].toLowerCase();
+ if ((possibleCommands.indexOf(settings.cmd) == -1) && (settings.cmd != 'help')) { console.log("Invalid command. Possible commands are: " + possibleCommands.join(', ') + '.'); return; }
+ //console.log(settings.cmd);
+
+ var ok = false;
+ switch (settings.cmd) {
+ case 'config': { performConfigOperations(args); return; }
+ case 'indexagenterrorlog': { indexAgentErrorLog(); return; }
+ case 'serverinfo': { ok = true; break; }
+ case 'userinfo': { ok = true; break; }
+ case 'listusers': { ok = true; break; }
+ case 'listusersessions': { ok = true; break; }
+ case 'listusergroups': { ok = true; break; }
+ case 'listdevicegroups': { ok = true; break; }
+ case 'listdevices': { ok = true; break; }
+ case 'listevents': { ok = true; break; }
+ case 'logintokens': { ok = true; break; }
+ case 'listusersofdevicegroup':
+ case 'deviceinfo':
+ case 'removedevice':
+ case 'editdevice': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else { ok = true; }
+ break;
+ }
+ case 'addlocaldevice': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else if (args.devicename == null) { console.log(winRemoveSingleQuotes("Missing devicename, use --devicename [devicename]")); }
+ else if (args.hostname == null) { console.log(winRemoveSingleQuotes("Missing hostname, use --hostname [hostname]")); }
+ else { ok = true; }
+ break;
+ }
+ case 'addamtdevice': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else if (args.devicename == null) { console.log(winRemoveSingleQuotes("Missing devicename, use --devicename [devicename]")); }
+ else if (args.hostname == null) { console.log(winRemoveSingleQuotes("Missing hostname, use --hostname [hostname]")); }
+ else if (args.user == null) { console.log(winRemoveSingleQuotes("Missing user, use --user [user]")); }
+ else if (args.pass == null) { console.log(winRemoveSingleQuotes("Missing pass, use --pass [pass]")); }
+ else { ok = true; }
+ break;
+ }
+ case 'addusertodevicegroup': {
+ if ((args.id == null) && (args.group == null)) { console.log(winRemoveSingleQuotes("Device group identifier missing, use --id '[groupid]' or --group [groupname]")); }
+ else if (args.userid == null) { console.log("Add user to group missing useid, use --userid [userid]"); }
+ else { ok = true; }
+ break;
+ }
+ case 'removeuserfromdevicegroup': {
+ if ((args.id == null) && (args.group == null)) { console.log(winRemoveSingleQuotes("Device group identifier missing, use --id '[groupid]' or --group [groupname]")); }
+ else if (args.userid == null) { console.log("Remove user from group missing useid, use --userid [userid]"); }
+ else { ok = true; }
+ break;
+ }
+ case 'addusertodevice': {
+ if (args.userid == null) { console.log("Add user to device missing userid, use --userid [userid]"); }
+ else if (args.id == null) { console.log(winRemoveSingleQuotes("Add user to device missing device id, use --id '[deviceid]'")); }
+ else { ok = true; }
+ break;
+ }
+ case 'removeuserfromdevice': {
+ if (args.userid == null) { console.log("Remove user from device missing userid, use --userid [userid]"); }
+ else if (args.id == null) { console.log(winRemoveSingleQuotes("Remove user from device missing device id, use --id '[deviceid]'")); }
+ else { ok = true; }
+ break;
+ }
+ case 'adddevicegroup': {
+ if (args.name == null) { console.log("Message group name, use --name [name]"); }
+ else { ok = true; }
+ break;
+ }
+ case 'editdevicegroup':
+ case 'removedevicegroup': {
+ if ((args.id == null) && (args.group == null)) { console.log(winRemoveSingleQuotes("Device group identifier missing, use --id '[groupid]' or --group [groupname]")); }
+ else { ok = true; }
+ break;
+ }
+ case 'movetodevicegroup': {
+ if ((args.id == null) && (args.group == null)) { console.log(winRemoveSingleQuotes("Device group identifier missing, use --id '[groupid]' or --group [groupname]")); }
+ else if (args.devid == null) { console.log(winRemoveSingleQuotes("Device identifier missing, use --devid '[deviceid]'")); }
+ else { ok = true; }
+ break;
+ }
+ case 'broadcast': {
+ if (args.msg == null) { console.log("Message missing, use --msg [message]"); }
+ else { ok = true; }
+ break;
+ }
+ case 'showevents': {
+ ok = true;
+ break;
+ }
+ case 'adduser': {
+ if (args.user == null) { console.log("New account name missing, use --user [name]"); }
+ else if ((args.pass == null) && (args.randompass == null)) { console.log("New account password missing, use --pass [password] or --randompass"); }
+ else { ok = true; }
+ break;
+ }
+ case 'edituser': {
+ if (args.userid == null) { console.log("Edit account user missing, use --userid [id]"); }
+ else { ok = true; }
+ break;
+ }
+ case 'removeuser': {
+ if (args.userid == null) { console.log("Remove account userid missing, use --userid [id]"); }
+ else { ok = true; }
+ break;
+ }
+ case 'addusergroup': {
+ if (args.name == null) { console.log("New user group name missing, use --name [name]"); }
+ else { ok = true; }
+ break;
+ }
+ case 'removeusergroup': {
+ if (args.groupid == null) { console.log(winRemoveSingleQuotes("Remove user group id missing, use --groupid '[id]'")); }
+ else { ok = true; }
+ break;
+ }
+ case 'addtousergroup': {
+ if (args.groupid == null) { console.log(winRemoveSingleQuotes("Group id missing, use --groupid '[id]'")); }
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing identifier to add, use --id [id]")); }
+ else { ok = true; }
+ break;
+ }
+ case 'removefromusergroup': {
+ if (args.groupid == null) { console.log(winRemoveSingleQuotes("Group id missing, use --groupid '[id]'")); }
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing identifier to remove, use --id [id]")); }
+ else { ok = true; }
+ break;
+ }
+ case 'removeallusersfromusergroup': {
+ if (args.groupid == null) { console.log(winRemoveSingleQuotes("Group id missing, use --groupid '[id]'")); }
+ else { ok = true; }
+ break;
+ }
+ case 'sendinviteemail': {
+ if ((args.id == null) && (args.group == null)) { console.log("Device group identifier missing, use --id '[groupid]' or --group [groupname]"); }
+ else if (args.email == null) { console.log("Device email is missing, use --email [email]"); }
+ else { ok = true; }
+ break;
+ }
+ case 'generateinvitelink': {
+ if ((args.id == null) && (args.group == null)) { console.log("Device group identifier missing, use --id '[groupid]' or --group [groupname]"); }
+ else if (args.hours == null) { console.log("Invitation validity period missing, use --hours [hours]"); }
+ else { ok = true; }
+ break;
+ }
+ case 'runcommand': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else if (args.run == null) { console.log("Missing run, use --run \"command\""); }
+ else { ok = true; }
+ break;
+ }
+ case 'shell': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else { ok = true; }
+ break;
+ }
+ case 'devicepower': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else { ok = true; }
+ break;
+ }
+ case 'devicesharing': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else if ((args.daily != null) && (args.weekly != null)) { console.log(winRemoveSingleQuotes("Can't specify both --daily and --weekly at the same time.")); }
+ else { ok = true; }
+ break;
+ }
+ case 'agentdownload': {
+ if (args.type == null) { console.log(winRemoveSingleQuotes("Missing device type, use --type [agenttype]")); }
+ else if ((parseInt(args.type) == null) || isNaN(parseInt(args.type)) || (parseInt(args.type) < 1) || (parseInt(args.type) > 11000)) { console.log(winRemoveSingleQuotes("Invalid agent type, must be a number.")); }
+ else if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[meshid]'")); }
+ else if ((typeof args.id != 'string') || (args.id.length != 64)) { console.log(winRemoveSingleQuotes("Invalid meshid.")); }
+ else { ok = true; }
+ break;
+ }
+ case 'upload': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else if (args.file == null) { console.log("Local file missing, use --file [file] specify the file to upload"); }
+ else if (args.target == null) { console.log("Remote target path missing, use --target [path] to specify the remote location"); }
+ else if (require('fs').existsSync(args.file) == false) { console.log("Local file does not exists, check --file"); }
+ else { ok = true; }
+ break;
+ }
+ case 'download': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else if (args.file == null) { console.log("Remote file missing, use --file [file] specify the remote file to download"); }
+ else if (args.target == null) { console.log("Target path missing, use --target [path] to specify the local download location"); }
+ else { ok = true; }
+ break;
+ }
+ case 'webrelay': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else if (args.type == null) { console.log(winRemoveSingleQuotes("Missing protocol type, use --type [http,https]")); }
+ else { ok = true; }
+ break;
+ }
+ case 'deviceopenurl': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else if (args.openurl == null) { console.log("Remote URL, use --openurl [url] specify the link to open."); }
+ else { ok = true; }
+ break;
+ }
+ case 'devicemessage': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else if (args.msg == null) { console.log("Remote message, use --msg \"[message]\" specify a remote message."); }
+ else { ok = true; }
+ break;
+ }
+ case 'devicetoast': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device id, use --id '[deviceid]'")); }
+ else if (args.msg == null) { console.log("Remote message, use --msg \"[message]\" specify a remote message."); }
+ else { ok = true; }
+ break;
+ }
+ case 'groupmessage': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device group id, use --id '[devicegroupid]'")); }
+ else if (args.msg == null) { console.log("Remote message, use --msg \"[message]\" specify a remote message."); }
+ else { ok = true; }
+ break;
+ }
+ case 'grouptoast': {
+ if (args.id == null) { console.log(winRemoveSingleQuotes("Missing device group id, use --id '[devicegroupid]'")); }
+ else if (args.msg == null) { console.log("Remote message, use --msg \"[message]\" specify a remote message."); }
+ else { ok = true; }
+ break;
+ }
+ case 'report': {
+ if (args.type == null) { console.log(winRemoveSingleQuotes("Missing report type, use --type '[reporttype]'")); }
+ else { ok = true; }
+ break;
+ }
+ case 'help': {
+ if (args['_'].length < 2) {
+ console.log("Get help on an action. Type:\r\n\r\n help [action]\r\n\r\nPossible actions are: " + possibleCommands.join(', ') + '.');
+ } else {
+ switch (args['_'][1].toLowerCase()) {
+ case 'config': {
+ displayConfigHelp();
+ break;
+ }
+ case 'sendinviteemail': {
+ console.log("Send invitation email with instructions on how to install the mesh agent for a specific device group. Example usage:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl SendInviteEmail --id 'groupid' --message \"msg\" --email user@sample.com"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl SendInviteEmail --group \"My Computers\" --name \"Jack\" --email user@sample.com"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [groupid] - Device group identifier (or --group).");
+ } else {
+ console.log(" --id '[groupid]' - Device group identifier (or --group).");
+ }
+ console.log(" --group [groupname] - Device group name (or --id).");
+ console.log(" --email [email] - Email address.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --name (name) - Name of recipient to be included in the email.");
+ console.log(" --message (msg) - Message to be included in the email.");
+ break;
+ }
+ case 'generateinvitelink': {
+ console.log("Generate a agent invitation URL for a given group. Example usage:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl GenerateInviteLink --id 'groupid' --hours 24"));
+ console.log(" MeshCtrl GenerateInviteLink --group \"My Computers\" --hours 0");
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [groupid] - Device group identifier (or --group).");
+ } else {
+ console.log(" --id '[groupid]' - Device group identifier (or --group).");
+ }
+ console.log(" --group [groupname] - Device group name (or --id).");
+ console.log(" --hours [hours] - Validity period in hours or 0 for infinite.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --flags [mode] - Mode flag for link type (0 = both, 1 = interactive only, 2 = background only)");
+ break;
+ }
+ case 'showevents': {
+ console.log("Show the server's event stream for this user account. Example usage:\r\n");
+ console.log(" MeshCtrl ShowEvents");
+ console.log(" MeshCtrl ShowEvents --filter nodeconnect");
+ console.log(" MeshCtrl ShowEvents --filter uicustomevent,changenode");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --filter [actions] - Show only specified actions.");
+ break;
+ }
+ case 'serverinfo': {
+ console.log("Get information on the MeshCentral server, Example usages:\r\n");
+ console.log(" MeshCtrl ServerInfo --loginuser myaccountname --loginpass mypassword");
+ console.log(" MeshCtrl ServerInfo --loginuser myaccountname --loginkeyfile key.txt");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --json - Show result as JSON.");
+ break;
+ }
+ case 'userinfo': {
+ console.log("Get account information for the login account, Example usages:\r\n");
+ console.log(" MeshCtrl UserInfo --loginuser myaccountname --loginpass mypassword");
+ console.log(" MeshCtrl UserInfo --loginuser myaccountname --loginkeyfile key.txt");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --json - Show result as JSON.");
+ break;
+ }
+ case 'listusers': {
+ console.log("List the account on the MeshCentral server, Example usages:\r\n");
+ console.log(" MeshCtrl ListUsers");
+ console.log(" MeshCtrl ListUsers --json");
+ console.log(" MeshCtrl ListUsers --nameexists \"bob\"");
+ console.log(" MeshCtrl ListUsers --filter 2fa");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --idexists [id] - Return 1 if id exists, 0 if not.");
+ console.log(" --nameexists [name] - Return id if name exists.");
+ console.log(" --filter [filter1,...] - Filter user names: 2FA, NO2FA.");
+ console.log(" --json - Show result as JSON.");
+ break;
+ }
+ case 'listusersessions': {
+ console.log("List active user sessions on the MeshCentral server, Example usages:\r\n");
+ console.log(" MeshCtrl ListUserSessions");
+ console.log(" MeshCtrl ListUserSessions --json");
+ break;
+ }
+ case 'listusergroups': {
+ console.log("List user groups on the MeshCentral server, Example usages:\r\n");
+ console.log(" MeshCtrl ListUserGroups");
+ console.log(" MeshCtrl ListUserGroups --json");
+ break;
+ }
+ case 'listdevicegroups': {
+ console.log("List the device groups for this account. Example usages:\r\n");
+ console.log(" MeshCtrl ListDeviceGroups ");
+ console.log(" MeshCtrl ListDeviceGroups --json");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --idexists [id] - Return 1 if id exists, 0 if not.");
+ console.log(" --nameexists [name] - Return id if name exists.");
+ console.log(" --emailexists [email] - Return id if email exists.");
+ console.log(" --hex - Display meshid in hex format.");
+ console.log(" --json - Show result as JSON.");
+ break;
+ }
+ case 'listdevices': {
+ console.log("List devices. Example usages:\r\n");
+ console.log(" MeshCtrl ListDevices");
+ console.log(winRemoveSingleQuotes(" MeshCtrl ListDevices -id '[groupid]' --json"));
+ console.log("\r\nOptional arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [groupid] - Filter by group identifier (or --group).");
+ } else {
+ console.log(" --id '[groupid]' - Filter by group identifier (or --group).");
+ }
+ console.log(" --group [groupname] - Filter by group name (or --id).");
+ console.log(" --count - Only return the device count.");
+ console.log(" --json - Show result as JSON.");
+ console.log(" --csv - Show result as comma separated values.");
+ console.log(" --filter \"[filter]\" - Filter devices using a filter string.");
+ console.log(" \"x\" - Devices with \"x\" in the name.");
+ console.log(" \"user:x or u:x\" - Devices with \"x\" in the name of currently logged in user.");
+ console.log(" \"ip:x\" - Devices \"x\" IP address.");
+ console.log(" \"group:x or g:x\" - Devices with \"x\" in device group name.");
+ console.log(" \"tag:x or t:x\" - Devices with \"x\" in device tag.");
+ console.log(" \"atag:x or a:x\" - Devices with \"x\" in device agent tag.");
+ console.log(" \"os:x\" - Devices with \"x\" in the device OS description.");
+ console.log(" \"amt:x\" - Devices with Intel AMT provisioning state (0, 1, 2).");
+ console.log(" \"desc:x\" - Devices with \"x\" in device description.");
+ console.log(" \"wsc:ok\" - Devices with Windows Security Center ok.");
+ console.log(" \"wsc:noav\" - Devices with Windows Security Center with anti-virus problem.");
+ console.log(" \"wsc:noupdate\" - Devices with Windows Security Center with update problem.");
+ console.log(" \"wsc:nofirewall\" - Devices with Windows Security Center with firewall problem.");
+ console.log(" \"wsc:any\" - Devices with Windows Security Center with any problem.");
+ console.log(" \"a and b\" - Match both conditions with precedence over OR. For example: \"lab and g:home\".");
+ console.log(" \"a or b\" - Math one of the conditions, for example: \"lab or g:home\".");
+ console.log(" --filterid [id,id...] - Show only results for devices with included id.");
+ console.log(" --details - Show all device details.");
+ break;
+ }
+ case 'listusersofdevicegroup': {
+ console.log("List users that have permissions for a given device group. Example usage:\r\n");
+ console.log(" MeshCtrl ListUserOfDeviceGroup ");
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [groupid] - Device group identifier.");
+ } else {
+ console.log(" --id '[groupid]' - Device group identifier.");
+ }
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --json - Show result as JSON.");
+ break;
+ }
+ case 'listevents': {
+ console.log("List server events optionally filtered by user or device. Example usage:\r\n");
+ console.log(" MeshCtrl ListEvents ");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --userid [name] - User account identifier.");
+ console.log(" --id [deviceid] - The device identifier.");
+ console.log(" --limit [number] - Maximum number of events to list.");
+ console.log(" --raw - Output raw data in JSON format.");
+ console.log(" --json - Give results in JSON format.");
+ break;
+ }
+ case 'logintokens': {
+ console.log("List account login tokens and allow addition and removal. Example usage:\r\n");
+ console.log(" MeshCtrl LoginTokens ");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --remove [name] - Remove a login token.");
+ console.log(" --add [name] - Add a login token.");
+ console.log(" --expire [minutes] - When adding a token, minutes until expire.");
+ console.log(" --json - Show login tokens in JSON format.");
+ break;
+ }
+ case 'adduser': {
+ console.log("Add a new user account. Example usages:\r\n");
+ console.log(" MeshCtrl AddUser --user newaccountname --pass newpassword");
+ console.log(" MeshCtrl AddUser --user newaccountname --randompass --rights full");
+ console.log("\r\nRequired arguments:\r\n");
+ console.log(" --user [name] - New account name.");
+ console.log(" --pass [password] - New account password.");
+ console.log(" --randompass - Create account with a random password.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --domain [domain] - Account domain, only for cross-domain admins.");
+ console.log(" --email [email] - New account email address.");
+ console.log(" --emailverified - New account email is verified.");
+ console.log(" --resetpass - Request password reset on next login.");
+ console.log(" --realname [name] - Set the real name for this account.");
+ console.log(" --phone [number] - Set the account phone number.");
+ console.log(" --rights [none|full|a,b,c] - Comma separated list of server permissions. Possible values:");
+ console.log(" manageusers,serverbackup,serverrestore,serverupdate,fileaccess,locked,nonewgroups,notools,usergroups,recordings,locksettings,allevents,nonewdevices");
+ break;
+ }
+ case 'edituser': {
+ console.log("Edit a user account, Example usages:\r\n");
+ console.log(" MeshCtrl EditUser --userid user --rights locked,locksettings");
+ console.log(" MeshCtrl EditUser --userid user --realname Jones");
+ console.log("\r\nRequired arguments:\r\n");
+ console.log(" --userid [name] - User account identifier.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --domain [domain] - Account domain, only for cross-domain admins.");
+ console.log(" --email [email] - Account email address.");
+ console.log(" --emailverified - Account email is verified.");
+ console.log(" --resetpass - Request password reset on next login.");
+ console.log(" --realname [name] - Set the real name for this account.");
+ console.log(" --phone [number] - Set the account phone number.");
+ console.log(" --rights [none|full|a,b,c] - Comma separated list of server permissions. Possible values:");
+ console.log(" manageusers,serverbackup,serverrestore,serverupdate,fileaccess,locked,nonewgroups,notools,usergroups,recordings,locksettings,allevents,nonewdevices");
+ break;
+ }
+ case 'removeuser': {
+ console.log("Delete a user account, Example usages:\r\n");
+ console.log(" MeshCtrl RemoveUser --userid accountid");
+ console.log("\r\nRequired arguments:\r\n");
+ console.log(" --userid [id] - Account identifier.");
+ break;
+ }
+ case 'addusergroup': {
+ console.log("Create a new user group, Example usages:\r\n");
+ console.log(" MeshCtrl AddUserGroup --name \"Test Group\"");
+ console.log("\r\nRequired arguments:\r\n");
+ console.log(" --name [name] - Name of the user group.");
+ break;
+ }
+ case 'removeusergroup': {
+ console.log("Remove a user group, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl RemoveUserGroup --groupid 'ugrp//abcdf'"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --groupid [groupid] - User group identifier.");
+ } else {
+ console.log(" --groupid '[groupid]' - User group identifier.");
+ }
+ break;
+ }
+ case 'addtousergroup': {
+ console.log("Add a user, device or device group to a user group, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl AddToUserGroup --id 'user//abcdef' --groupid 'ugrp//abcdf'"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl AddToUserGroup --id 'node//abcdef' --groupid 'ugrp//abcdf' --rights [rights]"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl AddToUserGroup --id 'mesh//abcdef' --groupid 'ugrp//abcdf' --rights [rights]"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [id] - Identifier to add.");
+ console.log(" --groupid [groupid] - User group identifier.");
+ } else {
+ console.log(" --id '[id]' - Identifier to add.");
+ console.log(" --groupid '[groupid]' - User group identifier.");
+ }
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --rights [number] - Rights granted for adding device or device group.");
+ console.log(" - 4294967295 for full admin or the sum of the following numbers.");
+ console.log(" 1 = Edit Device Group 2 = Manage Users ");
+ console.log(" 4 = Manage Computers 8 = Remote Control ");
+ console.log(" 16 = Agent Console 32 = Server Files ");
+ console.log(" 64 = Wake Device 128 = Set Notes ");
+ console.log(" 256 = Remote View Only 512 = No Terminal ");
+ console.log(" 1024 = No Files 2048 = No Intel AMT ");
+ console.log(" 4096 = Desktop Limited Input 8192 = Limit Events ");
+ console.log(" 16384 = Chat / Notify 32768 = Uninstall Agent ");
+ console.log(" 65536 = No Remote Desktop 131072 = Remote Commands ");
+ console.log(" 262144 = Reset / Power off ");
+ break;
+ }
+ case 'removefromusergroup': {
+ console.log("Remove a user, device or device group from a user group, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl RemoveUserFromUserGroup --userid 'user//abcdef' --groupid 'ugrp//abcdf'"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl RemoveUserFromUserGroup --userid 'node//abcdef' --groupid 'ugrp//abcdf'"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl RemoveUserFromUserGroup --userid 'mesh//abcdef' --groupid 'ugrp//abcdf'"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [userid] - Identifier to remove.");
+ console.log(" --groupid [groupid] - User group identifier.");
+ } else {
+ console.log(" --id '[userid]' - Identifier to remove.");
+ console.log(" --groupid '[groupid]' - User group identifier.");
+ }
+ break;
+ }
+ case 'removeallusersfromusergroup': {
+ console.log("Remove all users from a user group, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl RemoveAllUsersFromUserGroup --groupid 'ugrp//abcdf'"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --groupid [groupid] - User group identifier.");
+ } else {
+ console.log(" --groupid '[groupid]' - User group identifier.");
+ }
+ break;
+ }
+ case 'adddevicegroup': {
+ console.log("Add a device group, Example usages:\r\n");
+ console.log(" MeshCtrl AddDeviceGroup --name newgroupname");
+ console.log(" MeshCtrl AddDeviceGroup --name newgroupname --desc description --amtonly");
+ console.log(" MeshCtrl AddDeviceGroup --name newgroupname --features 1 --consent 7");
+ console.log("\r\nRequired arguments:\r\n");
+ console.log(" --name [name] - Name of the new group.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --desc [description] - New group description.");
+ console.log(" --amtonly - New group is agent-less, Intel AMT only.");
+ console.log(" --agentless - New group is agent-less only.");
+ console.log(" --features [number] - Set device group features, sum of numbers below.");
+ console.log(" 1 = Auto-Remove 2 = Hostname Sync");
+ console.log(" 4 = Record Sessions");
+ console.log(" --consent [number] - Set device group user consent, sum of numbers below.");
+ console.log(" 1 = Desktop notify user 2 = Terminal notify user ");
+ console.log(" 4 = Files notify user 8 = Desktop prompt user ");
+ console.log(" 16 = Terminal prompt user 32 = Files prompt user ");
+ console.log(" 64 = Desktop Toolbar ");
+ break;
+ }
+ case 'removedevicegroup': {
+ console.log("Remove a device group, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl RemoveDeviceGroup --id 'groupid'"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [groupid] - Device group identifier (or --group).");
+ } else {
+ console.log(" --id '[groupid]' - Device group identifier (or --group).");
+ }
+ console.log(" --group [groupname] - Device group name (or --id).");
+ break;
+ }
+ case 'editdevicegroup': {
+ console.log("Edit a device group, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl EditDeviceGroup --id 'groupid' --name \"New Name\""));
+ console.log(winRemoveSingleQuotes(" MeshCtrl EditDeviceGroup --id 'groupid' --desc \"Description\" --consent 63"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl EditDeviceGroup --id 'groupid' --invitecodes \"code1,code2\" --backgroundonly"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [groupid] - Device group identifier (or --group).");
+ } else {
+ console.log(" --id '[groupid]' - Device group identifier (or --group).");
+ }
+ console.log(" --group [groupname] - Device group name (or --id).");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --name [name] - Set new device group name.");
+ console.log(" --desc [description] - Set new device group description, blank to clear.");
+ console.log(" --flags [number] - Set device group flags, sum of the values below, 0 for none.");
+ console.log(" 1 = Auto remove device on disconnect.");
+ console.log(" 2 = Sync hostname.");
+ console.log(" --consent [number] - Set device group consent options, sum of the values below, 0 for none.");
+ console.log(" 1 = Desktop notify user.");
+ console.log(" 2 = Terminal notify user.");
+ console.log(" 4 = Files notify user.");
+ console.log(" 8 = Desktop prompt for user consent.");
+ console.log(" 16 = Terminal prompt for user consent.");
+ console.log(" 32 = Files prompt for user consent.");
+ console.log(" 64 = Desktop show connection toolbar.");
+ console.log(" --invitecodes [aa,bb] - Comma separated list of invite codes, blank to clear.");
+ console.log(" --backgroundonly - When used with invitecodes, set agent to only install in background.");
+ console.log(" --interactiveonly - When used with invitecodes, set agent to only run on demand.");
+ break;
+ }
+ case 'movetodevicegroup': {
+ console.log("Move a device to a new device group, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl MoveToDeviceGroup --devid 'deviceid' --id 'groupid'"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [groupid] - Device group identifier (or --group).");
+ } else {
+ console.log(" --id '[groupid]' - Device group identifier (or --group).");
+ }
+ console.log(" --group [groupname] - Device group name (or --id).");
+ if (process.platform == 'win32') {
+ console.log(" --devid [deviceid] - Device identifier.");
+ } else {
+ console.log(" --devid '[deviceid]' - Device identifier.");
+ }
+ break;
+ }
+ case 'addusertodevicegroup': {
+ console.log("Add a user to a device group, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl AddUserToDeviceGroup --id 'groupid' --userid userid --fullrights"));
+ console.log(" MeshCtrl AddUserToDeviceGroup --group groupname --userid userid --editgroup --manageusers");
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [groupid] - Device group identifier (or --group).");
+ } else {
+ console.log(" --id '[groupid]' - Device group identifier (or --group).");
+ }
+ console.log(" --group [groupname] - Device group name (or --id).");
+ console.log(" --userid [userid] - The user identifier.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --fullrights - Allow full rights over this device group.");
+ console.log(" --editgroup - Allow the user to edit group information.");
+ console.log(" --manageusers - Allow the user to add/remove users.");
+ console.log(" --managedevices - Allow the user to edit device information.");
+ console.log(" --remotecontrol - Allow device remote control operations.");
+ console.log(" --agentconsole - Allow agent console operations.");
+ console.log(" --serverfiles - Allow access to group server files.");
+ console.log(" --wakedevices - Allow device wake operation.");
+ console.log(" --notes - Allow editing of device notes.");
+ console.log(" --desktopviewonly - Restrict user to view-only remote desktop.");
+ console.log(" --limiteddesktop - Limit remote desktop keys.");
+ console.log(" --noterminal - Hide the terminal tab from this user.");
+ console.log(" --nofiles - Hide the files tab from this user.");
+ console.log(" --noamt - Hide the Intel AMT tab from this user.");
+ console.log(" --limitedevents - User can only see his own events.");
+ console.log(" --chatnotify - Allow chat and notification options.");
+ console.log(" --uninstall - Allow remote uninstall of the agent.");
+ if (args.limiteddesktop) { meshrights |= 4096; }
+ if (args.limitedevents) { meshrights |= 8192; }
+ if (args.chatnotify) { meshrights |= 16384; }
+ if (args.uninstall) { meshrights |= 32768; }
+
+ break;
+ }
+ case 'removeuserfromdevicegroup': {
+ console.log("Remove a user from a device group, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl RemoveuserFromDeviceGroup --id 'groupid' --userid userid"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [groupid] - Device group identifier (or --group).");
+ } else {
+ console.log(" --id '[groupid]' - Device group identifier (or --group).");
+ }
+ console.log(" --group [groupname] - Device group name (or --id).");
+ console.log(" --userid [userid] - The user identifier.");
+ break;
+ }
+ case 'addusertodevice': {
+ console.log("Add a user to a device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl AddUserToDevice --id 'deviceid' --userid userid --fullrights"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl AddUserToDevice --id 'deviceid' --userid userid --remotecontrol"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log(" --userid [userid] - The user identifier.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --fullrights - Allow full rights over this device.");
+ console.log(" --remotecontrol - Allow device remote control operations.");
+ console.log(" --agentconsole - Allow agent console operations.");
+ console.log(" --serverfiles - Allow access to group server files.");
+ console.log(" --wakedevices - Allow device wake operation.");
+ console.log(" --notes - Allow editing of device notes.");
+ console.log(" --desktopviewonly - Restrict user to view-only remote desktop.");
+ console.log(" --limiteddesktop - Limit remote desktop keys.");
+ console.log(" --noterminal - Hide the terminal tab from this user.");
+ console.log(" --nofiles - Hide the files tab from this user.");
+ console.log(" --noamt - Hide the Intel AMT tab from this user.");
+ console.log(" --limitedevents - User can only see his own events.");
+ console.log(" --chatnotify - Allow chat and notification options.");
+ console.log(" --uninstall - Allow remote uninstall of the agent.");
+ break;
+ }
+ case 'removeuserfromdevice': {
+ console.log("Remove a user from a device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl RemoveuserFromDeviceGroup --id 'deviceid' --userid userid"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log(" --userid [userid] - The user identifier.");
+ break;
+ }
+ case 'broadcast': {
+ console.log("Display a message to one or all logged in users, Example usages:\r\n");
+ console.log(" MeshCtrl Broadcast --msg \"This is a test\"");
+ console.log("\r\nRequired arguments:\r\n");
+ console.log(" --msg [message] - Message to display.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --user [userid] - Send the message to the specified user.");
+ break;
+ }
+ case 'deviceinfo': {
+ console.log("Display information about a device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceInfo --id 'deviceid'"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceInfo --id 'deviceid' --json"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --raw - Output raw data in JSON format.");
+ console.log(" --json - Give results in JSON format.");
+ break;
+ }
+ case 'removedevice': {
+ console.log("Delete a device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl RemoveDevice --id 'deviceid'"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ break;
+ }
+ case 'addlocaldevice': {
+ console.log("Add a Local Device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl AddLocalDevice --id 'meshid' --devicename 'devicename' --hostname 'hostname'"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl AddLocalDevice --id 'meshid' --devicename 'devicename' --hostname 'hostname' --type 6"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [meshid] - The mesh identifier.");
+ console.log(" --devicename [devicename] - The device name.");
+ console.log(" --hostname [hostname] - The devices hostname or ip address.");
+ } else {
+ console.log(" --id '[meshid]' - The mesh identifier.");
+ console.log(" --devicename '[devicename]' - The device name.");
+ console.log(" --hostname '[hostname]' - The devices hostname or ip address.");
+ }
+
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --type [TypeNumber] - With the following choices:");
+ console.log(" type 4 - Default, Windows (RDP)");
+ console.log(" type 6 - Linux (SSH/SCP/VNC)");
+ console.log(" type 29 - macOS (SSH/SCP/VNC)");
+ break;
+ }
+ case 'addamtdevice': {
+ console.log("Add an Intel AMT Device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl AddAmtDevice --id 'meshid' --devicename 'devicename' --hostname 'hostname --user 'admin' --pass 'admin'"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl AddAmtDevice --id 'meshid' --devicename 'devicename' --hostname 'hostname --user 'admin' --pass 'admin' --notls"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [meshid] - The mesh identifier.");
+ console.log(" --devicename [devicename] - The device name.");
+ console.log(" --hostname [hostname] - The devices hostname or ip address.");
+ console.log(" --user [user] - The devices AMT username.");
+ console.log(" --pass [pass] - The devices AMT password.");
+ console.log("")
+ } else {
+ console.log(" --id '[meshid]' - The mesh identifier.");
+ console.log(" --devicename '[devicename]' - The device name.");
+ console.log(" --hostname '[hostname]' - The devices hostname or ip address.");
+ console.log(" --user '[user]' - The devices AMT username.");
+ console.log(" --pass '[pass]' - The devices AMT password.");
+ }
+ console.log("\r\nOptional arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --notls - Use No TLS Security.");
+ } else {
+ console.log(" --notls - Use No TLS Security.");
+ }
+ break;
+ }
+ case 'editdevice': {
+ console.log("Change information about a device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl EditDevice --id 'deviceid' --name 'device1'"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log("\r\nOptional arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --name [name] - Change device name.");
+ console.log(" --desc [description] - Change device description.");
+ console.log(" --tags [tag1,tags2] - Change device tags.");
+ } else {
+ console.log(" --name '[name]' - Change device name.");
+ console.log(" --desc '[description]' - Change device description.");
+ console.log(" --tags '[tag1,tags2]' - Change device tags.");
+ }
+ console.log(" --icon [number] - Change the device icon (1 to 8).");
+ console.log(" --consent [flags] - Sum of the following numbers:");
+ console.log(" 1 = Desktop notify 2 = Terminal notify");
+ console.log(" 4 = Files notify 8 = Desktop prompt");
+ console.log(" 16 = Terminal prompt 32 = Files prompt");
+ console.log(" 64 = Desktop privacy bar");
+ break;
+ }
+ case 'runcommand': {
+ console.log("Run a shell command on a remote device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl RunCommand --id 'deviceid' --run \"command\""));
+ console.log(winRemoveSingleQuotes(" MeshCtrl RunCommand --id 'deviceid' --run \"command\" --powershell"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl RunCommand --id 'deviceid' --run \"command\" --reply"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log(" --run \"[command]\" - Shell command to execute on the remote device.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --powershell - Run in Windows PowerShell.");
+ console.log(" --runasuser - Attempt to run the command as logged in user.");
+ console.log(" --runasuseronly - Only run the command as the logged in user.");
+ console.log(" --reply - Return with the output from running the command.");
+ break;
+ }
+ case 'shell': {
+ console.log("Access a command shell on a remote device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl Shell --id 'deviceid'"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl Shell --id 'deviceid' --powershell"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --powershell - Run a Windows PowerShell.");
+ break;
+ }
+ case 'devicepower': {
+ console.log("Perform power operations on remote devices, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl DevicePower --wake --id 'deviceid'"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl DevicePower --sleep --id 'deviceid'"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl DevicePower --reset --id 'deviceid'"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl DevicePower --off --id 'deviceid1,deviceid2'"));
+ console.log("\r\nNote that some power operations may take up to a minute to execute.\r\n");
+ console.log("Required arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid1,deviceid2] - Device identifiers.");
+ } else {
+ console.log(" --id '[deviceid1,deviceid2]' - Device identifiers.");
+ }
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --wake - Attempt to wake up the remote device.");
+ console.log(" --reset - Attempt to remote the remote device.");
+ console.log(" --sleep - Attempt to place the remote device in low power mode.");
+ console.log(" --off - Attempt to power off the remote device.");
+ console.log(" --amtoff - Attempt to power off the remote device using Intel AMT.");
+ console.log(" --amton - Attempt to power on the remote device using Intel AMT.");
+ console.log(" --amtreset - Attempt to reset the remote device using Intel AMT.");
+ break;
+ }
+ case 'devicesharing': {
+ var tzoffset = (new Date()).getTimezoneOffset() * 60000; // Offset in milliseconds
+ var localISOTime = (new Date(Date.now() - tzoffset)).toISOString().slice(0, -5);
+ console.log("List sharing links for a specified device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid'"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --remove abcdef"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --add Guest --start " + localISOTime + " --duration 30"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --add Guest --start " + localISOTime + " --duration 30 --daily"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --add Guest --type desktop,terminal --consent prompt"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceSharing --id 'deviceid' --add Guest --type http --port 80"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --remove [shareid] - Remove a device sharing link.");
+ console.log(" --add [guestname] - Add a device sharing link.");
+ console.log(" --type [desktop,terminal,files,http,https] - Type of sharing to add, can be combined. default is desktop.");
+ console.log(" --viewonly - Make desktop sharing view only.");
+ console.log(" --consent [notify,prompt,none] - Consent flags, default is notify.");
+ console.log(" --start [yyyy-mm-ddThh:mm:ss] - Start time, default is now.");
+ console.log(" --end [yyyy-mm-ddThh:mm:ss] - End time.");
+ console.log(" --duration [minutes] - Duration of the share, default is 60 minutes.");
+ console.log(" --daily - Add recurring daily device share.");
+ console.log(" --weekly - Add recurring weekly device share.");
+ console.log(" --port [portnumber] - Set alternative port for http or https, default is 80 for http and 443 for https.");
+ break;
+ }
+ case 'agentdownload': {
+ console.log("Download an agent of a specific type for a given device group, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl AgentDownload --id 'groupid' --type 3"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl AgentDownload --id 'groupid' --type 3 --installflags 1"));
+ console.log("\r\nRequired arguments:\r\n");
+ console.log(" --type [ArchitectureNumber] - Agent architecture number.");
+ if (process.platform == 'win32') {
+ console.log(" --id [groupid] - The device group identifier.");
+ } else {
+ console.log(" --id '[groupid]' - The device group identifier.");
+ }
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --installflags [InstallFlagsNumber] - With the following choices:");
+ console.log(" installflags 0 - Default, Interactive & Background, offers connect button & install/uninstall");
+ console.log(" installflags 1 - Interactive only, offers only connect button, not install/uninstall");
+ console.log(" installflags 2 - Background only, offers only install/uninstall, not connect");
+ break;
+ }
+ case 'upload': {
+ console.log("Upload a local file to a remote device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl Upload --id 'deviceid' --file sample.txt --target c:\\"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl Upload --id 'deviceid' --file sample.txt --target /tmp"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log(" --file [localfile] - The local file to upload.");
+ console.log(" --target [remotepath] - The remote path to upload the file to.");
+ break;
+ }
+ case 'download': {
+ console.log("Download a file from a remote device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl Download --id 'deviceid' --file C:\\sample.txt --target c:\\temp"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl Download --id 'deviceid' --file /tmp/sample.txt --target /tmp"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log(" --file [remotefile] - The remote file to download.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --target [localpath] - The local path to download the file to.");
+ break;
+ }
+ case 'webrelay': {
+ console.log("Generate a webrelay URL to access a HTTP/HTTPS service on a remote device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl WebRelay --id 'deviceid' --type http --port 80"));
+ console.log(winRemoveSingleQuotes(" MeshCtrl WebRelay --id 'deviceid' --type https --port 443"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log(" --type [http,https] - Type of relay from remote device, http or https.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --port [portnumber] - Set alternative port for http or https, default is 80 for http and 443 for https.");
+ break;
+ }
+ case 'deviceopenurl': {
+ console.log("Open a web page on a remote device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceOpenUrl --id 'deviceid' --openurl http://meshcentral.com"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log(" --openurl [url] - Link to the web page.");
+ break;
+ }
+ case 'devicemessage': {
+ console.log("Display a message on the remote device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceMessage --id 'deviceid' --msg \"message\""));
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceMessage --id 'deviceid' --msg \"message\" --title \"title\""));
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceMessage --id 'deviceid' --msg \"message\" --title \"title\" --timeout 120000"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log(" --msg [message] - The message to display.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --title [title] - Messagebox title, default is \"MeshCentral\".");
+ console.log(" --timeout [miliseconds] - After timeout messagebox vanishes, 0 keeps messagebox open until closed manually, default is 120000 (2 Minutes).");
+ break;
+ }
+ case 'devicetoast': {
+ console.log("Display a toast message on the remote device, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceToast --id 'deviceid' --msg \"message\""));
+ console.log(winRemoveSingleQuotes(" MeshCtrl DeviceToast --id 'deviceid' --msg \"message\" --title \"title\""));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [deviceid] - The device identifier.");
+ } else {
+ console.log(" --id '[deviceid]' - The device identifier.");
+ }
+ console.log(" --msg [message] - The message to display.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --title [title] - Toast title, default is \"MeshCentral\".");
+ break;
+ }
+ case 'groupmessage': {
+ console.log("Open a message box on remote devices in a specific device group, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl GroupMessage --id 'devicegroupid' --msg \"message\""));
+ console.log(winRemoveSingleQuotes(" MeshCtrl GroupMessage --id 'devicegroupid' --msg \"message\" --title \"title\""));
+ console.log(winRemoveSingleQuotes(" MeshCtrl GroupMessage --id 'devicegroupid' --msg \"message\" --title \"title\" --timeout 120000"));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [devicegroupid] - The device identifier.");
+ } else {
+ console.log(" --id '[devicegroupid]' - The device identifier.");
+ }
+ console.log(" --msg [message] - The message to display.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --title [title] - Messagebox title, default is \"MeshCentral\".");
+ console.log(" --timeout [miliseconds] - After timeout messagebox vanishes, 0 keeps messagebox open until closed manually, default is 120000 (2 Minutes).");
+ break;
+ }
+ case 'grouptoast': {
+ console.log("Display a toast notification on remote devices in a specific device group, Example usages:\r\n");
+ console.log(winRemoveSingleQuotes(" MeshCtrl GroupToast --id 'devicegroupid' --msg \"message\""));
+ console.log(winRemoveSingleQuotes(" MeshCtrl GroupToast --id 'devicegroupid' --msg \"message\" --title \"title\""));
+ console.log("\r\nRequired arguments:\r\n");
+ if (process.platform == 'win32') {
+ console.log(" --id [devicegroupid] - The device identifier.");
+ } else {
+ console.log(" --id '[devicegroupid]' - The device identifier.");
+ }
+ console.log(" --msg [message] - The message to display.");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --title [title] - Toast title, default is \"MeshCentral\".");
+ break;
+ }
+ case 'report': {
+ console.log("Generate a CSV report, Example usages:\r\n");
+ console.log(" MeshCtrl Report --type sessions --devicegroup mesh//...");
+ console.log(" MeshCtrl Report --type traffic --json");
+ console.log(" MeshCtrl Report --type logins --groupby day");
+ console.log(" MeshCtrl Report --type db");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --start [yyyy-mm-ddThh:mm:ss] - Filter the results starting at that date. Defaults to last 24h and last week when used with --groupby day. Usable with sessions, traffic and logins");
+ console.log(" --end [yyyy-mm-ddThh:mm:ss] - Filter the results ending at that date. Defaults to now. Usable with sessions, traffic and logins");
+ console.log(" --groupby [name] - How to group results. Options: user, day, device. Defaults to user. User and day usable in sessions and logins, device usable in sessions.");
+ console.log(" --devicegroup [devicegroupid] - Filter the results by device group. Usable in sessions");
+ console.log(" --showtraffic - Add traffic data in sessions report");
+ break;
+ }
+ default: {
+ console.log("Get help on an action. Type:\r\n\r\n help [action]\r\n\r\nPossible actions are: " + possibleCommands.join(', ') + '.');
+ }
+ }
+ }
+ break;
+ }
+ }
+
+ if (ok) {
+ if(args.loginpass===true){
+ const readline = require('readline');
+ const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ terminal: false
+ });
+ process.stdout.write('Enter your password: ');
+ const stdin = process.openStdin();
+ stdin.setRawMode(true); // Set raw mode to prevent echoing of characters
+ stdin.resume();
+ args.loginpass = '';
+ process.stdin.on('data', (char) => {
+ char = char + '';
+ switch (char) {
+ case '\n':
+ case '\r':
+ case '\u0004': // They've finished entering their password
+ stdin.setRawMode(false);
+ stdin.pause();
+ process.stdout.clearLine(); process.stdout.cursorTo(0);
+ rl.close();
+ serverConnect();
+ break;
+ case '\u0003': // Ctrl+C
+ process.stdout.write('\n');
+ process.exit();
+ break;
+ default: // Mask the password with "*"
+ args.loginpass += char;
+ process.stdout.clearLine(); process.stdout.cursorTo(0);
+ process.stdout.write('Enter your password: ' + '*'.repeat(args.loginpass.length));
+ break;
+ }
+ });
+ }else{
+ serverConnect();
+ }
+ }
+}
+
+function displayConfigHelp() {
+ console.log("Perform operations on the config.json file. Example usage:\r\n");
+ console.log(" MeshCtrl config --show");
+ console.log("\r\nOptional arguments:\r\n");
+ console.log(" --show - Display the config.json file.");
+ console.log(" --listdomains - Display non-default domains.");
+ console.log(" --adddomain [domain] - Add a domain.");
+ console.log(" --removedomain [domain] - Remove a domain.");
+ console.log(" --settodomain [domain] - Set values to the domain.");
+ console.log(" --removefromdomain [domain] - Remove values from the domain.");
+ console.log("\r\nWith adddomain, removedomain, settodomain and removefromdomain you can add the key and value pair. For example:\r\n");
+ console.log(" --adddomain \"MyDomain\" --title \"My Server Name\" --newAccounts false");
+ console.log(" --settodomain \"MyDomain\" --title \"My Server Name\"");
+ console.log(" --removefromdomain \"MyDomain\" --title");
+}
+
+function performConfigOperations(args) {
+ var domainValues = ['title', 'title2', 'titlepicture', 'trustedcert', 'welcomepicture', 'welcometext', 'userquota', 'meshquota', 'newaccounts', 'usernameisemail', 'newaccountemaildomains', 'newaccountspass', 'newaccountsrights', 'geolocation', 'lockagentdownload', 'userconsentflags', 'Usersessionidletimeout', 'auth', 'ldapoptions', 'ldapusername', 'ldapuserbinarykey', 'ldapuseremail', 'footer', 'certurl', 'loginKey', 'userallowedip', 'agentallowedip', 'agentnoproxy', 'agentconfig', 'orphanagentuser', 'httpheaders', 'yubikey', 'passwordrequirements', 'limits', 'amtacmactivation', 'redirects', 'sessionrecording', 'hide', 'preventduplicatedevices'];
+ var domainObjectValues = ['ldapoptions', 'httpheaders', 'yubikey', 'passwordrequirements', 'limits', 'amtacmactivation', 'redirects', 'sessionrecording'];
+ var domainArrayValues = ['newaccountemaildomains', 'newaccountsrights', 'loginkey', 'agentconfig'];
+ var configChange = false;
+ var fs = require('fs');
+ var path = require('path');
+ var configFile = 'config.json';
+ var didSomething = 0;
+ if (fs.existsSync(configFile) == false) { configFile = path.join('meshcentral-data', 'config.json'); }
+ if (fs.existsSync(configFile) == false) { configFile = path.join(__dirname, 'config.json'); }
+ if (fs.existsSync(configFile) == false) { configFile = path.join(__dirname, 'meshcentral-data', 'config.json'); }
+ if (fs.existsSync(configFile) == false) { configFile = path.join(__dirname, '..', 'meshcentral-data', 'config.json'); }
+ if (fs.existsSync(configFile) == false) { configFile = path.join(__dirname, '..', '..', 'meshcentral-data', 'config.json'); }
+ if (fs.existsSync(configFile) == false) { console.log("Unable to find config.json."); return; }
+ var config = null;
+ try { config = fs.readFileSync(configFile).toString('utf8'); } catch (ex) { console.log("Error: Unable to read config.json"); return; }
+ try { config = JSON.parse(fs.readFileSync(configFile)); } catch (e) { console.log('ERROR: Unable to parse ' + configFile + '.'); return null; }
+ if (args.adddomain != null) {
+ didSomething++;
+ if (config.domains == null) { config.domains = {}; }
+ if (config.domains[args.adddomain] != null) { console.log("Error: Domain \"" + args.adddomain + "\" already exists"); }
+ else {
+ configChange = true;
+ config.domains[args.adddomain] = {};
+ for (var i in args) {
+ if (domainValues.indexOf(i.toLowerCase()) >= 0) {
+ if (args[i] == 'true') { args[i] = true; } else if (args[i] == 'false') { args[i] = false; } else if (parseInt(args[i]) == args[i]) { args[i] = parseInt(args[i]); }
+ config.domains[args.adddomain][i] = args[i];
+ configChange = true;
+ }
+ }
+ }
+ }
+ if (args.removedomain != null) {
+ didSomething++;
+ if (config.domains == null) { config.domains = {}; }
+ if (config.domains[args.removedomain] == null) { console.log("Error: Domain \"" + args.removedomain + "\" does not exist"); }
+ else { delete config.domains[args.removedomain]; configChange = true; }
+ }
+ if (args.settodomain != null) {
+ didSomething++;
+ if (config.domains == null) { config.domains = {}; }
+ if (args.settodomain == true) { args.settodomain = ''; }
+ if (config.domains[args.settodomain] == null) { console.log("Error: Domain \"" + args.settodomain + "\" does not exist"); }
+ else {
+ for (var i in args) {
+ if ((i == '_') || (i == 'settodomain')) continue;
+ if (domainValues.indexOf(i.toLowerCase()) >= 0) {
+ var isObj = (domainObjectValues.indexOf(i.toLowerCase()) >= 0);
+ var isArr = (domainArrayValues.indexOf(i.toLowerCase()) >= 0);
+ if ((isObj == false) && (isArr == false)) {
+ // Simple value set
+ if (args[i] == '') { delete config.domains[args.settodomain][i]; configChange = true; } else {
+ if (args[i] == 'true') { args[i] = true; } else if (args[i] == 'false') { args[i] = false; } else if (parseInt(args[i]) == args[i]) { args[i] = parseInt(args[i]); }
+ config.domains[args.settodomain][i] = args[i];
+ configChange = true;
+ }
+ } else if (isObj || isArr) {
+ // Set an object/array value
+ if (args[i] == '') { delete config.domains[args.settodomain][i]; configChange = true; } else {
+ var x = null;
+ try { x = JSON.parse(args[i]); } catch (ex) { }
+ if ((x == null) || (typeof x != 'object')) { console.log("Unable to parse JSON for " + i + "."); } else {
+ if (isArr && Array.isArray(x) == false) {
+ console.log("Value " + i + " must be an array.");
+ } else if (!isArr && Array.isArray(x) == true) {
+ console.log("Value " + i + " must be an object.");
+ } else {
+ config.domains[args.settodomain][i] = x;
+ configChange = true;
+ }
+ }
+ }
+ }
+ } else {
+ console.log('Invalid configuration value: ' + i);
+ }
+ }
+ }
+ }
+ if (args.removefromdomain != null) {
+ didSomething++;
+ if (config.domains == null) { config.domains = {}; }
+ if (config.domains[args.removefromdomain] == null) { console.log("Error: Domain \"" + args.removefromdomain + "\" does not exist"); }
+ else { for (var i in args) { if (domainValues.indexOf(i.toLowerCase()) >= 0) { delete config.domains[args.removefromdomain][i]; configChange = true; } } }
+ }
+ if (configChange) {
+ try { fs.writeFileSync(configFile, JSON.stringify(config, null, 2)); } catch (ex) { console.log("Error: Unable to read config.json"); return; }
+ }
+ if (args.show == 1) {
+ console.log(JSON.stringify(config, null, 2)); return;
+ } else if (args.listdomains == 1) {
+ if (config.domains == null) {
+ console.log('No domains found.'); return;
+ } else {
+ // Show the list of active domains, skip the default one.
+ for (var i in config.domains) { if ((i != '') && (i[0] != '_')) { console.log(i); } } return;
+ }
+ } else {
+ if (didSomething == 0) {
+ displayConfigHelp();
+ } else {
+ console.log("Done.");
+ }
+ }
+}
+
+function onVerifyServer(clientName, certs) { return null; }
+
+function serverConnect() {
+ const WebSocket = require('ws');
+
+ var url = 'wss://localhost/control.ashx';
+ if (args.url) {
+ url = args.url;
+ if (url.length < 5) { console.log("Invalid url."); process.exit(); return; }
+ if ((url.startsWith('wss://') == false) && (url.startsWith('ws://') == false)) { console.log("Invalid url."); process.exit(); return; }
+ var i = url.indexOf('?key='), loginKey = null;
+ if (i >= 0) { loginKey = url.substring(i + 5); url = url.substring(0, i); }
+ if (url.endsWith('/') == false) { url += '/'; }
+ url += 'control.ashx';
+ if (loginKey != null) { url += '?key=' + loginKey; }
+ }
+
+ // TODO: checkServerIdentity does not work???
+ var options = { rejectUnauthorized: false, checkServerIdentity: onVerifyServer }
+
+ // Setup the HTTP proxy if needed
+ if (args.proxy != null) {
+ const HttpsProxyAgent = require('https-proxy-agent');
+ options.agent = new HttpsProxyAgent(require('url').parse(args.proxy));
+ }
+
+ // Password authentication
+ if (args.loginpass != null) {
+ var username = 'admin';
+ if (args.loginuser != null) { username = args.loginuser; }
+ var token = '';
+ if (args.token != null) { token = ',' + Buffer.from('' + args.token).toString('base64'); }
+ options.headers = { 'x-meshauth': Buffer.from('' + username).toString('base64') + ',' + Buffer.from('' + args.loginpass).toString('base64') + token }
+ }
+
+ // Cookie authentication
+ var ckey = null, loginCookie = null;
+ if (args.loginkey != null) {
+ // User key passed in as argument hex
+ if (args.loginkey.length != 160) { loginCookie = args.loginkey; }
+ ckey = Buffer.from(args.loginkey, 'hex');
+ if (ckey.length != 80) { ckey = null; loginCookie = args.loginkey; }
+ } else if (args.loginkeyfile != null) {
+ // Load key from hex file
+ var fs = require('fs');
+ try {
+ var keydata = fs.readFileSync(args.loginkeyfile, 'utf8').split(' ').join('').split('\r').join('').split('\n').join('');
+ ckey = Buffer.from(keydata, 'hex');
+ if (ckey.length != 80) { ckey = null; loginCookie = args.loginkey; }
+ } catch (ex) { console.log(ex.message); process.exit(); return; }
+ }
+
+ settings.xxurl = url;
+ if (ckey != null) {
+ var domainid = '', username = 'admin';
+ if (args.logindomain != null) { domainid = args.logindomain; }
+ if (args.loginuser != null) { username = args.loginuser; }
+ url += (url.indexOf('?key=') >= 0 ? '&auth=' : '?auth=') + encodeCookie({ userid: 'user/' + domainid + '/' + username, domainid: domainid }, ckey);
+ } else {
+ if (args.logindomain != null) { console.log("--logindomain can only be used along with --loginkey."); process.exit(); return; }
+ if (loginCookie != null) { url += (url.indexOf('?key=') >= 0 ? '&auth=' : '?auth=') + loginCookie; }
+ }
+
+ const ws = new WebSocket(url, options);
+ //console.log('Connecting to ' + url);
+
+ ws.on('open', function open() {
+ //console.log('Connected.');
+ switch (settings.cmd) {
+ case 'serverinfo': { break; }
+ case 'userinfo': { break; }
+ case 'listusers': { ws.send(JSON.stringify({ action: 'users', responseid: 'meshctrl' })); break; }
+ case 'listusersessions': { ws.send(JSON.stringify({ action: 'wssessioncount', responseid: 'meshctrl' })); break; }
+ case 'removeallusersfromusergroup':
+ case 'listusergroups': { ws.send(JSON.stringify({ action: 'usergroups', responseid: 'meshctrl' })); break; }
+ case 'listdevicegroups': { ws.send(JSON.stringify({ action: 'meshes', responseid: 'meshctrl' })); break; }
+ case 'listusersofdevicegroup': { ws.send(JSON.stringify({ action: 'meshes', responseid: 'meshctrl' })); break; }
+ case 'listdevices': {
+ if (args.details) {
+ // Get list of devices with lots of details
+ ws.send(JSON.stringify({ action: 'getDeviceDetails', type: (args.csv) ? 'csv' : 'json' }));
+ } else if (args.group) {
+ ws.send(JSON.stringify({ action: 'nodes', meshname: args.group, responseid: 'meshctrl' }));
+ } else if (args.id) {
+ ws.send(JSON.stringify({ action: 'nodes', meshid: args.id, responseid: 'meshctrl' }));
+ } else {
+ ws.send(JSON.stringify({ action: 'meshes' }));
+ ws.send(JSON.stringify({ action: 'nodes', responseid: 'meshctrl' }));
+ }
+ break;
+ }
+ case 'listevents': {
+ limit = null;
+ if (args.limit) { limit = parseInt(args.limit); }
+ if ((typeof limit != 'number') || (limit < 1)) { limit = null; }
+
+ var cmd = null;
+ if (args.userid) {
+ cmd = { action: 'events', user: args.userid, responseid: 'meshctrl' };
+ } else if (args.id) {
+ cmd = { action: 'events', nodeid: args.id, responseid: 'meshctrl' };
+ } else {
+ cmd = { action: 'events', responseid: 'meshctrl' };
+ }
+ if (typeof limit == 'number') { cmd.limit = limit; }
+ ws.send(JSON.stringify(cmd));
+ break;
+ }
+ case 'logintokens': {
+ if (args.add) {
+ var cmd = { action: 'createLoginToken', name: args.add, expire: 0, responseid: 'meshctrl' };
+ if (args.expire) { cmd.expire = parseInt(args.expire); }
+ ws.send(JSON.stringify(cmd));
+ } else {
+ var cmd = { action: 'loginTokens', responseid: 'meshctrl' };
+ if (args.remove) { cmd.remove = [args.remove]; }
+ ws.send(JSON.stringify(cmd));
+ }
+ break;
+ }
+ case 'adduser': {
+ var siteadmin = getSiteAdminRights(args);
+ if (args.randompass) { args.pass = getRandomAmtPassword(); }
+ var op = { action: 'adduser', username: args.user, pass: args.pass, responseid: 'meshctrl' };
+ if (args.email) { op.email = args.email; if (args.emailverified) { op.emailVerified = true; } }
+ if (args.resetpass) { op.resetNextLogin = true; }
+ if (siteadmin != -1) { op.siteadmin = siteadmin; }
+ if (args.domain) { op.domain = args.domain; }
+ if (args.phone === true) { op.phone = ''; }
+ if (typeof args.phone == 'string') { op.phone = args.phone; }
+ if (typeof args.realname == 'string') { op.realname = args.realname; }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'edituser': {
+ var userid = args.userid;
+ if ((args.domain != null) && (userid.indexOf('/') < 0)) { userid = 'user/' + args.domain + '/' + userid; }
+ var siteadmin = getSiteAdminRights(args);
+ var op = { action: 'edituser', userid: userid, responseid: 'meshctrl' };
+ if (args.email) { op.email = args.email; if (args.emailverified) { op.emailVerified = true; } }
+ if (args.resetpass) { op.resetNextLogin = true; }
+ if (siteadmin != -1) { op.siteadmin = siteadmin; }
+ if (args.domain) { op.domain = args.domain; }
+ if (args.phone === true) { op.phone = ''; }
+ if (typeof args.phone == 'string') { op.phone = args.phone; }
+ if (typeof args.realname == 'string') { op.realname = args.realname; }
+ if (args.realname === true) { op.realname = ''; }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'removeuser': {
+ var userid = args.userid;
+ if ((args.domain != null) && (userid.indexOf('/') < 0)) { userid = 'user/' + args.domain + '/' + userid; }
+ ws.send(JSON.stringify({ action: 'deleteuser', userid: userid, responseid: 'meshctrl' }));
+ break;
+ }
+ case 'addusergroup': {
+ var op = { action: 'createusergroup', name: args.name, desc: args.desc, responseid: 'meshctrl' };
+ if (args.domain) { op.domain = args.domain; }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'removeusergroup': {
+ var ugrpid = args.groupid;
+ if ((args.domain != null) && (userid.indexOf('/') < 0)) { ugrpid = 'ugrp/' + args.domain + '/' + ugrpid; }
+ ws.send(JSON.stringify({ action: 'deleteusergroup', ugrpid: ugrpid, responseid: 'meshctrl' }));
+ break;
+ }
+ case 'addtousergroup': {
+ var ugrpid = args.groupid;
+ if ((args.domain != null) && (userid.indexOf('/') < 0)) { ugrpid = 'ugrp/' + args.domain + '/' + ugrpid; }
+
+ // Add a user to a user group
+ if (args.userid != null) {
+ var userid = args.userid;
+ if ((args.domain != null) && (userid.indexOf('/') < 0)) { userid = 'user/' + args.domain + '/' + userid; }
+ ws.send(JSON.stringify({ action: 'addusertousergroup', ugrpid: ugrpid, usernames: [userid.split('/')[2]], responseid: 'meshctrl' }));
+ break;
+ }
+
+ if ((args.id != null) && (args.id.startsWith('user/'))) {
+ ws.send(JSON.stringify({ action: 'addusertousergroup', ugrpid: ugrpid, usernames: [args.id.split('/')[2]], responseid: 'meshctrl' }));
+ break;
+ }
+
+ var rights = 0;
+ if (args.rights != null) { rights = parseInt(args.rights); }
+
+ // Add a device group to a user group
+ if (args.meshid != null) {
+ var meshid = args.meshid;
+ if ((args.domain != null) && (userid.indexOf('/') < 0)) { meshid = 'mesh/' + args.domain + '/' + meshid; }
+ ws.send(JSON.stringify({ action: 'addmeshuser', meshid: meshid, userid: ugrpid, meshadmin: rights, responseid: 'meshctrl' }));
+ break;
+ }
+
+ if ((args.id != null) && (args.id.startsWith('mesh/'))) {
+ ws.send(JSON.stringify({ action: 'addmeshuser', meshid: args.id, userid: ugrpid, meshadmin: rights, responseid: 'meshctrl' }));
+ break;
+ }
+
+ // Add a device to a user group
+ if (args.nodeid != null) {
+ var nodeid = args.nodeid;
+ if ((args.domain != null) && (userid.indexOf('/') < 0)) { nodeid = 'node/' + args.domain + '/' + nodeid; }
+ ws.send(JSON.stringify({ action: 'adddeviceuser', nodeid: nodeid, userids: [ugrpid], rights: rights, responseid: 'meshctrl' }));
+ break;
+ }
+
+ if ((args.id != null) && (args.id.startsWith('node/'))) {
+ ws.send(JSON.stringify({ action: 'adddeviceuser', nodeid: args.id, userids: [ugrpid], rights: rights, responseid: 'meshctrl' }));
+ break;
+ }
+
+ break;
+ }
+ case 'removefromusergroup': {
+ var ugrpid = args.groupid;
+ if ((args.domain != null) && (userid.indexOf('/') < 0)) { ugrpid = 'ugrp/' + args.domain + '/' + ugrpid; }
+
+ // Remove a user from a user group
+ if (args.userid != null) {
+ var userid = args.userid;
+ if ((args.domain != null) && (userid.indexOf('/') < 0)) { userid = 'user/' + args.domain + '/' + userid; }
+ ws.send(JSON.stringify({ action: 'removeuserfromusergroup', ugrpid: ugrpid, userid: userid, responseid: 'meshctrl' }));
+ break;
+ }
+
+ if ((args.id != null) && (args.id.startsWith('user/'))) {
+ ws.send(JSON.stringify({ action: 'removeuserfromusergroup', ugrpid: ugrpid, userid: args.id, responseid: 'meshctrl' }));
+ break;
+ }
+
+ // Remove a device group from a user group
+ if (args.meshid != null) {
+ var meshid = args.meshid;
+ if ((args.domain != null) && (userid.indexOf('/') < 0)) { meshid = 'mesh/' + args.domain + '/' + meshid; }
+ ws.send(JSON.stringify({ action: 'removemeshuser', meshid: meshid, userid: ugrpid, responseid: 'meshctrl' }));
+ break;
+ }
+
+ if ((args.id != null) && (args.id.startsWith('mesh/'))) {
+ ws.send(JSON.stringify({ action: 'removemeshuser', meshid: args.id, userid: ugrpid, responseid: 'meshctrl' }));
+ break;
+ }
+
+ // Remove a device from a user group
+ if (args.nodeid != null) {
+ var nodeid = args.nodeid;
+ if ((args.domain != null) && (userid.indexOf('/') < 0)) { nodeid = 'node/' + args.domain + '/' + nodeid; }
+ ws.send(JSON.stringify({ action: 'adddeviceuser', nodeid: nodeid, userids: [ugrpid], rights: 0, responseid: 'meshctrl', remove: true }));
+ break;
+ }
+
+ if ((args.id != null) && (args.id.startsWith('node/'))) {
+ ws.send(JSON.stringify({ action: 'adddeviceuser', nodeid: args.id, userids: [ugrpid], rights: 0, responseid: 'meshctrl', remove: true }));
+ break;
+ }
+
+ break;
+ }
+ case 'adddevicegroup': {
+ var op = { action: 'createmesh', meshname: args.name, meshtype: 2, responseid: 'meshctrl' };
+ if (args.desc) { op.desc = args.desc; }
+ if (args.amtonly) { op.meshtype = 1; }
+ if (args.agentless) { op.meshtype = 3; }
+ if (args.features) { op.flags = parseInt(args.features); }
+ if (args.consent) { op.consent = parseInt(args.consent); }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'removedevicegroup': {
+ var op = { action: 'deletemesh', responseid: 'meshctrl' };
+ if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'addamtdevice': {
+ var op = { action: 'addamtdevice', amttls: 1, responseid: 'meshctrl' };
+ if (args.id) { op.meshid = args.id; }
+ if ((typeof args.devicename == 'string') && (args.devicename != '')) { op.devicename = args.devicename; }
+ if ((typeof args.hostname == 'string') && (args.hostname != '')) { op.hostname = args.hostname; }
+ if ((typeof args.user == 'string') && (args.user != '')) { op.amtusername = args.user; }
+ if ((typeof args.pass == 'string') && (args.pass != '')) { op.amtpassword = args.pass; }
+ if (args.notls) { op.amttls = 0; }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'addlocaldevice': {
+ var op = { action: 'addlocaldevice', type: 4, responseid: 'meshctrl' };
+ if (args.id) { op.meshid = args.id; }
+ if ((typeof args.devicename == 'string') && (args.devicename != '')) { op.devicename = args.devicename; }
+ if ((typeof args.hostname == 'string') && (args.hostname != '')) { op.hostname = args.hostname; }
+ if (args.type) {
+ if ((typeof parseInt(args.type) != 'number') || isNaN(parseInt(args.type))) { console.log("Invalid type."); process.exit(1); return; }
+ op.type = args.type;
+ }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'editdevicegroup': {
+ var op = { action: 'editmesh', responseid: 'meshctrl' };
+ if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshidname = args.group; }
+ if ((typeof args.name == 'string') && (args.name != '')) { op.meshname = args.name; }
+ if (args.desc === true) { op.desc = ""; } else if (typeof args.desc == 'string') { op.desc = args.desc; }
+ if (args.invitecodes === true) { op.invite = "*"; } else if (typeof args.invitecodes == 'string') {
+ var invitecodes = args.invitecodes.split(','), invitecodes2 = [];
+ for (var i in invitecodes) { if (invitecodes[i].length > 0) { invitecodes2.push(invitecodes[i]); } }
+ if (invitecodes2.length > 0) {
+ op.invite = { codes: invitecodes2, flags: 0 };
+ if (args.backgroundonly === true) { op.invite.flags = 2; }
+ else if (args.interactiveonly === true) { op.invite.flags = 1; }
+ }
+ }
+ if (args.flags != null) {
+ var flags = parseInt(args.flags);
+ if (typeof flags == 'number') { op.flags = flags; }
+ }
+ if (args.consent != null) {
+ var consent = parseInt(args.consent);
+ if (typeof consent == 'number') { op.consent = consent; }
+ }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'movetodevicegroup': {
+ var op = { action: 'changeDeviceMesh', responseid: 'meshctrl', nodeids: [args.devid] };
+ if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'addusertodevicegroup': {
+ var meshrights = 0;
+ if (args.fullrights) { meshrights = 0xFFFFFFFF; }
+ if (args.editgroup) { meshrights |= 1; }
+ if (args.manageusers) { meshrights |= 2; }
+ if (args.managedevices) { meshrights |= 4; }
+ if (args.remotecontrol) { meshrights |= 8; }
+ if (args.agentconsole) { meshrights |= 16; }
+ if (args.serverfiles) { meshrights |= 32; }
+ if (args.wakedevices) { meshrights |= 64; }
+ if (args.notes) { meshrights |= 128; }
+ if (args.desktopviewonly) { meshrights |= 256; }
+ if (args.noterminal) { meshrights |= 512; }
+ if (args.nofiles) { meshrights |= 1024; }
+ if (args.noamt) { meshrights |= 2048; }
+ if (args.limiteddesktop) { meshrights |= 4096; }
+ if (args.limitedevents) { meshrights |= 8192; }
+ if (args.chatnotify) { meshrights |= 16384; }
+ if (args.uninstall) { meshrights |= 32768; }
+ var op = { action: 'addmeshuser', usernames: [args.userid], meshadmin: meshrights, responseid: 'meshctrl' };
+ if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'removeuserfromdevicegroup': {
+ var op = { action: 'removemeshuser', userid: args.userid, responseid: 'meshctrl' };
+ if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'addusertodevice': {
+ var meshrights = 0;
+ if (args.fullrights) { meshrights = (8 + 16 + 32 + 64 + 128 + 16384 + 32768); }
+ if (args.remotecontrol) { meshrights |= 8; }
+ if (args.agentconsole) { meshrights |= 16; }
+ if (args.serverfiles) { meshrights |= 32; }
+ if (args.wakedevices) { meshrights |= 64; }
+ if (args.notes) { meshrights |= 128; }
+ if (args.desktopviewonly) { meshrights |= 256; }
+ if (args.noterminal) { meshrights |= 512; }
+ if (args.nofiles) { meshrights |= 1024; }
+ if (args.noamt) { meshrights |= 2048; }
+ if (args.limiteddesktop) { meshrights |= 4096; }
+ if (args.limitedevents) { meshrights |= 8192; }
+ if (args.chatnotify) { meshrights |= 16384; }
+ if (args.uninstall) { meshrights |= 32768; }
+ var op = { action: 'adddeviceuser', nodeid: args.id, usernames: [args.userid], rights: meshrights, responseid: 'meshctrl' };
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'removeuserfromdevice': {
+ var op = { action: 'adddeviceuser', nodeid: args.id, usernames: [args.userid], rights: 0, remove: true, responseid: 'meshctrl' };
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'sendinviteemail': {
+ var op = { action: 'inviteAgent', email: args.email, name: '', os: '0', responseid: 'meshctrl' }
+ if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
+ if (args.name) { op.name = args.name; }
+ if (args.message) { op.msg = args.message; }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'generateinvitelink': {
+ var op = { action: 'createInviteLink', expire: args.hours, flags: 0, responseid: 'meshctrl' }
+ if (args.id) { op.meshid = args.id; } else if (args.group) { op.meshname = args.group; }
+ if (args.flags) { op.flags = args.flags; }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'broadcast': {
+ var op = { action: 'userbroadcast', msg: args.msg, responseid: 'meshctrl' };
+ if (args.user) { op.userid = args.user; }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'showevents': {
+ console.log('Connected. Press ctrl-c to end.');
+ break;
+ }
+ case 'deviceinfo': {
+ settings.deviceinfocount = 4;
+ ws.send(JSON.stringify({ action: 'nodes' }));
+ ws.send(JSON.stringify({ action: 'getnetworkinfo', nodeid: args.id, responseid: 'meshctrl' }));
+ ws.send(JSON.stringify({ action: 'lastconnect', nodeid: args.id, responseid: 'meshctrl' }));
+ ws.send(JSON.stringify({ action: 'getsysinfo', nodeid: args.id, nodeinfo: true, responseid: 'meshctrl' }));
+ break;
+ }
+ case 'removedevice': {
+ var op = { action: 'removedevices', nodeids: [ args.id ], responseid: 'meshctrl' };
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'editdevice': {
+ var op = { action: 'changedevice', nodeid: args.id, responseid: 'meshctrl' };
+ if (typeof args.name == 'string') { op.name = args.name; }
+ if (typeof args.name == 'number') { op.name = '' + args.name; }
+ if (args.desc) { if (args.desc === true) { op.desc = ''; } else if (typeof args.desc == 'string') { op.desc = args.desc; } else if (typeof args.desc == 'number') { op.desc = '' + args.desc; } }
+ if (args.tags) { if (args.tags === true) { op.tags = ''; } else if (typeof args.tags == 'string') { op.tags = args.tags.split(','); } else if (typeof args.tags == 'number') { op.tags = '' + args.tags; } }
+ if (args.icon) { op.icon = parseInt(args.icon); if ((typeof op.icon != 'number') || isNaN(op.icon) || (op.icon < 1) || (op.icon > 8)) { console.log("Icon must be between 1 and 8."); process.exit(1); return; } }
+ if (args.consent) { op.consent = parseInt(args.consent); if ((typeof op.consent != 'number') || isNaN(op.consent) || (op.consent < 1)) { console.log("Invalid consent flags."); process.exit(1); return; } }
+ ws.send(JSON.stringify(op));
+ break;
+ }
+ case 'runcommand': {
+ var runAsUser = 0;
+ if (args.runasuser) { runAsUser = 1; } else if (args.runasuseronly) { runAsUser = 2; }
+ var reply = false;
+ if (args.reply) { reply = true; }
+ ws.send(JSON.stringify({ action: 'runcommands', nodeids: [args.id], type: ((args.powershell) ? 2 : 0), cmds: args.run, responseid: 'meshctrl', runAsUser: runAsUser, reply: reply }));
+ break;
+ }
+ case 'shell':
+ case 'upload':
+ case 'download': {
+ ws.send("{\"action\":\"authcookie\"}");
+ break;
+ }
+ case 'devicepower': {
+ var nodes = args.id.split(',');
+ if (args.wake) {
+ // Wake operation
+ ws.send(JSON.stringify({ action: 'wakedevices', nodeids: nodes, responseid: 'meshctrl' }));
+ } else if (args.off) {
+ // Power off operation
+ ws.send(JSON.stringify({ action: 'poweraction', nodeids: nodes, actiontype: 2, responseid: 'meshctrl' }));
+ } else if (args.reset) {
+ // Reset operation
+ ws.send(JSON.stringify({ action: 'poweraction', nodeids: nodes, actiontype: 3, responseid: 'meshctrl' }));
+ } else if (args.sleep) {
+ // Sleep operation
+ ws.send(JSON.stringify({ action: 'poweraction', nodeids: nodes, actiontype: 4, responseid: 'meshctrl' }));
+ } else if (args.amton) {
+ // Intel AMT Power on operation
+ ws.send(JSON.stringify({ action: 'poweraction', nodeids: nodes, actiontype: 302, responseid: 'meshctrl' }));
+ } else if (args.amtoff) {
+ // Intel AMT Power off operation
+ ws.send(JSON.stringify({ action: 'poweraction', nodeids: nodes, actiontype: 308, responseid: 'meshctrl' }));
+ } else if (args.amtreset) {
+ // Intel AMT Power reset operation
+ ws.send(JSON.stringify({ action: 'poweraction', nodeids: nodes, actiontype: 310, responseid: 'meshctrl' }));
+ } else {
+ console.log('No power operation specified.');
+ process.exit(1);
+ }
+ break;
+ }
+ case 'agentdownload': {
+ // Download an agent
+ var u = settings.xxurl.replace('wss://', 'https://').replace('/control.ashx', '/meshagents');
+ if (u.indexOf('?') > 0) { u += '&'; } else { u += '?'; }
+ u += 'id=' + args.type + '&meshid=' + args.id;
+ if (args.installflags) {
+ if ((typeof parseInt(args.installflags) != 'number') || isNaN(parseInt(args.installflags)) || (parseInt(args.installflags) < 0) || (parseInt(args.installflags) > 2)) { console.log("Invalid Installflags."); process.exit(1); return; }
+ u += '&installflags=' + args.installflags;
+ }
+ const options = { rejectUnauthorized: false, checkServerIdentity: onVerifyServer }
+ const fs = require('fs');
+ const https = require('https');
+ var downloadSize = 0;
+ const req = https.request(u, options, function (res) {
+ if (res.statusCode != 200) {
+ console.log('Download error, statusCode: ' + res.statusCode);
+ process.exit(1);
+ } else {
+ // Agent the agent filename
+ var agentFileName = 'meshagent';
+ if ((res.headers) && (res.headers['content-disposition'] != null)) {
+ var i = res.headers['content-disposition'].indexOf('filename=\"');
+ if (i >= 0) {
+ agentFileName = res.headers['content-disposition'].substring(i + 10);
+ i = agentFileName.indexOf('\"');
+ if (i >= 0) { agentFileName = agentFileName.substring(0, i); }
+ }
+ }
+ // Check if this file already exists
+ if (fs.existsSync(agentFileName)) { console.log('File \"' + agentFileName + '\" already exists.'); process.exit(1); }
+ var fd = fs.openSync(agentFileName, 'w'); // Open the file for writing
+ res.on('data', function (d) {
+ downloadSize += d.length;
+ fs.writeSync(fd, d); // Save to file
+ });
+ res.on('end', function (d) {
+ fs.closeSync(fd); // Close file
+ console.log('Downloaded ' + downloadSize + ' byte(s) to \"' + agentFileName + '\"');
+ process.exit(1);
+ });
+ }
+ })
+ req.on('error', function (error) { console.error(error); process.exit(1); })
+ req.end()
+ break;
+ }
+ case 'webrelay': {
+ var protocol = null;
+ if (args.type != null) {
+ if (args.type == 'http') {
+ protocol = 1;
+ } else if (args.type == 'https') {
+ protocol = 2;
+ } else {
+ console.log("Unknown protocol type: " + args.type); process.exit(1);
+ }
+ }
+ var port = null;
+ if (typeof args.port == 'number') {
+ if ((args.port < 1) || (args.port > 65535)) { console.log("Port number must be between 1 and 65535."); process.exit(1); }
+ port = args.port;
+ } else if (protocol == 1) {
+ port = 80;
+ } else if (protocol == 2) {
+ port = 443;
+ }
+ ws.send(JSON.stringify({ action: 'webrelay', nodeid: args.id, port: port, appid: protocol, responseid: 'meshctrl' }));
+ break;
+ }
+ case 'devicesharing': {
+ if (args.add) {
+ if (args.add.length == 0) { console.log("Invalid guest name."); process.exit(1); }
+
+ // Sharing type, desktop or terminal
+ var p = 0;
+ if (args.type != null) {
+ var shareTypes = args.type.toLowerCase().split(',');
+ for (var i in shareTypes) { if ((shareTypes[i] != 'terminal') && (shareTypes[i] != 'desktop') && (shareTypes[i] != 'files') && (shareTypes[i] != 'http') && (shareTypes[i] != 'https')) { console.log("Unknown sharing type: " + shareTypes[i]); process.exit(1); } }
+ if (shareTypes.indexOf('terminal') >= 0) { p |= 1; }
+ if (shareTypes.indexOf('desktop') >= 0) { p |= 2; }
+ if (shareTypes.indexOf('files') >= 0) { p |= 4; }
+ if (shareTypes.indexOf('http') >= 0) { p |= 8; }
+ if (shareTypes.indexOf('https') >= 0) { p |= 16; }
+ }
+ if (p == 0) { p = 2; } // Desktop
+
+ // Sharing view only
+ var viewOnly = false;
+ if (args.viewonly) { viewOnly = true; }
+
+ // User consent
+ var consent = 0;
+ if (args.consent == null) {
+ if ((p & 1) != 0) { consent = 0x0002; } // Terminal notify
+ if ((p & 2) != 0) { consent = 0x0001; } // Desktop notify
+ if ((p & 4) != 0) { consent = 0x0004; } // Files notify
+ } else {
+ if (typeof args.consent == 'string') {
+ var flagStrs = args.consent.split(',');
+ for (var i in flagStrs) {
+ var flagStr = flagStrs[i].toLowerCase();
+ if (flagStr == 'none') { consent = 0; }
+ else if (flagStr == 'notify') {
+ if ((p & 1) != 0) { consent |= 0x0002; } // Terminal notify
+ if ((p & 2) != 0) { consent |= 0x0001; } // Desktop notify
+ if ((p & 4) != 0) { consent |= 0x0004; } // Files notify
+ } else if (flagStr == 'prompt') {
+ if ((p & 1) != 0) { consent |= 0x0010; } // Terminal prompt
+ if ((p & 2) != 0) { consent |= 0x0008; } // Desktop prompt
+ if ((p & 4) != 0) { consent |= 0x0020; } // Files prompt
+ } else if (flagStr == 'bar') {
+ if ((p & 2) != 0) { consent |= 0x0040; } // Desktop toolbar
+ } else { console.log("Unknown consent type."); process.exit(1); return; }
+ }
+ }
+ }
+
+ var port = null;
+ // Set Port Number if http or https
+ if ((p & 8) || (p & 16)) {
+ if (typeof args.port == 'number') {
+ if ((args.port < 1) || (args.port > 65535)) { console.log("Port number must be between 1 and 65535."); process.exit(1); }
+ port = args.port;
+ } else if ((p & 8)) {
+ port = 80;
+ } else if ((p & 16)) {
+ port = 443;
+ }
+ }
+
+ // Start and end time
+ var start = null, end = null;
+ if (args.start) { start = Math.floor(Date.parse(args.start) / 1000); end = start + (60 * 60); }
+ if (args.end) { if (start == null) { start = Math.floor(Date.now() / 1000) } end = Math.floor(Date.parse(args.end) / 1000); if (end <= start) { console.log("End time must be ahead of start time."); process.exit(1); return; } }
+ if (args.duration) { if (start == null) { start = Math.floor(Date.now() / 1000) } end = start + parseInt(args.duration * 60); }
+
+ // Recurring
+ var recurring = 0;
+ if (args.daily) { recurring = 1; } else if (args.weekly) { recurring = 2; }
+ if (recurring > 0) {
+ if (args.end != null) { console.log("End time can't be specified for recurring shares, use --duration only."); process.exit(1); return; }
+ if (args.duration == null) { args.duration = 60; } else { args.duration = parseInt(args.duration); }
+ if (start == null) { start = Math.floor(Date.now() / 1000) }
+ if ((typeof args.duration != 'number') || (args.duration < 1)) { console.log("Invalid duration value."); process.exit(1); return; }
+
+ // Recurring sharing
+ ws.send(JSON.stringify({ action: 'createDeviceShareLink', nodeid: args.id, guestname: args.add, p: p, consent: consent, start: start, expire: args.duration, recurring: recurring, viewOnly: viewOnly, port: port, responseid: 'meshctrl' }));
+ } else {
+ if ((start == null) && (end == null)) {
+ // Unlimited sharing
+ ws.send(JSON.stringify({ action: 'createDeviceShareLink', nodeid: args.id, guestname: args.add, p: p, consent: consent, expire: 0, viewOnly: viewOnly, port: port, responseid: 'meshctrl' }));
+ } else {
+ // Time limited sharing
+ ws.send(JSON.stringify({ action: 'createDeviceShareLink', nodeid: args.id, guestname: args.add, p: p, consent: consent, start: start, end: end, viewOnly: viewOnly, port: port, responseid: 'meshctrl' }));
+ }
+ }
+ } else if (args.remove) {
+ ws.send(JSON.stringify({ action: 'removeDeviceShare', nodeid: args.id, publicid: args.remove, responseid: 'meshctrl' }));
+ } else {
+ ws.send(JSON.stringify({ action: 'deviceShares', nodeid: args.id, responseid: 'meshctrl' }));
+ }
+ break;
+ }
+ case 'deviceopenurl': {
+ ws.send(JSON.stringify({ action: 'msg', type: 'openUrl', nodeid: args.id, url: args.openurl, responseid: 'meshctrl' }));
+ break;
+ }
+ case 'devicemessage': {
+ ws.send(JSON.stringify({ action: 'msg', type: 'messagebox', nodeid: args.id, title: args.title ? args.title : "MeshCentral", msg: args.msg, timeout: args.timeout ? args.timeout : 120000, responseid: 'meshctrl' }));
+ break;
+ }
+ case 'devicetoast': {
+ ws.send(JSON.stringify({ action: 'toast', nodeids: [args.id], title: args.title ? args.title : "MeshCentral", msg: args.msg, responseid: 'meshctrl' }));
+ break;
+ }
+ case 'groupmessage': {
+ ws.send(JSON.stringify({ action: 'nodes', meshid: args.id, responseid: 'meshctrl' }));
+ break;
+ }
+ case 'grouptoast': {
+ ws.send(JSON.stringify({ action: 'nodes', meshid: args.id, responseid: 'meshctrl' }));
+ break;
+ }
+ case 'report': {
+ var reporttype = 1;
+ switch(args.type) {
+ case 'traffic':
+ reporttype = 2;
+ break;
+ case 'logins':
+ reporttype = 3;
+ break;
+ case 'db':
+ reporttype = 4;
+ break;
+ }
+
+ var reportgroupby = 1;
+ if(args.groupby){
+ reportgroupby = args.groupby === 'device' ? 2 : args.groupby === 'day' ? 3: 1;
+ }
+
+ var start = null, end = null;
+ if (args.start) {
+ start = Math.floor(Date.parse(args.start) / 1000);
+ } else {
+ start = reportgroupby === 3 ? Math.round(new Date().getTime() / 1000) - (168 * 3600) : Math.round(new Date().getTime() / 1000) - (24 * 3600);
+ }
+ if (args.end) {
+ end = Math.floor(Date.parse(args.end) / 1000);
+ } else {
+ end = Math.round(new Date().getTime() / 1000);
+ }
+ if (end <= start) { console.log("End time must be ahead of start time."); process.exit(1); return; }
+
+ ws.send(JSON.stringify({ action: 'report', type: reporttype, groupBy: reportgroupby, devGroup: args.devicegroup || null, start, end, tz: Intl.DateTimeFormat().resolvedOptions().timeZone, tf: new Date().getTimezoneOffset(), showTraffic: args.hasOwnProperty('showtraffic'), l: 'en', responseid: 'meshctrl' }));
+ break;
+ }
+ }
+ });
+
+ function getSiteAdminRights(args) {
+ var siteadmin = -1;
+ if (typeof args.rights == 'number') {
+ siteadmin = args.rights;
+ } else if (typeof args.rights == 'string') {
+ siteadmin = 0;
+ var srights = args.rights.toLowerCase().split(',');
+ if (srights.indexOf('full') != -1) { siteadmin = 0xFFFFFFFF; }
+ if (srights.indexOf('none') != -1) { siteadmin = 0x00000000; }
+ if (srights.indexOf('backup') != -1 || srights.indexOf('serverbackup') != -1) { siteadmin |= 0x00000001; }
+ if (srights.indexOf('manageusers') != -1) { siteadmin |= 0x00000002; }
+ if (srights.indexOf('restore') != -1 || srights.indexOf('serverrestore') != -1) { siteadmin |= 0x00000004; }
+ if (srights.indexOf('fileaccess') != -1) { siteadmin |= 0x00000008; }
+ if (srights.indexOf('update') != -1 || srights.indexOf('serverupdate') != -1) { siteadmin |= 0x00000010; }
+ if (srights.indexOf('locked') != -1) { siteadmin |= 0x00000020; }
+ if (srights.indexOf('nonewgroups') != -1) { siteadmin |= 0x00000040; }
+ if (srights.indexOf('notools') != -1) { siteadmin |= 0x00000080; }
+ if (srights.indexOf('usergroups') != -1) { siteadmin |= 0x00000100; }
+ if (srights.indexOf('recordings') != -1) { siteadmin |= 0x00000200; }
+ if (srights.indexOf('locksettings') != -1) { siteadmin |= 0x00000400; }
+ if (srights.indexOf('allevents') != -1) { siteadmin |= 0x00000800; }
+ if (srights.indexOf('nonewdevices') != -1) { siteadmin |= 0x00001000; }
+ }
+
+ if (args.siteadmin) { siteadmin = 0xFFFFFFFF; }
+ if (args.manageusers) { if (siteadmin == -1) { siteadmin = 0; } siteadmin |= 2; }
+ if (args.fileaccess) { if (siteadmin == -1) { siteadmin = 0; } siteadmin |= 8; }
+ if (args.serverupdate) { if (siteadmin == -1) { siteadmin = 0; } siteadmin |= 16; }
+ if (args.locked) { if (siteadmin == -1) { siteadmin = 0; } siteadmin |= 32; }
+ if (args.nonewgroups) { if (siteadmin == -1) { siteadmin = 0; } siteadmin |= 64; }
+ if (args.notools) { if (siteadmin == -1) { siteadmin = 0; } siteadmin |= 128; }
+ return siteadmin;
+ }
+
+ ws.on('close', function () { process.exit(); });
+ ws.on('error', function (err) {
+ if (err.code == 'ENOTFOUND') { console.log('Unable to resolve ' + url); }
+ else if (err.code == 'ECONNREFUSED') { console.log('Unable to connect to ' + url); }
+ else { console.log('Unable to connect to ' + url); }
+ process.exit();
+ });
+
+ ws.on('message', function incoming(rawdata) {
+ var data = null;
+ try { data = JSON.parse(rawdata); } catch (ex) { }
+ if (data == null) { console.log('Unable to parse data: ' + rawdata); }
+ if (settings.cmd == 'showevents') {
+ if (args.filter == null) {
+ // Display all events
+ console.log(JSON.stringify(data, null, 2));
+ } else {
+ // Display select events
+ var filters = args.filter.split(',');
+ if (typeof data.event == 'object') {
+ if (filters.indexOf(data.event.action) >= 0) { console.log(JSON.stringify(data, null, 2) + '\r\n'); }
+ } else {
+ if (filters.indexOf(data.action) >= 0) { console.log(JSON.stringify(data, null, 2) + '\r\n'); }
+ }
+ }
+ return;
+ }
+ switch (data.action) {
+ case 'serverinfo': { // SERVERINFO
+ settings.currentDomain = data.serverinfo.domain;
+ if (settings.cmd == 'serverinfo') {
+ if (args.json) {
+ console.log(JSON.stringify(data.serverinfo, ' ', 2));
+ } else {
+ for (var i in data.serverinfo) { console.log(i + ':', data.serverinfo[i]); }
+ }
+ process.exit();
+ }
+ break;
+ }
+ case 'events': {
+ if (settings.cmd == 'listevents') {
+ if (args.raw) {
+ // RAW JSON
+ console.log(JSON.stringify(data.events));
+ } else if (args.json) {
+ // Formatted JSON
+ console.log(JSON.stringify(data.events, null, 2));
+ } else {
+ if ((args.id == null) && (args.userid == null)) {
+ // CSV format
+ console.log("time,type,action,nodeid,userid,msg");
+ for (var i in data.events) {
+ var x = [];
+ x.push(data.events[i].time);
+ x.push(data.events[i].etype);
+ x.push(data.events[i].action);
+ x.push(data.events[i].nodeid);
+ x.push(data.events[i].userid);
+ x.push(data.events[i].msg);
+ console.log(csvFormatArray(x));
+ }
+ } else if (args.id != null) {
+ // CSV format
+ console.log("time,type,action,userid,msg");
+ for (var i in data.events) {
+ var x = [];
+ x.push(data.events[i].time);
+ x.push(data.events[i].etype);
+ x.push(data.events[i].action);
+ x.push(data.events[i].userid);
+ x.push(data.events[i].msg);
+ console.log(csvFormatArray(x));
+ }
+ } else if (args.userid != null) {
+ // CSV format
+ console.log("time,type,action,nodeid,msg");
+ for (var i in data.events) {
+ var x = [];
+ x.push(data.events[i].time);
+ x.push(data.events[i].etype);
+ x.push(data.events[i].action);
+ x.push(data.events[i].nodeid);
+ x.push(data.events[i].msg);
+ console.log(csvFormatArray(x));
+ }
+ }
+ }
+ process.exit();
+ }
+ break;
+ }
+ case 'authcookie': { // SHELL, UPLOAD, DOWNLOAD
+ if ((settings.cmd == 'shell') || (settings.cmd == 'upload') || (settings.cmd == 'download')) {
+ var protocol = 1; // Terminal
+ if ((settings.cmd == 'upload') || (settings.cmd == 'download')) { protocol = 5; } // Files
+ if ((args.id.split('/') != 3) && (settings.currentDomain != null)) { args.id = 'node/' + settings.currentDomain + '/' + args.id; }
+ var id = getRandomHex(6);
+ ws.send(JSON.stringify({ action: 'msg', nodeid: args.id, type: 'tunnel', usage: 1, value: '*/meshrelay.ashx?p=' + protocol + '&nodeid=' + args.id + '&id=' + id + '&rauth=' + data.rcookie, responseid: 'meshctrl' }));
+ connectTunnel(url.replace('/control.ashx', '/meshrelay.ashx?browser=1&p=' + protocol + '&nodeid=' + encodeURIComponent(args.id) + '&id=' + id + '&auth=' + data.cookie));
+ }
+ break;
+ }
+ case 'deviceShares': { // DEVICESHARING
+ if (data.result != null) {
+ console.log(data.result);
+ } else {
+ if ((data.deviceShares == null) || (data.deviceShares.length == 0)) {
+ console.log('No device sharing links for this device.');
+ } else {
+ if (args.json) {
+ console.log(data.deviceShares);
+ } else {
+ for (var i in data.deviceShares) {
+ var share = data.deviceShares[i];
+ var shareType = [];
+ if ((share.p & 1) != 0) { shareType.push("Terminal"); }
+ if ((share.p & 2) != 0) { if (share.viewOnly) { shareType.push("View Only Desktop"); } else { shareType.push("Desktop"); } }
+ if ((share.p & 4) != 0) { shareType.push("Files"); }
+ shareType = shareType.join(' + ');
+ if (shareType == '') { shareType = "Unknown"; }
+ var consent = [];
+ if ((share.consent & 0x0001) != 0) { consent.push("Desktop Notify"); }
+ if ((share.consent & 0x0008) != 0) { consent.push("Desktop Prompt"); }
+ if ((share.consent & 0x0040) != 0) { consent.push("Desktop Connection Toolbar"); }
+ if ((share.consent & 0x0002) != 0) { consent.push("Terminal Notify"); }
+ if ((share.consent & 0x0010) != 0) { consent.push("Terminal Prompt"); }
+ if ((share.consent & 0x0004) != 0) { consent.push("Files Notify"); }
+ if ((share.consent & 0x0020) != 0) { consent.push("Files Prompt"); }
+ console.log('----------');
+ console.log('Identifier: ' + share.publicid);
+ console.log('Type: ' + shareType);
+ console.log('UserId: ' + share.userid);
+ console.log('Guest Name: ' + share.guestName);
+ console.log('User Consent: ' + consent.join(', '));
+ if (share.startTime) { console.log('Start Time: ' + new Date(share.startTime).toLocaleString()); }
+ if (share.expireTime) { console.log('Expire Time: ' + new Date(share.expireTime).toLocaleString()); }
+ if (share.duration) { console.log('Duration: ' + share.duration + ' minute' + ((share.duration > 1) ? 's' : '')); }
+ if (share.recurring == 1) { console.log('Recurring: ' + 'Daily'); }
+ if (share.recurring == 2) { console.log('Recurring: ' + 'Weekly'); }
+ console.log('URL: ' + share.url);
+ }
+ }
+ }
+ }
+ process.exit();
+ break;
+ }
+ case 'userinfo': { // USERINFO
+ if (settings.cmd == 'userinfo') {
+ if (args.json) {
+ console.log(JSON.stringify(data.userinfo, ' ', 2));
+ } else {
+ for (var i in data.userinfo) { console.log(i + ':', data.userinfo[i]); }
+ }
+ process.exit();
+ }
+ break;
+ }
+ case 'getsysinfo': { // DEVICEINFO
+ if (settings.cmd == 'deviceinfo') {
+ settings.sysinfo = (data.result) ? null : data;
+ if (--settings.deviceinfocount == 0) { displayDeviceInfo(settings.sysinfo, settings.lastconnect, settings.networking, settings.nodes); process.exit(); }
+ }
+ break;
+ }
+ case 'lastconnect': {
+ if (settings.cmd == 'deviceinfo') {
+ settings.lastconnect = (data.result) ? null : data;
+ if (--settings.deviceinfocount == 0) { displayDeviceInfo(settings.sysinfo, settings.lastconnect, settings.networking, settings.nodes); process.exit(); }
+ }
+ break;
+ }
+ case 'getnetworkinfo': {
+ if (settings.cmd == 'deviceinfo') {
+ settings.networking = (data.result) ? null : data;
+ if (--settings.deviceinfocount == 0) { displayDeviceInfo(settings.sysinfo, settings.lastconnect, settings.networking, settings.nodes); process.exit(); }
+ }
+ break;
+ }
+ case 'msg': // SHELL
+ case 'toast': // TOAST
+ case 'adduser': // ADDUSER
+ case 'edituser': // EDITUSER
+ case 'addamtdevice': // ADDAMTDEVICE
+ case 'addlocaldevice': // ADDLOCALDEVICE
+ case 'removedevices': // REMOVEDEVICE
+ case 'changedevice': // EDITDEVICE
+ case 'deleteuser': // REMOVEUSER
+ case 'createmesh': // ADDDEVICEGROUP
+ case 'deletemesh': // REMOVEDEVICEGROUP
+ case 'editmesh': // EDITDEVICEGROUP
+ case 'wakedevices':
+ case 'changeDeviceMesh':
+ case 'addmeshuser': //
+ case 'removemeshuser': //
+ case 'wakedevices': //
+ case 'inviteAgent': //
+ case 'adddeviceuser': //
+ case 'createusergroup': //
+ case 'deleteusergroup': //
+ case 'runcommands':
+ case 'poweraction':
+ case 'addusertousergroup':
+ case 'removeuserfromusergroup':
+ case 'removeDeviceShare':
+ case 'userbroadcast': { // BROADCAST
+ if (((settings.cmd == 'shell') || (settings.cmd == 'upload') || (settings.cmd == 'download')) && (data.result == 'OK')) return;
+ if ((data.type == 'runcommands') && (settings.cmd != 'runcommand')) return;
+ if ((settings.multiresponse != null) && (settings.multiresponse > 1)) { settings.multiresponse--; break; }
+ if (data.responseid == 'meshctrl') {
+ if (data.meshid) { console.log(data.result, data.meshid); }
+ else if (data.userid) { console.log(data.result, data.userid); }
+ else console.log(data.result);
+ process.exit();
+ }
+ break;
+ }
+ case 'createDeviceShareLink':
+ case 'webrelay':
+ if (data.result == 'OK') {
+ if (data.publicid) { console.log('ID: ' + data.publicid); }
+ console.log('URL: ' + data.url);
+ } else {
+ console.log(data.result);
+ }
+ process.exit();
+ break;
+ case 'createInviteLink':
+ if (data.responseid == 'meshctrl') {
+ if (data.url) { console.log(data.url); }
+ else console.log(data.result);
+ process.exit();
+ }
+ break;
+ case 'wssessioncount': { // LIST USER SESSIONS
+ if (args.json) {
+ console.log(JSON.stringify(data.wssessions, ' ', 2));
+ } else {
+ for (var i in data.wssessions) { console.log(i + ', ' + ((data.wssessions[i] > 1) ? (data.wssessions[i] + ' sessions.') : ("1 session."))); }
+ }
+ process.exit();
+ break;
+ }
+ case 'usergroups': { // LIST USER GROUPS
+ if (settings.cmd == 'listusergroups') {
+ if (args.json) {
+ console.log(JSON.stringify(data.ugroups, ' ', 2));
+ } else {
+ for (var i in data.ugroups) {
+ var x = i + ', ' + data.ugroups[i].name;
+ if (data.ugroups[i].desc && (data.ugroups[i].desc != '')) { x += ', ' + data.ugroups[i].desc; }
+ console.log(x);
+ var mesh = [], user = [], node = [];
+ if (data.ugroups[i].links != null) { for (var j in data.ugroups[i].links) { if (j.startsWith('mesh/')) { mesh.push(j); } if (j.startsWith('user/')) { user.push(j); } if (j.startsWith('node/')) { node.push(j); } } }
+ console.log(' Users:');
+ if (user.length > 0) { for (var j in user) { console.log(' ' + user[j]); } } else { console.log(' (None)'); }
+ console.log(' Device Groups:');
+ if (mesh.length > 0) { for (var j in mesh) { console.log(' ' + mesh[j] + ', ' + data.ugroups[i].links[mesh[j]].rights); } } else { console.log(' (None)'); }
+ console.log(' Devices:');
+ if (node.length > 0) { for (var j in node) { console.log(' ' + node[j] + ', ' + data.ugroups[i].links[node[j]].rights); } } else { console.log(' (None)'); }
+ }
+ }
+ process.exit();
+ } else if (settings.cmd == 'removeallusersfromusergroup') {
+ var ugrpid = args.groupid, exit = false;
+ if ((args.domain != null) && (userid.indexOf('/') < 0)) { ugrpid = 'ugrp/' + args.domain + '/' + ugrpid; }
+ var ugroup = data.ugroups[ugrpid];
+ if (ugroup == null) {
+ console.log('User group not found.');
+ exit = true;
+ } else {
+ var usercount = 0;
+ if (ugroup.links) {
+ for (var i in ugroup.links) {
+ if (i.startsWith('user/')) {
+ usercount++;
+ ws.send(JSON.stringify({ action: 'removeuserfromusergroup', ugrpid: ugrpid, userid: i, responseid: 'meshctrl' }));
+ console.log('Removing ' + i);
+ }
+ }
+ }
+ if (usercount == 0) { console.log('No users in this user group.'); exit = true; } else { settings.multiresponse = usercount; }
+ }
+ if (exit) { process.exit(); }
+ }
+ break;
+ }
+ case 'users': { // LISTUSERS
+ if (data.result) { console.log(data.result); process.exit(); return; }
+ if (args.filter) {
+ // Filter the list of users
+ var filters = args.filter.toLowerCase().split(',');
+ var filteredusers = [];
+ for (var i in data.users) {
+ var ok = false;
+ if ((filters.indexOf('2fa') >= 0) && ((data.users[i].otphkeys != null) || (data.users[i].otpkeys != null) || (data.users[i].otpsecret != null))) { ok = true; }
+ if ((filters.indexOf('no2fa') >= 0) && ((data.users[i].otphkeys == null) && (data.users[i].otpkeys == null) && (data.users[i].otpsecret == null))) { ok = true; }
+ if (ok == true) { filteredusers.push(data.users[i]); }
+ }
+ data.users = filteredusers;
+ }
+ if (args.json) {
+ console.log(JSON.stringify(data.users, ' ', 2));
+ } else {
+ if (args.idexists) { for (var i in data.users) { const u = data.users[i]; if ((u._id == args.idexists) || (u._id.split('/')[2] == args.idexists)) { console.log('1'); process.exit(); return; } } console.log('0'); process.exit(); return; }
+ if (args.nameexists) { for (var i in data.users) { const u = data.users[i]; if (u.name == args.nameexists) { console.log(u._id); process.exit(); return; } } process.exit(); return; }
+
+ console.log('id, name, email\r\n---------------');
+ for (var i in data.users) {
+ const u = data.users[i];
+ var t = "\"" + u._id.split('/')[2] + "\", \"" + u.name + "\"";
+ if (u.email != null) { t += ", \"" + u.email + "\""; }
+ console.log(t);
+ }
+ }
+ process.exit();
+ break;
+ }
+ case 'nodes': {
+ if (settings.cmd == 'deviceinfo') {
+ settings.nodes = (data.result) ? null : data;
+ if (--settings.deviceinfocount == 0) { displayDeviceInfo(settings.sysinfo, settings.lastconnect, settings.networking, settings.nodes); process.exit(); }
+ }
+ if ((settings.cmd == 'listdevices') && (data.responseid == 'meshctrl')) {
+ if ((data.result != null) && (data.result != 'ok')) {
+ console.log(data.result);
+ } else {
+ // Filter devices based on device id.
+ if (args.filterid) {
+ var filteridSplit = args.filterid.split(','), filters = [];
+ for (var i in filteridSplit) {
+ var f = filteridSplit[i].trim();
+ var g = f.split('/'); // If there is any / in the id, just grab the last part.
+ if (g.length > 0) { f = g[g.length - 1]; }
+ if (f != '') { filters.push(f); }
+ }
+ if (filters.length > 0) {
+ for (var mid in data.nodes) {
+ var filteredNodes = [];
+ for (var nid in data.nodes[mid]) {
+ var n = data.nodes[mid][nid], match = false;
+ for (var f in filters) { if (n._id.indexOf(filters[f]) >= 0) { match = true; } }
+ if (match) { filteredNodes.push(n); }
+ }
+ data.nodes[mid] = filteredNodes;
+ }
+ }
+ }
+
+ // Filter devices based on filter string
+ if (args.filter != null) {
+ for (var meshid in data.nodes) {
+ for (var d in data.nodes[meshid]) { data.nodes[meshid][d].meshid = meshid; }
+ data.nodes[meshid] = parseSearchOrInput(data.nodes[meshid], args.filter.toString().toLowerCase());
+ }
+ }
+
+ if (args.csv) {
+ // Return a flat list
+ var nodecount = 0;
+ for (var i in data.nodes) {
+ var devicesInMesh = data.nodes[i];
+ for (var j in devicesInMesh) {
+ var n = devicesInMesh[j];
+ nodecount++;
+ if (settings.xmeshes && settings.xmeshes[i]) {
+ console.log('\"' + settings.xmeshes[i]._id.split('/')[2] + '\",\"' + settings.xmeshes[i].name.split('\"').join('') + '\",\"' + n._id.split('/')[2] + '\",\"' + n.name.split('\"').join('') + '\",' + (n.icon ? n.icon : 0) + ',' + (n.conn ? n.conn : 0) + ',' + (n.pwr ? n.pwr : 0));
+ } else {
+ console.log('\"\",\"\",\"' + n._id.split('/')[2] + '\",\"' + n.name.split('\"').join('') + '\",' + (n.icon ? n.icon : 0) + ',' + (n.conn ? n.conn : 0) + ',' + (n.pwr ? n.pwr : 0));
+ }
+ }
+ }
+ if (nodecount == 0) { console.log('None'); }
+ } else if (args.count) {
+ // Return how many devices are in this group
+ var nodes = [];
+ for (var i in data.nodes) { var devicesInMesh = data.nodes[i]; for (var j in devicesInMesh) { nodes.push(devicesInMesh[j]); } }
+ console.log(nodes.length);
+ } else if (args.json) {
+ // Return all devices in JSON format
+ var nodes = [];
+
+ for (var i in data.nodes) {
+ const devicesInMesh = data.nodes[i];
+ for (var j in devicesInMesh) {
+ devicesInMesh[j].meshid = i; // Add device group id
+ if (settings.xmeshes && settings.xmeshes[i] && settings.xmeshes[i].name) { devicesInMesh[j].groupname = settings.xmeshes[i].name; } // Add device group name
+ nodes.push(devicesInMesh[j]);
+ }
+ }
+ console.log(JSON.stringify(nodes, ' ', 2));
+ } else {
+ // Display the list of nodes in text format
+ var nodecount = 0;
+ for (var i in data.nodes) {
+ var devicesInMesh = data.nodes[i];
+ if (devicesInMesh.length > 0) {
+ if (settings.xmeshes && settings.xmeshes[i] && settings.xmeshes[i].name) { console.log('\r\nDevice group: \"' + settings.xmeshes[i].name.split('\"').join('') + '\"'); }
+ console.log('id, name, icon, conn, pwr\r\n-------------------------');
+ for (var j in devicesInMesh) {
+ var n = devicesInMesh[j];
+ nodecount++;
+ console.log('\"' + n._id.split('/')[2] + '\", \"' + n.name.split('\"').join('') + '\", ' + (n.icon ? n.icon : 0) + ', ' + (n.conn ? n.conn : 0) + ', ' + (n.pwr ? n.pwr : 0));
+ }
+ }
+ }
+ if (nodecount == 0) { console.log('None'); }
+ }
+ }
+ process.exit();
+ }
+ if ((settings.cmd == 'groupmessage') && (data.responseid == 'meshctrl')) {
+ if ((data.nodes != null)) {
+ for (var i in data.nodes) {
+ for (let index = 0; index < data.nodes[i].length; index++) {
+ const element = data.nodes[i][index];
+ ws.send(JSON.stringify({ action: 'msg', type: 'messagebox', nodeid: element._id, title: args.title ? args.title : "MeshCentral", msg: args.msg, timeout: args.timeout ? args.timeout : 120000 }));
+ }
+ }
+ }
+
+ setTimeout(function(){ console.log('ok'); process.exit(); }, 1000);
+ }
+ if ((settings.cmd == 'grouptoast') && (data.responseid == 'meshctrl')) {
+ if (data.nodes != null) {
+ for (var i in data.nodes) {
+ var nodes = [];
+ for (let index = 0; index < data.nodes[i].length; index++) {
+ const element = data.nodes[i][index];
+ nodes.push(element._id);
+ }
+ ws.send(JSON.stringify({ action: 'toast', nodeids: nodes, title: args.title ? args.title : "MeshCentral", msg: args.msg, responseid: 'meshctrl' }));
+ }
+ }
+ }
+ break;
+ }
+ case 'meshes': { // LISTDEVICEGROUPS
+ if (settings.cmd == 'listdevices') {
+ // Store the list of device groups for later use
+ settings.xmeshes = {}
+ for (var i in data.meshes) { settings.xmeshes[data.meshes[i]._id] = data.meshes[i]; }
+ } else if (settings.cmd == 'listdevicegroups') {
+ if (args.json) {
+ // If asked, add the MeshID hex encoding to the JSON.
+ if (args.hex) { for (var i in data.meshes) { data.meshes[i]._idhex = '0x' + Buffer.from(data.meshes[i]._id.split('/')[2].replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase(); } }
+ console.log(JSON.stringify(data.meshes, ' ', 2));
+ } else {
+ if (args.idexists) { for (var i in data.meshes) { const u = data.meshes[i]; if ((u._id == args.idexists) || (u._id.split('/')[2] == args.idexists)) { console.log('1'); process.exit(); return; } } console.log('0'); process.exit(); return; }
+ if (args.nameexists) { for (var i in data.meshes) { const u = data.meshes[i]; if (u.name == args.nameexists) { console.log(u._id); process.exit(); return; } } process.exit(); return; }
+
+ console.log('id, name\r\n---------------');
+ for (var i in data.meshes) {
+ const m = data.meshes[i];
+ var mid = m._id.split('/')[2];
+ if (args.hex) { mid = '0x' + Buffer.from(mid.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase(); }
+ var t = "\"" + mid + "\", \"" + m.name + "\"";
+ console.log(t);
+ }
+ }
+ process.exit();
+ } else if (settings.cmd == 'listusersofdevicegroup') {
+ for (var i in data.meshes) {
+ const m = data.meshes[i];
+ var mid = m._id.split('/')[2];
+ if (mid == args.id) {
+ if (args.json) {
+ console.log(JSON.stringify(m.links, ' ', 2));
+ } else {
+ console.log('userid, rights\r\n---------------');
+ for (var l in m.links) {
+ var rights = m.links[l].rights;
+ var rightsstr = [];
+ if (rights == 4294967295) { rightsstr = ['FullAdministrator']; } else {
+ if (rights & 1) { rightsstr.push('EditMesh'); }
+ if (rights & 2) { rightsstr.push('ManageUsers'); }
+ if (rights & 4) { rightsstr.push('ManageComputers'); }
+ if (rights & 8) { rightsstr.push('RemoteControl'); }
+ if (rights & 16) { rightsstr.push('AgentConsole'); }
+ if (rights & 32) { rightsstr.push('ServerFiles'); }
+ if (rights & 64) { rightsstr.push('WakeDevice'); }
+ if (rights & 128) { rightsstr.push('SetNotes'); }
+ if (rights & 256) { rightsstr.push('RemoteViewOnly'); }
+ if (rights & 512) { rightsstr.push('NoTerminal'); }
+ if (rights & 1024) { rightsstr.push('NoFiles'); }
+ if (rights & 2048) { rightsstr.push('NoAMT'); }
+ if (rights & 4096) { rightsstr.push('DesktopLimitedInput'); }
+ }
+ console.log(l.split('/')[2] + ', ' + rightsstr.join(', '));
+ }
+ }
+ process.exit();
+ return;
+ }
+ }
+ console.log('Group id not found');
+ process.exit();
+ }
+ break;
+ }
+ case 'close': {
+ if (data.cause == 'noauth') {
+ if (data.msg == 'tokenrequired') {
+ console.log('Authentication token required, use --token [number].');
+ } else if (data.msg == 'nokey') {
+ console.log('URL key is invalid or missing, please specify ?key=xxx in url');
+ } else {
+ if ((args.loginkeyfile != null) || (args.loginkey != null)) {
+ console.log('Invalid login, check the login key and that this computer has the correct time.');
+ } else {
+ console.log('Invalid login.');
+ }
+ }
+ }
+ process.exit();
+ break;
+ }
+ case 'createLoginToken': {
+ if (data.result != null) {
+ console.log(data.result);
+ process.exit();
+ } else {
+ if (args.json) {
+ console.log(data);
+ } else {
+ console.log("New login token created.");
+ if (data.name) { console.log("Token name: " + data.name); }
+ if (data.created) { console.log("Created: " + new Date(data.created).toLocaleString()); }
+ if (data.expire) { console.log("Expire: " + new Date(data.expire).toLocaleString()); }
+ if (data.tokenUser) { console.log("Username: " + data.tokenUser); }
+ if (data.tokenPass) { console.log("Password: " + data.tokenPass); }
+ }
+ }
+ process.exit();
+ break;
+ }
+ case 'loginTokens': {
+ if (args.json) {
+ console.log(data.loginTokens);
+ } else {
+ console.log("Name Username Expire");
+ console.log("-------------------------------------------------------------------------------------");
+ if (data.loginTokens.length == 0) {
+ console.log("No login tokens");
+ } else {
+ for (var i in data.loginTokens) {
+ var t = data.loginTokens[i];
+ var e = (t.expire == 0) ? "Unlimited" : new Date(t.expire).toLocaleString();
+ console.log(padString(t.name, 28) + padString(t.tokenUser, 28) + e);
+ }
+ }
+ }
+ process.exit();
+ break;
+ }
+ case 'getDeviceDetails': {
+ console.log(data.data);
+ process.exit();
+ }
+ case 'report': {
+ console.log('group,' + data.data.columns.flatMap(c => c.id).join(','));
+ Object.keys(data.data.groups).forEach(gk => {
+ data.data.groups[gk].entries.forEach(e => {
+ console.log(gk + ',' + Object.values(e).join(','));
+ });
+ });
+ process.exit();
+ }
+ default: { break; }
+ }
+ //console.log('Data', data);
+ //setTimeout(function timeout() { ws.send(Date.now()); }, 500);
+ });
+}
+
+// String padding function
+
+function padString(str, pad) {
+ var xpad = ' ';
+ if (str.length >= pad) return str; return str + xpad.substring(0, pad - str.length)
+}
+
+function parseSearchAndInput(nodes, x) {
+ var s = x.split(' ' + "and" + ' '), r = null;
+ for (var i in s) {
+ var r2 = getDevicesThatMatchFilter(nodes, s[i]);
+ if (r == null) { r = r2; } else { var r3 = []; for (var j in r2) { if (r.indexOf(r2[j]) >= 0) { r3.push(r2[j]); } } r = r3; }
+ }
+ return r;
+}
+
+function parseSearchOrInput(nodes, x) {
+ var s = x.split(' ' + "or" + ' '), r = null;
+ for (var i in s) { var r2 = parseSearchAndInput(nodes, s[i]); if (r == null) { r = r2; } else { for (var j in r2) { if (r.indexOf(r2[j] >= 0)) { r.push(r2[j]); } } } }
+ return r;
+}
+
+function getDevicesThatMatchFilter(nodes, x) {
+ var r = [];
+ var userSearch = null, ipSearch = null, groupSearch = null, tagSearch = null, agentTagSearch = null, wscSearch = null, osSearch = null, amtSearch = null, descSearch = null;
+ if (x.startsWith("user:".toLowerCase())) { userSearch = x.substring("user:".length); }
+ else if (x.startsWith("u:".toLowerCase())) { userSearch = x.substring("u:".length); }
+ else if (x.startsWith("ip:".toLowerCase())) { ipSearch = x.substring("ip:".length); }
+ else if (x.startsWith("group:".toLowerCase())) { groupSearch = x.substring("group:".length); }
+ else if (x.startsWith("g:".toLowerCase())) { groupSearch = x.substring("g:".length); }
+ else if (x.startsWith("tag:".toLowerCase())) { tagSearch = x.substring("tag:".length); }
+ else if (x.startsWith("t:".toLowerCase())) { tagSearch = x.substring("t:".length); }
+ else if (x.startsWith("atag:".toLowerCase())) { agentTagSearch = x.substring("atag:".length); }
+ else if (x.startsWith("a:".toLowerCase())) { agentTagSearch = x.substring("a:".length); }
+ else if (x.startsWith("os:".toLowerCase())) { osSearch = x.substring("os:".length); }
+ else if (x.startsWith("amt:".toLowerCase())) { amtSearch = x.substring("amt:".length); }
+ else if (x.startsWith("desc:".toLowerCase())) { descSearch = x.substring("desc:".length); }
+ else if (x == 'wsc:ok') { wscSearch = 1; }
+ else if (x == 'wsc:noav') { wscSearch = 2; }
+ else if (x == 'wsc:noupdate') { wscSearch = 3; }
+ else if (x == 'wsc:nofirewall') { wscSearch = 4; }
+ else if (x == 'wsc:any') { wscSearch = 5; }
+
+ if (x == '') {
+ // No search
+ for (var d in nodes) { r.push(nodes[d]); }
+ } else if (ipSearch != null) {
+ // IP address search
+ for (var d in nodes) { if ((nodes[d].ip != null) && (nodes[d].ip.indexOf(ipSearch) >= 0)) { r.push(nodes[d]); } }
+ } else if (groupSearch != null) {
+ // Group filter
+ if (settings.xmeshes) { for (var d in nodes) { if (settings.xmeshes[nodes[d].meshid] && settings.xmeshes[nodes[d].meshid].name.toLowerCase().indexOf(groupSearch) >= 0) { r.push(nodes[d]); } } }
+ } else if (tagSearch != null) {
+ // Tag filter
+ for (var d in nodes) {
+ if ((nodes[d].tags == null) && (tagSearch == '')) { r.push(nodes[d]); }
+ else if (nodes[d].tags != null) { for (var j in nodes[d].tags) { if (nodes[d].tags[j].toLowerCase() == tagSearch) { r.push(nodes[d]); break; } } }
+ }
+ } else if (agentTagSearch != null) {
+ // Agent Tag filter
+ for (var d in nodes) {
+ if ((((nodes[d].agent != null) && (nodes[d].agent.tag == null)) && (agentTagSearch == '')) || ((nodes[d].agent != null) && (nodes[d].agent.tag != null) && (nodes[d].agent.tag.toLowerCase().indexOf(agentTagSearch) >= 0))) { r.push(nodes[d]); };
+ }
+ } else if (userSearch != null) {
+ // User search
+ for (var d in nodes) {
+ if (nodes[d].users && nodes[d].users.length > 0) { for (var i in nodes[d].users) { if (nodes[d].users[i].toLowerCase().indexOf(userSearch) >= 0) { r.push(nodes[d]); } } }
+ }
+ } else if (osSearch != null) {
+ // OS search
+ for (var d in nodes) { if ((nodes[d].osdesc != null) && (nodes[d].osdesc.toLowerCase().indexOf(osSearch) >= 0)) { r.push(nodes[d]); }; }
+ } else if (amtSearch != null) {
+ // Intel AMT search
+ for (var d in nodes) { if ((nodes[d].intelamt != null) && ((amtSearch == '') || (nodes[d].intelamt.state == amtSearch))) { r.push(nodes[d]); } }
+ } else if (descSearch != null) {
+ // Device description search
+ for (var d in nodes) { if ((nodes[d].desc != null) && (nodes[d].desc != '') && ((descSearch == '') || (nodes[d].desc.toLowerCase().indexOf(descSearch) >= 0))) { r.push(nodes[d]); } }
+ } else if (wscSearch != null) {
+ // Windows Security Center
+ for (var d in nodes) {
+ if (nodes[d].wsc) {
+ if ((wscSearch == 1) && (nodes[d].wsc.antiVirus == 'OK') && (nodes[d].wsc.autoUpdate == 'OK') && (nodes[d].wsc.firewall == 'OK')) { r.push(nodes[d]); }
+ else if (((wscSearch == 2) || (wscSearch == 5)) && (nodes[d].wsc.antiVirus != 'OK')) { r.push(nodes[d]); }
+ else if (((wscSearch == 3) || (wscSearch == 5)) && (nodes[d].wsc.autoUpdate != 'OK')) { r.push(nodes[d]); }
+ else if (((wscSearch == 4) || (wscSearch == 5)) && (nodes[d].wsc.firewall != 'OK')) { r.push(nodes[d]); }
+ }
+ }
+ } else if (x == '*') {
+ // Star filter
+ for (var d in nodes) { if (stars[nodes[d]._id] == 1) { r.push(nodes[d]); } }
+ } else {
+ // Device name search
+ try {
+ var rs = x.split(/\s+/).join('|'), rx = new RegExp(rs); // In some cases (like +), this can throw an exception.
+ for (var d in nodes) {
+ //if (showRealNames) {
+ //if (nodes[d].rnamel != null && rx.test(nodes[d].rnamel.toLowerCase())) { r.push(nodes[d]); }
+ //} else {
+ if (rx.test(nodes[d].name.toLowerCase())) { r.push(nodes[d]); }
+ //}
+ }
+ } catch (ex) { for (var d in nodes) { r.push(nodes[d]); } }
+ }
+
+ return r;
+}
+
+
+// Connect tunnel to a remote agent
+function connectTunnel(url) {
+ // Setup WebSocket options
+ var options = { rejectUnauthorized: false, checkServerIdentity: onVerifyServer }
+
+ // Setup the HTTP proxy if needed
+ if (args.proxy != null) { const HttpsProxyAgent = require('https-proxy-agent'); options.agent = new HttpsProxyAgent(require('url').parse(args.proxy)); }
+
+ // Connect the WebSocket
+ console.log('Connecting...');
+ const WebSocket = require('ws');
+ settings.tunnelwsstate = 0;
+ settings.tunnelws = new WebSocket(url, options);
+ settings.tunnelws.on('open', function () { console.log('Waiting for Agent...'); }); // Wait for agent connection
+ settings.tunnelws.on('close', function () { console.log('Connection Closed.'); process.exit(); });
+ settings.tunnelws.on('error', function (err) { console.log(err); process.exit(); });
+
+ if (settings.cmd == 'shell') {
+ // This code does all of the work for a shell command
+ settings.tunnelws.on('message', function (rawdata) {
+ var data = rawdata.toString();
+ if (settings.tunnelwsstate == 1) {
+ // If the incoming text looks exactly like a control command, ignore it.
+ if ((typeof data == 'string') && (data.startsWith('{"ctrlChannel":"102938","type":"'))) {
+ var ctrlCmd = null;
+ try { ctrlCmd = JSON.parse(data); } catch (ex) { }
+ if ((ctrlCmd != null) && (ctrlCmd.ctrlChannel == '102938') && (ctrlCmd.type != null)) return; // This is a control command, like ping/pong. Ignore it.
+ }
+ process.stdout.write(data);
+ } else if (settings.tunnelwsstate == 0) {
+ if (data == 'c') { console.log('Connected.'); } else if (data == 'cr') { console.log('Connected, session is being recorded.'); } else return;
+ // Send terminal size
+ var termSize = null;
+ if (typeof process.stdout.getWindowSize == 'function') { termSize = process.stdout.getWindowSize(); }
+ if (termSize != null) { settings.tunnelws.send(JSON.stringify({ ctrlChannel: '102938', type: 'options', cols: termSize[0], rows: termSize[1] })); }
+ settings.tunnelwsstate = 1;
+ settings.tunnelws.send('1'); // Terminal
+ process.stdin.setEncoding('utf8');
+ process.stdin.setRawMode(true);
+ process.stdout.setEncoding('utf8');
+ process.stdin.unpipe(process.stdout);
+ process.stdout.unpipe(process.stdin);
+ process.stdin.on('data', function (data) { settings.tunnelws.send(Buffer.from(data)); });
+ //process.stdin.on('readable', function () { var chunk; while ((chunk = process.stdin.read()) !== null) { settings.tunnelws.send(Buffer.from(chunk)); } });
+ process.stdin.on('end', function () { process.exit(); });
+ process.stdout.on('resize', function () {
+ var termSize = null;
+ if (typeof process.stdout.getWindowSize == 'function') { termSize = process.stdout.getWindowSize(); }
+ if (termSize != null) { settings.tunnelws.send(JSON.stringify({ ctrlChannel: '102938', type: 'termsize', cols: termSize[0], rows: termSize[1] })); }
+ });
+ }
+ });
+ } else if (settings.cmd == 'upload') {
+ // This code does all of the work for a file upload
+ // node meshctrl upload --id oL4Y6Eg0qjnpHFrp1AxfxnBPenbDGnDSkC@HSOnAheIyd51pKhqSCUgJZakzwfKl --file readme.md --target c:\
+ settings.tunnelws.on('message', function (rawdata) {
+ if (settings.tunnelwsstate == 1) {
+ var cmd = null;
+ try { cmd = JSON.parse(rawdata.toString()); } catch (ex) { return; }
+ if (cmd.reqid == 'up') {
+ if ((cmd.action == 'uploadack') || (cmd.action == 'uploadstart')) {
+ settings.inFlight--;
+ if (settings.uploadFile == null) { if (settings.inFlight == 0) { process.exit(); } return; } // If the file is closed and there is no more in-flight data, exit.
+ var loops = (cmd.action == 'uploadstart') ? 16 : 1; // If this is the first data to be sent, hot start now. We are going to have 16 blocks of data in-flight.
+ for (var i = 0; i < loops; i++) {
+ if (settings.uploadFile == null) continue;
+ var buf = Buffer.alloc(65565);
+ var len = require('fs').readSync(settings.uploadFile, buf, 1, 65564, settings.uploadPtr);
+ var start = 1;
+ settings.uploadPtr += len;
+ if (len > 0) {
+ if ((buf[1] == 0) || (buf[1] == 123)) { start = 0; buf[0] = 0; len++; } // If the buffer starts with 0 or 123, we must add an extra 0 at the start of the buffer
+ settings.inFlight++;
+ settings.tunnelws.send(buf.slice(start, start + len));
+ } else {
+ console.log('Upload done, ' + settings.uploadPtr + ' bytes sent.');
+ if (settings.uploadFile != null) { require('fs').closeSync(settings.uploadFile); delete settings.uploadFile; }
+ if (settings.inFlight == 0) { process.exit(); return; } // File is closed, if there is no more in-flight data, exit.
+ }
+ }
+
+ } else if (cmd.action == 'uploaderror') {
+ if (settings.uploadFile != null) { require('fs').closeSync(settings.uploadFile); }
+ console.log('Upload error.');
+ process.exit();
+ }
+ }
+ } else if (settings.tunnelwsstate == 0) {
+ var data = rawdata.toString();
+ if (data == 'c') { console.log('Connected.'); } else if (data == 'cr') { console.log('Connected, session is being recorded.'); } else return;
+ settings.tunnelwsstate = 1;
+ settings.tunnelws.send('5'); // Files
+ settings.uploadSize = require('fs').statSync(args.file).size;
+ settings.uploadFile = require('fs').openSync(args.file, 'r');
+ settings.uploadPtr = 0;
+ settings.inFlight = 1;
+ console.log('Uploading...');
+ settings.tunnelws.send(JSON.stringify({ action: 'upload', reqid: 'up', path: args.target, name: require('path').basename(args.file), size: settings.uploadSize }));
+ }
+ });
+ } else if (settings.cmd == 'download') {
+ // This code does all of the work for a file download
+ // node meshctrl download --id oL4Y6Eg0qjnpHFrp1AxfxnBPenbDGnDSkC@HSOnAheIyd51pKhqSCUgJZakzwfKl --file c:\temp\MC-8Languages.png --target c:\temp\bob.png
+ settings.tunnelws.on('message', function (rawdata) {
+ if (settings.tunnelwsstate == 1) {
+ if ((rawdata.length > 0) && (rawdata.toString()[0] != '{')) {
+ // This is binary data, this test is ok because 4 first bytes is a control value.
+ if ((rawdata.length > 4) && (settings.downloadFile != null)) { settings.downloadSize += (rawdata.length - 4); require('fs').writeSync(settings.downloadFile, rawdata, 4, rawdata.length - 4); }
+ if ((rawdata[3] & 1) != 0) { // Check end flag
+ // File is done, close everything.
+ if (settings.downloadFile != null) { require('fs').closeSync(settings.downloadFile); }
+ console.log('Download completed, ' + settings.downloadSize + ' bytes written.');
+ process.exit();
+ } else {
+ settings.tunnelws.send(JSON.stringify({ action: 'download', sub: 'ack', id: args.file })); // Send the ACK
+ }
+ } else {
+ // This is text data
+ var cmd = null;
+ try { cmd = JSON.parse(rawdata.toString()); } catch (ex) { return; }
+ if (cmd.action == 'download') {
+ if (cmd.id != args.file) return;
+ if (cmd.sub == 'start') {
+ if ((args.target.endsWith('\\')) || (args.target.endsWith('/'))) { args.target += path.parse(args.file).name; }
+ try { settings.downloadFile = require('fs').openSync(args.target, 'w'); } catch (ex) { console.log("Unable to create file: " + args.target); process.exit(); return; }
+ settings.downloadSize = 0;
+ settings.tunnelws.send(JSON.stringify({ action: 'download', sub: 'startack', id: args.file }));
+ console.log('Download started: ' + args.target);
+ } else if (cmd.sub == 'cancel') {
+ if (settings.downloadFile != null) { require('fs').closeSync(settings.downloadFile); }
+ console.log('Download canceled.');
+ process.exit();
+ }
+ }
+ }
+ } else if (settings.tunnelwsstate == 0) {
+ var data = rawdata.toString();
+ if (data == 'c') { console.log('Connected.'); } else if (data == 'cr') { console.log('Connected, session is being recorded.'); } else return;
+ settings.tunnelwsstate = 1;
+ settings.tunnelws.send('5'); // Files
+ settings.tunnelws.send(JSON.stringify({ action: 'download', sub: 'start', id: args.file, path: args.file }));
+ }
+ });
+ }
+}
+
+// Encode an object as a cookie using a key using AES-GCM. (key must be 32 bytes or more)
+function encodeCookie(o, key) {
+ try {
+ if (key == null) { return null; }
+ o.time = Math.floor(Date.now() / 1000); // Add the cookie creation time
+ const iv = Buffer.from(crypto.randomBytes(12), 'binary'), cipher = crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv);
+ const crypted = Buffer.concat([cipher.update(JSON.stringify(o), 'utf8'), cipher.final()]);
+ return Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
+ } catch (e) { return null; }
+}
+
+// Generate a random Intel AMT password
+function checkAmtPassword(p) { return (p.length > 7) && (/\d/.test(p)) && (/[a-z]/.test(p)) && (/[A-Z]/.test(p)) && (/\W/.test(p)); }
+function getRandomAmtPassword() { var p; do { p = Buffer.from(crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); } while (checkAmtPassword(p) == false); return p; }
+function getRandomHex(count) { return Buffer.from(crypto.randomBytes(count), 'binary').toString('hex'); }
+function format(format) { var args = Array.prototype.slice.call(arguments, 1); return format.replace(/{(\d+)}/g, function (match, number) { return typeof args[number] != 'undefined' ? args[number] : match; }); };
+function winRemoveSingleQuotes(str) { if (process.platform != 'win32') return str; else return str.split('\'').join(''); }
+
+function csvFormatArray(x) {
+ var y = [];
+ for (var i in x) { if ((x[i] == null) || (x[i] == '')) { y.push(''); } else { y.push('"' + x[i].split('"').join('') + '"'); } }
+ return y.join(',');
+}
+
+function displayDeviceInfo(sysinfo, lastconnect, network, nodes) {
+ //console.log('displayDeviceInfo', sysinfo, lastconnect, network, nodes);
+
+ // Fetch the node information
+ var node = null;;
+ if (sysinfo != null && (sysinfo.node != null)) {
+ // Node information came with system information
+ node = sysinfo.node;
+ } else {
+ // This device does not have system information, get node information from the nodes list.
+ for (var m in nodes.nodes) {
+ for (var n in nodes.nodes[m]) {
+ if (nodes.nodes[m][n]._id.indexOf(args.id) >= 0) { node = nodes.nodes[m][n]; }
+ }
+ }
+ }
+ if ((sysinfo == null && lastconnect == null && network == null) || (node == null)) {
+ console.log("Invalid device id");
+ process.exit(); return;
+ }
+
+ var info = {};
+
+ //if (network != null) { sysinfo.netif = network.netif; }
+ if (lastconnect != null) { node.lastconnect = lastconnect.time; node.lastaddr = lastconnect.addr; }
+ if (args.raw) { console.log(JSON.stringify(sysinfo, ' ', 2)); return; }
+
+ // General
+ var output = {}, outputCount = 0;
+ if (node.name) { output["Server Name"] = node.name; outputCount++; }
+ if (node.rname) { output["Computer Name"] = node.rname; outputCount++; }
+ if (node.host != null) { output["Hostname"] = node.host; outputCount++; }
+ if (node.ip != null) { output["IP Address"] = node.ip; outputCount++; }
+ if (node.desc != null) { output["Description"] = node.desc; outputCount++; }
+ if (node.icon != null) { output["Icon"] = node.icon; outputCount++; }
+ if (node.tags) { output["Tags"] = node.tags; outputCount++; }
+ if (node.av) {
+ var av = [];
+ for (var i in node.av) {
+ if (typeof node.av[i]['product'] == 'string') {
+ var n = node.av[i]['product'];
+ if (node.av[i]['updated'] === true) { n += ', updated'; }
+ if (node.av[i]['updated'] === false) { n += ', not updated'; }
+ if (node.av[i]['enabled'] === true) { n += ', enabled'; }
+ if (node.av[i]['enabled'] === false) { n += ', disabled'; }
+ av.push(n);
+ }
+ }
+ output["AntiVirus"] = av; outputCount++;
+ }
+ if (typeof node.wsc == 'object') {
+ output["WindowsSecurityCenter"] = node.wsc; outputCount++;
+ }
+ if (outputCount > 0) { info["General"] = output; }
+
+ // Operating System
+ var hardware = null;
+ if ((sysinfo != null) && (sysinfo.hardware != null)) { hardware = sysinfo.hardware; }
+ if ((hardware && hardware.windows && hardware.windows.osinfo) || node.osdesc) {
+ var output = {}, outputCount = 0;
+ if (node.rname) { output["Name"] = node.rname; outputCount++; }
+ if (node.osdesc) { output["Version"] = node.osdesc; outputCount++; }
+ if (hardware && hardware.windows && hardware.windows.osinfo) { var m = hardware.windows.osinfo; if (m.OSArchitecture) { output["Architecture"] = m.OSArchitecture; outputCount++; } }
+ if (outputCount > 0) { info["Operating System"] = output; }
+ }
+
+ // MeshAgent
+ if (node.agent) {
+ var output = {}, outputCount = 0;
+ var agentsStr = ["Unknown", "Windows 32bit console", "Windows 64bit console", "Windows 32bit service", "Windows 64bit service", "Linux 32bit", "Linux 64bit", "MIPS", "XENx86", "Android", "Linux ARM", "macOS x86-32bit", "Android x86", "PogoPlug ARM", "Android", "Linux Poky x86-32bit", "macOS x86-64bit", "ChromeOS", "Linux Poky x86-64bit", "Linux NoKVM x86-32bit", "Linux NoKVM x86-64bit", "Windows MinCore console", "Windows MinCore service", "NodeJS", "ARM-Linaro", "ARMv6l / ARMv7l", "ARMv8 64bit", "ARMv6l / ARMv7l / NoKVM", "MIPS24KC (OpenWRT)", "Apple Silicon", "FreeBSD x86-64", "Unknown", "Linux ARM 64 bit (glibc/2.24 NOKVM)", "Alpine Linux x86 64 Bit (MUSL)", "Assistant (Windows)", "Armada370 - ARM32/HF (libc/2.26)", "OpenWRT x86-64", "OpenBSD x86-64", "Unknown", "Unknown", "MIPSEL24KC (OpenWRT)", "ARMADA/CORTEX-A53/MUSL (OpenWRT)", "Windows ARM 64bit console", "Windows ARM 64bit service", "ARMVIRT32 (OpenWRT)", "RISC-V x86-64"];
+ if ((node.agent != null) && (node.agent.id != null) && (node.agent.ver != null)) {
+ var str = '';
+ if (node.agent.id <= agentsStr.length) { str = agentsStr[node.agent.id]; } else { str = agentsStr[0]; }
+ if (node.agent.ver != 0) { str += ' v' + node.agent.ver; }
+ output["Mesh Agent"] = str; outputCount++;
+ }
+ if ((node.conn & 1) != 0) {
+ output["Last agent connection"] = "Connected now"; outputCount++;
+ } else {
+ if (node.lastconnect) { output["Last agent connection"] = new Date(node.lastconnect).toLocaleString(); outputCount++; }
+ }
+ if (node.lastaddr) {
+ var splitip = node.lastaddr.split(':');
+ if (splitip.length > 2) {
+ output["Last agent address"] = node.lastaddr; outputCount++; // IPv6
+ } else {
+ output["Last agent address"] = splitip[0]; outputCount++; // IPv4
+ }
+ }
+ if ((node.agent != null) && (node.agent.tag != null)) {
+ output["Tag"] = node.agent.tag; outputCount++;
+ }
+ if (outputCount > 0) { info["Mesh Agent"] = output; }
+ }
+
+ // Networking
+ if (network.netif != null) {
+ var output = {}, outputCount = 0, minfo = {};
+ for (var i in network.netif) {
+ var m = network.netif[i], moutput = {}, moutputCount = 0;
+ if (m.desc) { moutput["Description"] = m.desc; moutputCount++; }
+ if (m.mac) {
+ if (m.gatewaymac) {
+ moutput["MAC Layer"] = format("MAC: {0}, Gateway: {1}", m.mac, m.gatewaymac); moutputCount++;
+ } else {
+ moutput["MAC Layer"] = format("MAC: {0}", m.mac); moutputCount++;
+ }
+ }
+ if (m.v4addr && (m.v4addr != '0.0.0.0')) {
+ if (m.v4gateway && m.v4mask) {
+ moutput["IPv4 Layer"] = format("IP: {0}, Mask: {1}, Gateway: {2}", m.v4addr, m.v4mask, m.v4gateway); moutputCount++;
+ } else {
+ moutput["IPv4 Layer"] = format("IP: {0}", m.v4addr); moutputCount++;
+ }
+ }
+ if (moutputCount > 0) { minfo[m.name + (m.dnssuffix ? (', ' + m.dnssuffix) : '')] = moutput; info["Networking"] = minfo; }
+ }
+ }
+
+ if (network.netif2 != null) {
+ var minfo = {};
+ for (var i in network.netif2) {
+ var m = network.netif2[i], moutput = {}, moutputCount = 0;
+
+ if (Array.isArray(m) == false ||
+ m.length < 1 ||
+ m[0] == null ||
+ ((typeof m[0].mac == 'string') && (m[0].mac.startsWith('00:00:00:00')))
+ )
+ continue;
+
+ var ifTitle = '' + i;
+ if (m[0].fqdn != null && m[0].fqdn != '') ifTitle += ', ' + m[0].fqdn;
+
+ if (typeof m[0].mac == 'string') {
+ if (m[0].gatewaymac) {
+ moutput['MAC Layer'] = format("MAC: {0}, Gateway: {1}", m[0].mac, m[0].gatewaymac);
+ } else {
+ moutput['MAC Layer'] = format("MAC: {0}", m[0].mac);
+ }
+ moutputCount++;
+ }
+
+ moutput['IPv4 Layer'] = '';
+ moutput['IPv6 Layer'] = '';
+ for (var j = 0; j < m.length; j++) {
+ var iplayer = m[j];
+ if (iplayer.family == 'IPv4' || iplayer.family == 'IPv6') {
+ if (iplayer.gateway && iplayer.netmask) {
+ moutput[iplayer.family + ' Layer'] += format("IP: {0}, Mask: {1}, Gateway: {2} ", iplayer.address, iplayer.netmask, iplayer.gateway);
+ moutputCount++;
+ } else {
+ if (iplayer.address) {
+ moutput[iplayer.family + ' Layer'] += format("IP: {0} ", iplayer.address);
+ moutputCount++;
+ }
+ }
+ }
+ }
+ if (moutput['IPv4 Layer'] == '') delete moutput['IPv4 Layer'];
+ if (moutput['IPv6 Layer'] == '') delete moutput['IPv6 Layer'];
+ if (moutputCount > 0) {
+ minfo[ifTitle] = moutput;
+ info["Networking"] = minfo;
+ }
+ }
+ }
+
+ // Intel AMT
+ if (node.intelamt != null) {
+ var output = {}, outputCount = 0;
+ output["Version"] = (node.intelamt.ver) ? ('v' + node.intelamt.ver) : ('' + "Unknown" + ''); outputCount++;
+ var provisioningStates = { 0: "Not Activated (Pre)", 1: "Not Activated (In)", 2: "Activated" };
+ var provisioningMode = '';
+ if ((node.intelamt.state == 2) && node.intelamt.flags) { if (node.intelamt.flags & 2) { provisioningMode = (', ' + "Client Control Mode (CCM)"); } else if (node.intelamt.flags & 4) { provisioningMode = (', ' + "Admin Control Mode (ACM)"); } }
+ output["Provisioning State"] = ((node.intelamt.state) ? (provisioningStates[node.intelamt.state]) : ('' + "Unknown" + '')) + provisioningMode; outputCount++;
+ output["Security"] = (node.intelamt.tls == 1) ? "Secured using TLS" : "TLS is not setup"; outputCount++;
+ output["Admin Credentials"] = (node.intelamt.user == null || node.intelamt.user == '') ? "Not Known" : "Known"; outputCount++;
+ if (outputCount > 0) { info["Intel Active Management Technology (Intel AMT)"] = output; }
+ }
+
+ if (hardware != null) {
+ if (hardware.identifiers) {
+ var output = {}, outputCount = 0, ident = hardware.identifiers;
+ // BIOS
+ if (ident.bios_vendor) { output["Vendor"] = ident.bios_vendor; outputCount++; }
+ if (ident.bios_version) { output["Version"] = ident.bios_version; outputCount++; }
+ if (outputCount > 0) { info["BIOS"] = output; }
+ output = {}, outputCount = 0;
+
+ // Motherboard
+ if (ident.board_vendor) { output["Vendor"] = ident.board_vendor; outputCount++; }
+ if (ident.board_name) { output["Name"] = ident.board_name; outputCount++; }
+ if (ident.board_serial && (ident.board_serial != '')) { output["Serial"] = ident.board_serial; outputCount++; }
+ if (ident.board_version) { output["Version"] = ident.board_version; }
+ if (ident.product_uuid) { output["Identifier"] = ident.product_uuid; }
+ if (ident.cpu_name) { output["CPU"] = ident.cpu_name; }
+ if (ident.gpu_name) { for (var i in ident.gpu_name) { output["GPU" + (parseInt(i) + 1)] = ident.gpu_name[i]; } }
+ if (outputCount > 0) { info["Motherboard"] = output; }
+ }
+
+ // Memory
+ if (hardware.windows) {
+ if (hardware.windows.memory) {
+ var output = {}, outputCount = 0, minfo = {};
+ hardware.windows.memory.sort(function (a, b) { if (a.BankLabel > b.BankLabel) return 1; if (a.BankLabel < b.BankLabel) return -1; return 0; });
+ for (var i in hardware.windows.memory) {
+ var m = hardware.windows.memory[i], moutput = {}, moutputCount = 0;
+ if (m.Capacity) { moutput["Capacity/Speed"] = (m.Capacity / 1024 / 1024) + " Mb, " + m.Speed + " Mhz"; moutputCount++; }
+ if (m.PartNumber) { moutput["Part Number"] = ((m.Manufacturer && m.Manufacturer != 'Undefined') ? (m.Manufacturer + ', ') : '') + m.PartNumber; moutputCount++; }
+ if (moutputCount > 0) { minfo[m.BankLabel] = moutput; info["Memory"] = minfo; }
+ }
+ }
+ }
+
+ // Storage
+ if (hardware.identifiers && ident.storage_devices) {
+ var output = {}, outputCount = 0, minfo = {};
+ // Sort Storage
+ ident.storage_devices.sort(function (a, b) { if (a.Caption > b.Caption) return 1; if (a.Caption < b.Caption) return -1; return 0; });
+ for (var i in ident.storage_devices) {
+ var m = ident.storage_devices[i], moutput = {};
+ if (m.Size) {
+ if (m.Model && (m.Model != m.Caption)) { moutput["Model"] = m.Model; outputCount++; }
+ if ((typeof m.Size == 'string') && (parseInt(m.Size) == m.Size)) { m.Size = parseInt(m.Size); }
+ if (typeof m.Size == 'number') { moutput["Capacity"] = Math.floor(m.Size / 1024 / 1024) + 'Mb'; outputCount++; }
+ if (typeof m.Size == 'string') { moutput["Capacity"] = m.Size; outputCount++; }
+ if (moutputCount > 0) { minfo[m.Caption] = moutput; info["Storage"] = minfo; }
+ }
+ }
+ }
+ }
+
+ // Display everything
+ if (args.json) {
+ console.log(JSON.stringify(info, ' ', 2));
+ } else {
+ for (var i in info) {
+ console.log('--- ' + i + ' ---');
+ for (var j in info[i]) {
+ if ((typeof info[i][j] == 'string') || (typeof info[i][j] == 'number')) {
+ console.log(' ' + j + ': ' + info[i][j]);
+ } else {
+ console.log(' ' + j + ':');
+ for (var k in info[i][j]) {
+ console.log(' ' + k + ': ' + info[i][j][k]);
+ }
+ }
+ }
+ }
+ }
+}
+
+// Read the Mesh Agent error log and index it.
+function indexAgentErrorLog() {
+ // Index the messages
+ const lines = require('fs').readFileSync('../meshcentral-data/agenterrorlogs.txt', { encoding: 'utf8', flag: 'r' }).split('\r\n');
+ var errorIndex = {}; // "msg" --> [ { lineNumber, elemenetNumber } ]
+ for (var i = 0; i < lines.length; i++) {
+ const line = lines[i];
+ if (line.length > 88) {
+ var nodeid = line.substring(0, 70);
+ var fetchTime = parseInt(line.substring(72, 85));
+ var data = JSON.parse(line.substring(87));
+ if ((data != null) && (data.action == 'errorlog') && (Array.isArray(data.log))) {
+ for (var j = 0; j < data.log.length; j++) {
+ var entry = data.log[j];
+ if ((entry != null) && (typeof entry.t == 'number') && (typeof entry.m == 'string')) {
+ const msg = entry.m;
+ if (errorIndex[msg] == null) { errorIndex[msg] = []; }
+ errorIndex[msg].push({ l: i, e: j });
+ }
+ }
+ }
+ }
+ }
+
+ // Sort the messages by frequency
+ var errorIndexCount = []; // [ { m: "msg", c: count } ]
+ for (var i in errorIndex) { errorIndexCount.push({ m: i, c: errorIndex[i].length }); }
+ errorIndexCount = errorIndexCount.sort(function (a, b) { return b.c - a.c })
+
+ // Display the results
+ for (var i = 0; i < errorIndexCount.length; i++) {
+ const m = errorIndexCount[i].m;
+ if ((m.indexOf('STUCK') >= 0) || (m.indexOf('FATAL') >= 0)) { console.log(errorIndexCount[i].c, m); }
+ }
+}
From 011d5a2fc2b1c7610930b53509001f93c319d7e5 Mon Sep 17 00:00:00 2001
From: stephannn <38951729+stephannn@users.noreply.github.com>
Date: Mon, 13 Oct 2025 15:41:56 +0200
Subject: [PATCH 6/8] Add files via upload
---
meshagent.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/meshagent.js b/meshagent.js
index 04d88bf970..70cda5aaea 100644
--- a/meshagent.js
+++ b/meshagent.js
@@ -761,7 +761,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
// Device does not exist with the name
nodeExists = false;
} else {
- console.log("Device already exists");
+ // Device exists with the name
// Remove nodes with the same name
existingNodes.forEach((eNode) => {
From 621f3bf31bb63f488b0745adb28c15dbab4eda2e Mon Sep 17 00:00:00 2001
From: stephannn <38951729+stephannn@users.noreply.github.com>
Date: Tue, 14 Oct 2025 13:09:10 +0200
Subject: [PATCH 7/8] Add files via upload
---
db.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/db.js b/db.js
index 05a1d487d4..36ec01bfc8 100644
--- a/db.js
+++ b/db.js
@@ -1657,7 +1657,7 @@ module.exports.CreateDB = function (parent, func) {
});
}
obj.GetNodeByComputerName = function (domain, rname, func) {
- sqlDbQuery('SELECT doc FROM main WHERE type = $1 AND domain = $2 AND JSON_EXTRACT(doc, "$.rname") = $3 ORDER BY lastbootuptime',
+ sqlDbQuery('SELECT doc FROM main WHERE type = $1 AND domain = $2 AND JSON_EXTRACT(doc, "$.rname") = $3',
['node', domain, rname], function (err, docs) {
func(err, performTypedRecordDecrypt(docs));
});
@@ -2220,7 +2220,7 @@ module.exports.CreateDB = function (parent, func) {
obj.GetAllIdsOfType = function (ids, domain, type, func) { sqlDbQuery('SELECT doc FROM main WHERE (id = ANY ($1)) AND domain = $2 AND type = $3', [ids, domain, type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
obj.GetUserWithEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extra = $2', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
obj.GetUserWithVerifiedEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extra = $2', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetNodeByComputerName = function (domain, rname, func) { sqlDbQuery('SELECT doc FROM main WHERE type = "$1" AND domain = $2 AND doc->>\'rname\' = $3 ORDER BY lastbootuptime', ['node', domain, rname], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });};
+ obj.GetNodeByComputerName = function (domain, rname, func) { sqlDbQuery('SELECT doc FROM main WHERE type = $1 AND domain = $2 AND doc->>\'rname\' = $3', ['node', domain, rname], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });};
obj.Remove = function (id, func) { sqlDbQuery('DELETE FROM main WHERE id = $1', [id], func); };
obj.RemoveAll = function (func) { sqlDbQuery('DELETE FROM main', null, func); };
obj.RemoveAllOfType = function (type, func) { sqlDbQuery('DELETE FROM main WHERE type = $1', [type], func); };
@@ -2481,7 +2481,7 @@ module.exports.CreateDB = function (parent, func) {
}
obj.GetUserWithEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = ? AND extra = ?', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
obj.GetUserWithVerifiedEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = ? AND extra = ?', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetNodeByComputerName = function (domain, rname, func) { sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND JSON_EXTRACT(doc, "$.rname") = ? ORDER BY lastbootuptime', ['node', domain, rname], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetNodeByComputerName = function (domain, rname, func) { sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND JSON_EXTRACT(doc, "$.rname") = ?', ['node', domain, rname], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
obj.Remove = function (id, func) { sqlDbQuery('DELETE FROM main WHERE id = ?', [id], func); };
obj.RemoveAll = function (func) { sqlDbQuery('DELETE FROM main', null, func); };
obj.RemoveAllOfType = function (type, func) { sqlDbQuery('DELETE FROM main WHERE type = ?', [type], func); };
From 4e160f2d7e0591eb9bc07dad278605a6fbd8e156 Mon Sep 17 00:00:00 2001
From: stephannn <38951729+stephannn@users.noreply.github.com>
Date: Sat, 1 Nov 2025 14:34:16 +0100
Subject: [PATCH 8/8] Add files via upload
---
db.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/db.js b/db.js
index 36ec01bfc8..5790e24206 100644
--- a/db.js
+++ b/db.js
@@ -2386,6 +2386,7 @@ module.exports.CreateDB = function (parent, func) {
obj.getPowerTimeline = function (nodeid, func) { sqlDbQuery('SELECT doc FROM power WHERE ((nodeid = $1) OR (nodeid = \'*\')) ORDER BY time ASC', [nodeid], func); };
obj.removeAllPowerEvents = function () { sqlDbQuery('DELETE FROM power', null, function (err, docs) { }); };
obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; sqlDbQuery('DELETE FROM power WHERE nodeid = $1', [nodeid], function (err, docs) { }); };
+ obj.moveAllPowerEventsToNode = function (nodeid, oldnodeid) { if (nodeid == null) return; sqlDbQuery('UPDATE SET nodeid = $1 FROM power WHERE nodeid = $2', [nodeid, oldnodeid], function (err, docs) { }); };
// Database actions on the SMBIOS collection
obj.GetAllSMBIOS = function (func) { sqlDbQuery('SELECT doc FROM smbios', null, func); };
@@ -2481,7 +2482,7 @@ module.exports.CreateDB = function (parent, func) {
}
obj.GetUserWithEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = ? AND extra = ?', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
obj.GetUserWithVerifiedEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = ? AND extra = ?', [domain, 'email/' + email], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
- obj.GetNodeByComputerName = function (domain, rname, func) { sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND JSON_EXTRACT(doc, "$.rname") = ?', ['node', domain, rname], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
+ obj.GetNodeByComputerName = function (domain, rname, func) { sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND rname = ?', ['node', domain, rname], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
obj.Remove = function (id, func) { sqlDbQuery('DELETE FROM main WHERE id = ?', [id], func); };
obj.RemoveAll = function (func) { sqlDbQuery('DELETE FROM main', null, func); };
obj.RemoveAllOfType = function (type, func) { sqlDbQuery('DELETE FROM main WHERE type = ?', [type], func); };