master
js 4,297 lines 298 KB
Raw
1 /**
2 * @description MeshCentral database module
3 * @author Ylian Saint-Hilaire
4 * @copyright Intel Corporation 2018-2022
5 * @license Apache-2.0
6 * @version v0.0.2
7 */
8
9 /*xjslint node: true */
10 /*xjslint plusplus: true */
11 /*xjslint maxlen: 256 */
12 /*jshint node: true */
13 /*jshint strict: false */
14 /*jshint esversion: 6 */
15 "use strict";
16
17 //
18 // Construct Meshcentral database object
19 //
20 // The default database is NeDB
21 // https://github.com/louischatriot/nedb
22 //
23 // Alternativety, MongoDB can be used
24 // https://www.mongodb.com/
25 // Just run with --mongodb [connectionstring], where the connection string is documented here: https://docs.mongodb.com/manual/reference/connection-string/
26 // The default collection is "meshcentral", but you can override it using --mongodbcol [collection]
27 //
28 module.exports.CreateDB = function (parent, func) {
29 var obj = {};
30 var Datastore = null;
31 var expireEventsSeconds = (60 * 60 * 24 * 20); // By default, expire events after 20 days (1728000). (Seconds * Minutes * Hours * Days)
32 var expirePowerEventsSeconds = (60 * 60 * 24 * 10); // By default, expire power events after 10 days (864000). (Seconds * Minutes * Hours * Days)
33 var expireServerStatsSeconds = (60 * 60 * 24 * 30); // By default, expire server stats after 30 days (2592000). (Seconds * Minutes * Hours * Days)
34 const common = require('./common.js');
35 const path = require('path');
36 const fs = require('fs');
37 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;
38 const DB_LIST = ['None', 'NeDB', 'MongoJS', 'MongoDB', 'MariaDB', 'MySQL', 'PostgreSQL', 'AceBase', 'SQLite']; //for the info command
39 let databaseName = 'meshcentral';
40 let datapathParentPath = path.dirname(parent.datapath);
41 let datapathFoldername = path.basename(parent.datapath);
42 const SQLITE_AUTOVACUUM = ['none', 'full', 'incremental'];
43 const SQLITE_SYNCHRONOUS = ['off', 'normal', 'full', 'extra'];
44 obj.sqliteConfig = {
45 maintenance: '',
46 startupVacuum: false,
47 autoVacuum: 'full',
48 incrementalVacuum: 100,
49 journalMode: 'delete',
50 journalSize: 4096000,
51 synchronous: 'full',
52 };
53 obj.performingBackup = false;
54 const BACKUPFAIL_ZIPCREATE = 0x0001;
55 const BACKUPFAIL_ZIPMODULE = 0x0010;
56 const BACKUPFAIL_DBDUMP = 0x0100;
57 obj.backupStatus = 0x0;
58 obj.newAutoBackupFile = null;
59 obj.newDBDumpFile = null;
60 obj.identifier = null;
61 obj.dbKey = null;
62 obj.dbRecordsEncryptKey = null;
63 obj.dbRecordsDecryptKey = null;
64 obj.changeStream = false;
65 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)));
66 obj.dbCounters = {
67 fileSet: 0,
68 fileRemove: 0,
69 powerSet: 0,
70 eventsSet: 0
71 }
72
73 // MongoDB bulk operations state
74 if (parent.config.settings.mongodbbulkoperations) {
75 // Added counters
76 obj.dbCounters.fileSetPending = 0;
77 obj.dbCounters.fileSetBulk = 0;
78 obj.dbCounters.fileRemovePending = 0;
79 obj.dbCounters.fileRemoveBulk = 0;
80 obj.dbCounters.powerSetPending = 0;
81 obj.dbCounters.powerSetBulk = 0;
82 obj.dbCounters.eventsSetPending = 0;
83 obj.dbCounters.eventsSetBulk = 0;
84
85 /// Added bulk accumulators
86 obj.filePendingGet = null;
87 obj.filePendingGets = null;
88 obj.filePendingRemove = null;
89 obj.filePendingRemoves = null;
90 obj.filePendingSet = false;
91 obj.filePendingSets = null;
92 obj.filePendingCb = null;
93 obj.filePendingCbs = null;
94 obj.powerFilePendingSet = false;
95 obj.powerFilePendingSets = null;
96 obj.powerFilePendingCb = null;
97 obj.powerFilePendingCbs = null;
98 obj.eventsFilePendingSet = false;
99 obj.eventsFilePendingSets = null;
100 obj.eventsFilePendingCb = null;
101 obj.eventsFilePendingCbs = null;
102 }
103
104 obj.SetupDatabase = function (func) {
105 // Check if the database unique identifier is present
106 // This is used to check that in server peering mode, everyone is using the same database.
107 obj.Get('DatabaseIdentifier', function (err, docs) {
108 if (err != null) { parent.debug('db', 'ERROR (Get DatabaseIdentifier): ' + err); }
109 if ((err == null) && (docs.length == 1) && (docs[0].value != null)) {
110 obj.identifier = docs[0].value;
111 } else {
112 obj.identifier = Buffer.from(require('crypto').randomBytes(48), 'binary').toString('hex');
113 obj.Set({ _id: 'DatabaseIdentifier', value: obj.identifier });
114 }
115 });
116
117 // Load database schema version and check if we need to update
118 obj.Get('SchemaVersion', function (err, docs) {
119 if (err != null) { parent.debug('db', 'ERROR (Get SchemaVersion): ' + err); }
120 var ver = 0;
121 if ((err == null) && (docs.length == 1)) { ver = docs[0].value; }
122 if (ver == 1) { console.log('This is an unsupported beta 1 database, delete it to create a new one.'); process.exit(0); }
123
124 // TODO: Any schema upgrades here...
125 obj.Set({ _id: 'SchemaVersion', value: 2 });
126
127 func(ver);
128 });
129 };
130
131 // Perform database maintenance
132 obj.maintenance = function () {
133 parent.debug('db', 'Entering database maintenance');
134 if (obj.databaseType == DB_NEDB) { // NeDB will not remove expired records unless we try to access them. This will force the removal.
135 obj.eventsfile.remove({ time: { '$lt': new Date(Date.now() - (expireEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
136 obj.powerfile.remove({ time: { '$lt': new Date(Date.now() - (expirePowerEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
137 obj.serverstatsfile.remove({ time: { '$lt': new Date(Date.now() - (expireServerStatsSeconds * 1000)) } }, { multi: true }); // Force delete older events
138 } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) { // MariaDB or MySQL
139 sqlDbQuery('DELETE FROM events WHERE time < ?', [new Date(Date.now() - (expireEventsSeconds * 1000))], function (doc, err) { }); // Delete events older than expireEventsSeconds
140 sqlDbQuery('DELETE FROM power WHERE time < ?', [new Date(Date.now() - (expirePowerEventsSeconds * 1000))], function (doc, err) { }); // Delete events older than expirePowerSeconds
141 sqlDbQuery('DELETE FROM serverstats WHERE expire < ?', [new Date()], function (doc, err) { }); // Delete events where expiration date is in the past
142 sqlDbQuery('DELETE FROM smbios WHERE expire < ?', [new Date()], function (doc, err) { }); // Delete events where expiration date is in the past
143 } else if (obj.databaseType == DB_POSTGRESQL) { // PostgreSQL
144 sqlDbQuery('DELETE FROM events WHERE time < $1', [new Date(Date.now() - (expireEventsSeconds * 1000))], function (doc, err) { }); // Delete events older than expireEventsSeconds
145 sqlDbQuery('DELETE FROM power WHERE time < $1', [new Date(Date.now() - (expirePowerEventsSeconds * 1000))], function (doc, err) { }); // Delete events older than expirePowerSeconds
146 sqlDbQuery('DELETE FROM serverstats WHERE time < $1', [new Date(Date.now() - (expireServerStatsSeconds * 1000))], function (doc, err) { }); // Delete server stats older than expireServerStatsSeconds
147 sqlDbQuery('DELETE FROM smbios WHERE expire < $1', [new Date()], function (doc, err) { }); // Delete SMBIOS records where expiration date is in the past
148 } else if (obj.databaseType == DB_ACEBASE) { // AceBase
149 //console.log('Performing AceBase maintenance');
150 obj.file.query('events').filter('time', '<', new Date(Date.now() - (expireEventsSeconds * 1000))).remove().then(function () {
151 obj.file.query('stats').filter('time', '<', new Date(Date.now() - (expireServerStatsSeconds * 1000))).remove().then(function () {
152 obj.file.query('power').filter('time', '<', new Date(Date.now() - (expirePowerEventsSeconds * 1000))).remove().then(function () {
153 //console.log('AceBase maintenance done');
154 });
155 });
156 });
157 } else if (obj.databaseType == DB_SQLITE) { // SQLite3
158 //sqlite does not return rows affected for INSERT, UPDATE or DELETE statements, see https://www.sqlite.org/pragma.html#pragma_count_changes
159 obj.file.serialize(function () {
160 obj.file.run('DELETE FROM events WHERE time < ?', [new Date(Date.now() - (expireEventsSeconds * 1000))]);
161 obj.file.run('DELETE FROM power WHERE time < ?', [new Date(Date.now() - (expirePowerEventsSeconds * 1000))]);
162 obj.file.run('DELETE FROM serverstats WHERE expire < ?', [new Date()]);
163 obj.file.run('DELETE FROM smbios WHERE expire < ?', [new Date()]);
164 obj.file.exec(obj.sqliteConfig.maintenance, function (err) {
165 if (err) {console.log('Maintenance error: ' + err.message)};
166 if (parent.config.settings.debug) {
167 sqliteGetPragmas(['freelist_count', 'page_size', 'page_count', 'cache_size' ], function (pragma, pragmaValue) {
168 parent.debug('db', 'SQLite Maintenance: ' + pragma + '=' + pragmaValue);
169 });
170 };
171 });
172 });
173 }
174 obj.removeInactiveDevices();
175 }
176
177 // Remove inactive devices
178 obj.removeInactiveDevices = function (showall, cb) {
179 // Get a list of domains and what their inactive device removal setting is
180 var removeInactiveDevicesPerDomain = {}, minRemoveInactiveDevicesPerDomain = {}, minRemoveInactiveDevice = 9999;
181 for (var i in parent.config.domains) {
182 if (typeof parent.config.domains[i].autoremoveinactivedevices == 'number') {
183 var v = parent.config.domains[i].autoremoveinactivedevices;
184 if ((v >= 1) && (v <= 2000)) {
185 if (v < minRemoveInactiveDevice) { minRemoveInactiveDevice = v; }
186 removeInactiveDevicesPerDomain[i] = v;
187 minRemoveInactiveDevicesPerDomain[i] = v;
188 }
189 }
190 }
191
192 // Check if any device groups have a inactive device removal setting
193 for (var i in parent.webserver.meshes) {
194 if (typeof parent.webserver.meshes[i].expireDevs == 'number') {
195 var v = parent.webserver.meshes[i].expireDevs;
196 if ((v >= 1) && (v <= 2000)) {
197 if (v < minRemoveInactiveDevice) { minRemoveInactiveDevice = v; }
198 if ((minRemoveInactiveDevicesPerDomain[parent.webserver.meshes[i].domain] == null) || (minRemoveInactiveDevicesPerDomain[parent.webserver.meshes[i].domain] > v)) {
199 minRemoveInactiveDevicesPerDomain[parent.webserver.meshes[i].domain] = v;
200 }
201 } else {
202 delete parent.webserver.meshes[i].expireDevs;
203 }
204 }
205 }
206
207 // If there are no such settings for any domain, we can exit now.
208 if (minRemoveInactiveDevice == 9999) { if (cb) { cb("No device removal policy set, nothing to do."); } return; }
209 const now = Date.now();
210
211 // For each domain with a inactive device removal setting, get a list of last device connections
212 for (var domainid in minRemoveInactiveDevicesPerDomain) {
213 obj.GetAllTypeNoTypeField('lastconnect', domainid, function (err, docs) {
214 if ((err != null) || (docs == null)) return;
215 for (var j in docs) {
216 const days = Math.floor((now - docs[j].time) / 86400000); // Calculate the number of inactive days
217 var expireDays = -1;
218 if (removeInactiveDevicesPerDomain[docs[j].domain]) { expireDays = removeInactiveDevicesPerDomain[docs[j].domain]; }
219 const mesh = parent.webserver.meshes[docs[j].meshid];
220 if (mesh && (typeof mesh.expireDevs == 'number')) { expireDays = mesh.expireDevs; }
221 var remove = false;
222 if (expireDays > 0) {
223 if (expireDays < days) { remove = true; }
224 if (cb) { if (showall || remove) { cb(docs[j]._id.substring(2) + ', ' + days + ' days, expire ' + expireDays + ' days' + (remove ? ', removing' : '')); } }
225 if (remove) {
226 // Check if this device is connected right now
227 const nodeid = docs[j]._id.substring(2);
228 const conn = parent.GetConnectivityState(nodeid);
229 if (conn == null) {
230 // Remove the device
231 obj.Get(nodeid, function (err, docs) {
232 if (err != null) return;
233 if ((docs == null) || (docs.length != 1)) { obj.Remove('lc' + nodeid); return; } // Remove last connect time
234 const node = docs[0];
235
236 // Delete this node including network interface information, events and timeline
237 obj.Remove(node._id); // Remove node with that id
238 obj.Remove('if' + node._id); // Remove interface information
239 obj.Remove('nt' + node._id); // Remove notes
240 obj.Remove('lc' + node._id); // Remove last connect time
241 obj.Remove('si' + node._id); // Remove system information
242 obj.Remove('al' + node._id); // Remove error log last time
243 if (obj.RemoveSMBIOS) { obj.RemoveSMBIOS(node._id); } // Remove SMBios data
244 obj.RemoveAllNodeEvents(node.domain, node._id); // Remove all events for this node
245 obj.removeAllPowerEventsForNode(node._id); // Remove all power events for this node
246 if (typeof node.pmt == 'string') { obj.Remove('pmt_' + node.pmt); } // Remove Push Messaging Token
247 obj.Get('ra' + node._id, function (err, nodes) {
248 if ((nodes != null) && (nodes.length == 1)) { obj.Remove('da' + nodes[0].daid); } // Remove diagnostic agent to real agent link
249 obj.Remove('ra' + node._id); // Remove real agent to diagnostic agent link
250 });
251
252 // Remove any user node links
253 if (node.links != null) {
254 for (var i in node.links) {
255 if (i.startsWith('user/')) {
256 var cuser = parent.webserver.users[i];
257 if ((cuser != null) && (cuser.links != null) && (cuser.links[node._id] != null)) {
258 // Remove the user link & save the user
259 delete cuser.links[node._id];
260 if (Object.keys(cuser.links).length == 0) { delete cuser.links; }
261 obj.SetUser(cuser);
262
263 // Notify user change
264 var targets = ['*', 'server-users', cuser._id];
265 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) };
266 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.
267 parent.DispatchEvent(targets, obj, event);
268 }
269 } else if (i.startsWith('ugrp/')) {
270 var cusergroup = parent.webserver.userGroups[i];
271 if ((cusergroup != null) && (cusergroup.links != null) && (cusergroup.links[node._id] != null)) {
272 // Remove the user link & save the user
273 delete cusergroup.links[node._id];
274 if (Object.keys(cusergroup.links).length == 0) { delete cusergroup.links; }
275 obj.Set(cusergroup);
276
277 // Notify user change
278 var targets = ['*', 'server-users', cusergroup._id];
279 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 };
280 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.
281 parent.DispatchEvent(targets, obj, event);
282 }
283 }
284 }
285 }
286
287 // Event node deletion
288 var meshname = '(unknown)';
289 if ((parent.webserver.meshes[node.meshid] != null) && (parent.webserver.meshes[node.meshid].name != null)) { meshname = parent.webserver.meshes[node.meshid].name; }
290 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 };
291 // TODO: We can't use the changeStream for node delete because we will not know the meshid the device was in.
292 //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.
293 parent.DispatchEvent(parent.webserver.CreateNodeDispatchTargets(node.meshid, node._id), obj, event);
294 });
295 }
296 }
297 }
298 }
299 });
300 }
301 }
302
303 // Remove all reference to a domain from the database
304 obj.removeDomain = function (domainName, func) {
305 var pendingCalls;
306 // Remove all events, power events and SMBIOS data from the main collection. They are all in seperate collections now.
307 if (obj.databaseType == DB_ACEBASE) {
308 // AceBase
309 pendingCalls = 3;
310 obj.file.query('meshcentral').filter('domain', '==', domainName).remove().then(function () { if (--pendingCalls == 0) { func(); } });
311 obj.file.query('events').filter('domain', '==', domainName).remove().then(function () { if (--pendingCalls == 0) { func(); } });
312 obj.file.query('power').filter('domain', '==', domainName).remove().then(function () { if (--pendingCalls == 0) { func(); } });
313 } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL) || (obj.databaseType == DB_POSTGRESQL)) {
314 // MariaDB, MySQL or PostgreSQL
315 pendingCalls = 2;
316 sqlDbQuery('DELETE FROM main WHERE domain = $1', [domainName], function () { if (--pendingCalls == 0) { func(); } });
317 sqlDbQuery('DELETE FROM events WHERE domain = $1', [domainName], function () { if (--pendingCalls == 0) { func(); } });
318 } else if (obj.databaseType == DB_MONGODB) {
319 // MongoDB
320 pendingCalls = 3;
321 obj.file.deleteMany({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
322 obj.eventsfile.deleteMany({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
323 obj.powerfile.deleteMany({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
324 } else {
325 // NeDB or MongoJS
326 pendingCalls = 3;
327 obj.file.remove({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
328 obj.eventsfile.remove({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
329 obj.powerfile.remove({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
330 }
331 }
332
333 obj.cleanup = function (func) {
334 // TODO: Remove all mesh links to invalid users
335 // TODO: Remove all meshes that dont have any links
336
337 // Remove all events, power events and SMBIOS data from the main collection. They are all in seperate collections now.
338 if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL) || (obj.databaseType == DB_POSTGRESQL)) {
339 // MariaDB, MySQL or PostgreSQL
340 obj.RemoveAllOfType('event', function () { });
341 obj.RemoveAllOfType('power', function () { });
342 obj.RemoveAllOfType('smbios', function () { });
343 } else if (obj.databaseType == DB_MONGODB) {
344 // MongoDB
345 obj.file.deleteMany({ type: 'event' }, { multi: true });
346 obj.file.deleteMany({ type: 'power' }, { multi: true });
347 obj.file.deleteMany({ type: 'smbios' }, { multi: true });
348 } else if ((obj.databaseType == DB_NEDB) || (obj.databaseType == DB_MONGOJS)) {
349 // NeDB or MongoJS
350 obj.file.remove({ type: 'event' }, { multi: true });
351 obj.file.remove({ type: 'power' }, { multi: true });
352 obj.file.remove({ type: 'smbios' }, { multi: true });
353 }
354
355 // List of valid identifiers
356 var validIdentifiers = {}
357
358 // Load all user groups
359 obj.GetAllType('ugrp', function (err, docs) {
360 if (err != null) { parent.debug('db', 'ERROR (GetAll user): ' + err); }
361 if ((err == null) && (docs.length > 0)) {
362 for (var i in docs) {
363 // Add this as a valid user identifier
364 validIdentifiers[docs[i]._id] = 1;
365 }
366 }
367
368 // Fix all of the creating & login to ticks by seconds, not milliseconds.
369 obj.GetAllType('user', function (err, docs) {
370 if (err != null) { parent.debug('db', 'ERROR (GetAll user): ' + err); }
371 if ((err == null) && (docs.length > 0)) {
372 for (var i in docs) {
373 var fixed = false;
374
375 // Add this as a valid user identifier
376 validIdentifiers[docs[i]._id] = 1;
377
378 // Fix email address capitalization
379 if (docs[i].email && (docs[i].email != docs[i].email.toLowerCase())) {
380 docs[i].email = docs[i].email.toLowerCase(); fixed = true;
381 }
382
383 // Fix account creation
384 if (docs[i].creation) {
385 if (docs[i].creation > 1300000000000) { docs[i].creation = Math.floor(docs[i].creation / 1000); fixed = true; }
386 if ((docs[i].creation % 1) != 0) { docs[i].creation = Math.floor(docs[i].creation); fixed = true; }
387 }
388
389 // Fix last account login
390 if (docs[i].login) {
391 if (docs[i].login > 1300000000000) { docs[i].login = Math.floor(docs[i].login / 1000); fixed = true; }
392 if ((docs[i].login % 1) != 0) { docs[i].login = Math.floor(docs[i].login); fixed = true; }
393 }
394
395 // Fix last password change
396 if (docs[i].passchange) {
397 if (docs[i].passchange > 1300000000000) { docs[i].passchange = Math.floor(docs[i].passchange / 1000); fixed = true; }
398 if ((docs[i].passchange % 1) != 0) { docs[i].passchange = Math.floor(docs[i].passchange); fixed = true; }
399 }
400
401 // Fix subscriptions
402 if (docs[i].subscriptions != null) { delete docs[i].subscriptions; fixed = true; }
403
404 // Save the user if needed
405 if (fixed) { obj.Set(docs[i]); }
406 }
407
408 // Remove all objects that have a "meshid" that no longer points to a valid mesh.
409 // Fix any incorrectly escaped user identifiers
410 obj.GetAllType('mesh', function (err, docs) {
411 if (err != null) { parent.debug('db', 'ERROR (GetAll mesh): ' + err); }
412 var meshlist = [];
413 if ((err == null) && (docs.length > 0)) {
414 for (var i in docs) {
415 var meshChange = false;
416 docs[i] = common.unEscapeLinksFieldName(docs[i]);
417 meshlist.push(docs[i]._id);
418
419 // Make sure all mesh types are number type, if not, fix it.
420 if (typeof docs[i].mtype == 'string') { docs[i].mtype = parseInt(docs[i].mtype); meshChange = true; }
421
422 // If the device group is deleted, remove any invite codes
423 if (docs[i].deleted && docs[i].invite) { delete docs[i].invite; meshChange = true; }
424
425 // Take a look at the links
426 if (docs[i].links != null) {
427 for (var j in docs[i].links) {
428 if (validIdentifiers[j] == null) {
429 // This identifier is not known, let see if we can fix it.
430 var xid = j, xid2 = common.unEscapeFieldName(xid);
431 while ((xid != xid2) && (validIdentifiers[xid2] == null)) { xid = xid2; xid2 = common.unEscapeFieldName(xid2); }
432 if (validIdentifiers[xid2] == 1) {
433 //console.log('Fixing id: ' + j + ' to ' + xid2);
434 docs[i].links[xid2] = docs[i].links[j];
435 delete docs[i].links[j];
436 meshChange = true;
437 } else {
438 // TODO: here, we may want to clean up links to users and user groups that do not exist anymore.
439 //console.log('Unknown id: ' + j);
440 }
441 }
442 }
443 }
444
445 // Save the updated device group if needed
446 if (meshChange) { obj.Set(docs[i]); }
447 }
448 }
449 if (obj.databaseType == DB_SQLITE) {
450 // SQLite
451
452 } else if (obj.databaseType == DB_ACEBASE) {
453 // AceBase
454
455 } else if (obj.databaseType == DB_POSTGRESQL) {
456 // Postgres
457 sqlDbQuery('DELETE FROM main WHERE extra LIKE \'mesh/%\' AND extra <> ALL ($1)', [meshlist], function (err, response) { });
458 } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
459 // MariaDB
460 sqlDbQuery('DELETE FROM Main WHERE (extra LIKE ("mesh/%") AND (extra NOT IN ?)', [meshlist], function (err, response) { });
461 } else if (obj.databaseType == DB_MONGODB) {
462 // MongoDB
463 obj.file.deleteMany({ meshid: { $exists: true, $nin: meshlist } }, { multi: true });
464 } else {
465 // NeDB or MongoJS
466 obj.file.remove({ meshid: { $exists: true, $nin: meshlist } }, { multi: true });
467 }
468
469 // We are done
470 validIdentifiers = null;
471 if (func) { func(); }
472 });
473 }
474 });
475 });
476 };
477
478 // Get encryption key
479 obj.getEncryptDataKey = function (password, salt, iterations) {
480 if (typeof password != 'string') return null;
481 let key;
482 try {
483 key = parent.crypto.pbkdf2Sync(password, salt, iterations, 32, 'sha384');
484 } catch (ex) {
485 // If this previous call fails, it's probably because older pbkdf2 did not specify the hashing function, just use the default.
486 key = parent.crypto.pbkdf2Sync(password, salt, iterations, 32);
487 }
488 return key
489 }
490
491 // Encrypt data
492 obj.encryptData = function (password, plaintext) {
493 let encryptionVersion = 0x01;
494 let iterations = 100000
495 const iv = parent.crypto.randomBytes(16);
496 var key = obj.getEncryptDataKey(password, iv, iterations);
497 if (key == null) return null;
498 const aes = parent.crypto.createCipheriv('aes-256-gcm', key, iv);
499 var ciphertext = aes.update(plaintext);
500 let versionbuf = Buffer.allocUnsafe(2);
501 versionbuf.writeUInt16BE(encryptionVersion);
502 let iterbuf = Buffer.allocUnsafe(4);
503 iterbuf.writeUInt32BE(iterations);
504 let encryptedBuf = aes.final();
505 ciphertext = Buffer.concat([versionbuf, iterbuf, aes.getAuthTag(), iv, ciphertext, encryptedBuf]);
506 return ciphertext.toString('base64');
507 }
508
509 // Decrypt data
510 obj.decryptData = function (password, ciphertext) {
511 // Adding an encryption version lets us avoid try catching in the future
512 let ciphertextBytes = Buffer.from(ciphertext, 'base64');
513 let encryptionVersion = ciphertextBytes.readUInt16BE(0);
514 try {
515 switch (encryptionVersion) {
516 case 0x01:
517 let iterations = ciphertextBytes.readUInt32BE(2);
518 let authTag = ciphertextBytes.slice(6, 22);
519 const iv = ciphertextBytes.slice(22, 38);
520 const data = ciphertextBytes.slice(38);
521 let key = obj.getEncryptDataKey(password, iv, iterations);
522 if (key == null) return null;
523 const aes = parent.crypto.createDecipheriv('aes-256-gcm', key, iv);
524 aes.setAuthTag(authTag);
525 let plaintextBytes = Buffer.from(aes.update(data));
526 plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
527 return plaintextBytes;
528 default:
529 return obj.oldDecryptData(password, ciphertextBytes);
530 }
531 } catch (ex) { return obj.oldDecryptData(password, ciphertextBytes); }
532 }
533
534 // Encrypt data
535 // The older encryption system uses CBC without integraty checking.
536 // This method is kept only for testing
537 obj.oldEncryptData = function (password, plaintext) {
538 let key = parent.crypto.createHash('sha384').update(password).digest('raw').slice(0, 32);
539 if (key == null) return null;
540 const iv = parent.crypto.randomBytes(16);
541 const aes = parent.crypto.createCipheriv('aes-256-cbc', key, iv);
542 var ciphertext = aes.update(plaintext);
543 ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
544 return ciphertext.toString('base64');
545 }
546
547 // Decrypt data
548 // The older encryption system uses CBC without integraty checking.
549 // This method is kept only to convert the old encryption to the new one.
550 obj.oldDecryptData = function (password, ciphertextBytes) {
551 if (typeof password != 'string') return null;
552 try {
553 const iv = ciphertextBytes.slice(0, 16);
554 const data = ciphertextBytes.slice(16);
555 let key = parent.crypto.createHash('sha384').update(password).digest('raw').slice(0, 32);
556 const aes = parent.crypto.createDecipheriv('aes-256-cbc', key, iv);
557 let plaintextBytes = Buffer.from(aes.update(data));
558 plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
559 return plaintextBytes;
560 } catch (ex) { return null; }
561 }
562
563 // Get the number of records in the database for various types, this is the slow NeDB way.
564 // 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.
565 obj.getStats = function (func) {
566 if (obj.databaseType == DB_ACEBASE) {
567 // AceBase
568 // TODO
569 } else if (obj.databaseType == DB_POSTGRESQL) {
570 // PostgreSQL
571 // TODO
572 } else if (obj.databaseType == DB_MYSQL) {
573 // MySQL
574 // TODO
575 } else if (obj.databaseType == DB_MARIADB) {
576 // MariaDB
577 // TODO
578 } else if (obj.databaseType == DB_MONGODB) {
579 // MongoDB
580 obj.file.aggregate([{ "$group": { _id: "$type", count: { $sum: 1 } } }]).toArray(function (err, docs) {
581 var counters = {}, totalCount = 0;
582 if (err == null) { for (var i in docs) { if (docs[i]._id != null) { counters[docs[i]._id] = docs[i].count; totalCount += docs[i].count; } } }
583 func(counters);
584 });
585 } else if (obj.databaseType == DB_MONGOJS) {
586 // MongoJS
587 obj.file.aggregate([{ "$group": { _id: "$type", count: { $sum: 1 } } }], function (err, docs) {
588 var counters = {}, totalCount = 0;
589 if (err == null) { for (var i in docs) { if (docs[i]._id != null) { counters[docs[i]._id] = docs[i].count; totalCount += docs[i].count; } } }
590 func(counters);
591 });
592 } else if (obj.databaseType == DB_NEDB) {
593 // NeDB version
594 obj.file.count({ type: 'node' }, function (err, nodeCount) {
595 obj.file.count({ type: 'mesh' }, function (err, meshCount) {
596 obj.file.count({ type: 'user' }, function (err, userCount) {
597 obj.file.count({ type: 'sysinfo' }, function (err, sysinfoCount) {
598 obj.file.count({ type: 'note' }, function (err, noteCount) {
599 obj.file.count({ type: 'iploc' }, function (err, iplocCount) {
600 obj.file.count({ type: 'ifinfo' }, function (err, ifinfoCount) {
601 obj.file.count({ type: 'cfile' }, function (err, cfileCount) {
602 obj.file.count({ type: 'lastconnect' }, function (err, lastconnectCount) {
603 obj.file.count({}, function (err, totalCount) {
604 func({ node: nodeCount, mesh: meshCount, user: userCount, sysinfo: sysinfoCount, iploc: iplocCount, note: noteCount, ifinfo: ifinfoCount, cfile: cfileCount, lastconnect: lastconnectCount, total: totalCount });
605 });
606 });
607 });
608 });
609 });
610 });
611 });
612 });
613 });
614 });
615 }
616 }
617
618 // 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.
619 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 }); }); };
620 obj.escapeBase64 = function escapeBase64(val) { return (val.replace(/\+/g, '@').replace(/\//g, '$')); }
621
622 // Encrypt an database object
623 obj.performRecordEncryptionRecode = function (func) {
624 var count = 0;
625 obj.GetAllType('user', function (err, docs) {
626 if (err != null) { parent.debug('db', 'ERROR (performRecordEncryptionRecode): ' + err); }
627 if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } }
628 obj.GetAllType('node', function (err, docs) {
629 if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } }
630 obj.GetAllType('mesh', function (err, docs) {
631 if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } }
632 if (obj.databaseType == DB_NEDB) { // If we are using NeDB, compact the database.
633 obj.file.compactDatafile();
634 obj.file.on('compaction.done', function () { func(count); }); // It's important to wait for compaction to finish before exit, otherwise NeDB may corrupt.
635 } else {
636 func(count); // For all other databases, normal exit.
637 }
638 });
639 });
640 });
641 }
642
643 // Encrypt an database object
644 function performTypedRecordDecrypt(data) {
645 if ((data == null) || (obj.dbRecordsDecryptKey == null) || (typeof data != 'object')) return data;
646 for (var i in data) {
647 if ((data[i] == null) || (typeof data[i] != 'object')) continue;
648 data[i] = performPartialRecordDecrypt(data[i]);
649 if ((data[i].intelamt != null) && (typeof data[i].intelamt == 'object') && (data[i].intelamt._CRYPT)) { data[i].intelamt = performPartialRecordDecrypt(data[i].intelamt); }
650 if ((data[i].amt != null) && (typeof data[i].amt == 'object') && (data[i].amt._CRYPT)) { data[i].amt = performPartialRecordDecrypt(data[i].amt); }
651 if ((data[i].kvm != null) && (typeof data[i].kvm == 'object') && (data[i].kvm._CRYPT)) { data[i].kvm = performPartialRecordDecrypt(data[i].kvm); }
652 }
653 return data;
654 }
655
656 // Encrypt an database object
657 function performTypedRecordEncrypt(data) {
658 if (obj.dbRecordsEncryptKey == null) return data;
659 if (data.type == 'user') { return performPartialRecordEncrypt(Clone(data), ['otpkeys', 'otphkeys', 'otpsecret', 'salt', 'hash', 'oldpasswords']); }
660 else if ((data.type == 'node') && (data.ssh || data.rdp || data.intelamt)) {
661 var xdata = Clone(data);
662 if (data.ssh || data.rdp) { xdata = performPartialRecordEncrypt(xdata, ['ssh', 'rdp']); }
663 if (data.intelamt) { xdata.intelamt = performPartialRecordEncrypt(xdata.intelamt, ['pass', 'mpspass']); }
664 return xdata;
665 }
666 else if ((data.type == 'mesh') && (data.amt || data.kvm)) {
667 var xdata = Clone(data);
668 if (data.amt) { xdata.amt = performPartialRecordEncrypt(xdata.amt, ['password']); }
669 if (data.kvm) { xdata.kvm = performPartialRecordEncrypt(xdata.kvm, ['pass']); }
670 return xdata;
671 }
672 return data;
673 }
674
675 // Encrypt an object and return a buffer.
676 function performPartialRecordEncrypt(plainobj, encryptNames) {
677 if (typeof plainobj != 'object') return plainobj;
678 var enc = {}, enclen = 0;
679 for (var i in encryptNames) { if (plainobj[encryptNames[i]] != null) { enclen++; enc[encryptNames[i]] = plainobj[encryptNames[i]]; delete plainobj[encryptNames[i]]; } }
680 if (enclen > 0) { plainobj._CRYPT = performRecordEncrypt(enc); } else { delete plainobj._CRYPT; }
681 return plainobj;
682 }
683
684 // Encrypt an object and return a buffer.
685 function performPartialRecordDecrypt(plainobj) {
686 if ((typeof plainobj != 'object') || (plainobj._CRYPT == null)) return plainobj;
687 var enc = performRecordDecrypt(plainobj._CRYPT);
688 if (enc != null) { for (var i in enc) { plainobj[i] = enc[i]; } }
689 delete plainobj._CRYPT;
690 return plainobj;
691 }
692
693 // Encrypt an object and return a base64.
694 function performRecordEncrypt(plainobj) {
695 if (obj.dbRecordsEncryptKey == null) return null;
696 const iv = parent.crypto.randomBytes(12);
697 const aes = parent.crypto.createCipheriv('aes-256-gcm', obj.dbRecordsEncryptKey, iv);
698 var ciphertext = aes.update(JSON.stringify(plainobj));
699 var cipherfinal = aes.final();
700 ciphertext = Buffer.concat([iv, aes.getAuthTag(), ciphertext, cipherfinal]);
701 return ciphertext.toString('base64');
702 }
703
704 // Takes a base64 and return an object.
705 function performRecordDecrypt(ciphertext) {
706 if (obj.dbRecordsDecryptKey == null) return null;
707 const ciphertextBytes = Buffer.from(ciphertext, 'base64');
708 const iv = ciphertextBytes.slice(0, 12);
709 const data = ciphertextBytes.slice(28);
710 const aes = parent.crypto.createDecipheriv('aes-256-gcm', obj.dbRecordsDecryptKey, iv);
711 aes.setAuthTag(ciphertextBytes.slice(12, 28));
712 var plaintextBytes, r;
713 try {
714 plaintextBytes = Buffer.from(aes.update(data));
715 plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
716 r = JSON.parse(plaintextBytes.toString());
717 } catch (e) { throw "Incorrect DbRecordsDecryptKey/DbRecordsEncryptKey or invalid database _CRYPT data: " + e; }
718 return r;
719 }
720
721 // Clone an object (TODO: Make this more efficient)
722 function Clone(v) { return JSON.parse(JSON.stringify(v)); }
723
724 // Read expiration time from configuration file
725 if (typeof parent.args.dbexpire == 'object') {
726 if (typeof parent.args.dbexpire.events == 'number') { expireEventsSeconds = parent.args.dbexpire.events; }
727 if (typeof parent.args.dbexpire.powerevents == 'number') { expirePowerEventsSeconds = parent.args.dbexpire.powerevents; }
728 if (typeof parent.args.dbexpire.statsevents == 'number') { expireServerStatsSeconds = parent.args.dbexpire.statsevents; }
729 }
730
731 // If a DB record encryption key is provided, perform database record encryption
732 if ((typeof parent.args.dbrecordsencryptkey == 'string') && (parent.args.dbrecordsencryptkey.length != 0)) {
733 // Hash the database password into a AES256 key and setup encryption and decryption.
734 obj.dbRecordsEncryptKey = obj.dbRecordsDecryptKey = parent.crypto.createHash('sha384').update(parent.args.dbrecordsencryptkey).digest('raw').slice(0, 32);
735 }
736
737 // If a DB record decryption key is provided, perform database record decryption
738 if ((typeof parent.args.dbrecordsdecryptkey == 'string') && (parent.args.dbrecordsdecryptkey.length != 0)) {
739 // Hash the database password into a AES256 key and setup encryption and decryption.
740 obj.dbRecordsDecryptKey = parent.crypto.createHash('sha384').update(parent.args.dbrecordsdecryptkey).digest('raw').slice(0, 32);
741 }
742
743
744 function createTablesIfNotExist(dbname) {
745 var useDatabase = 'USE ' + dbname;
746 sqlDbQuery(useDatabase, null, function (err, docs) {
747 if (err != null) {
748 console.log("Unable to connect to database: " + err);
749 process.exit();
750 }
751 if (err == null) {
752 parent.debug('db', 'Checking tables...');
753 sqlDbBatchExec([
754 '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)))',
755 '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)))',
756 '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)',
757 'CREATE TABLE IF NOT EXISTS serverstats (time DATETIME, expire DATETIME, doc JSON, PRIMARY KEY(time), CHECK (json_valid(doc)))',
758 '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)))',
759 'CREATE TABLE IF NOT EXISTS smbios (id CHAR(255), time DATETIME, expire DATETIME, doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))',
760 'CREATE TABLE IF NOT EXISTS plugin (id INT NOT NULL AUTO_INCREMENT, doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))',
761 'CREATE TABLE IF NOT EXISTS pluginpermissions (id VARCHAR(255) PRIMARY KEY, doc JSON)'
762 ], function (err) {
763 parent.debug('db', 'Checking indexes...');
764 sqlDbExec('CREATE INDEX ndxtypedomainextra ON main (type, domain, extra)', null, function (err, response) { });
765 sqlDbExec('CREATE INDEX ndxextra ON main (extra)', null, function (err, response) { });
766 sqlDbExec('CREATE INDEX ndxextraex ON main (extraex)', null, function (err, response) { });
767 sqlDbExec('CREATE INDEX ndxeventstime ON events(time)', null, function (err, response) { });
768 sqlDbExec('CREATE INDEX ndxeventsusername ON events(domain, userid, time)', null, function (err, response) { });
769 sqlDbExec('CREATE INDEX ndxeventsdomainnodeidtime ON events(domain, nodeid, time)', null, function (err, response) { });
770 sqlDbExec('CREATE INDEX ndxeventids ON eventids(target)', null, function (err, response) { });
771 sqlDbExec('CREATE INDEX ndxeventidsfkid ON eventids(fkid)', null, function (err, response) { });
772 sqlDbExec('CREATE INDEX ndxserverstattime ON serverstats (time)', null, function (err, response) { });
773 sqlDbExec('CREATE INDEX ndxserverstatexpire ON serverstats (expire)', null, function (err, response) { });
774 sqlDbExec('CREATE INDEX ndxpowernodeidtime ON power (nodeid, time)', null, function (err, response) { });
775 sqlDbExec('CREATE INDEX ndxsmbiostime ON smbios (time)', null, function (err, response) { });
776 sqlDbExec('CREATE INDEX ndxsmbiosexpire ON smbios (expire)', null, function (err, response) { });
777 setupFunctions(func);
778 });
779 }
780 });
781 }
782
783 if (parent.args.sqlite3) {
784 // SQLite3 database setup
785 obj.databaseType = DB_SQLITE;
786 const sqlite3 = require('sqlite3');
787 let configParams = parent.config.settings.sqlite3;
788 if (typeof configParams == 'string') {databaseName = configParams} else {databaseName = configParams.name ? configParams.name : 'meshcentral';};
789 obj.sqliteConfig.startupVacuum = configParams.startupvacuum ? configParams.startupvacuum : false;
790 obj.sqliteConfig.autoVacuum = configParams.autovacuum ? configParams.autovacuum.toLowerCase() : 'incremental';
791 obj.sqliteConfig.incrementalVacuum = configParams.incrementalvacuum ? configParams.incrementalvacuum : 100;
792 obj.sqliteConfig.journalMode = configParams.journalmode ? configParams.journalmode.toLowerCase() : 'delete';
793 //allowed modes, 'none' excluded because not usefull for this app, maybe also remove 'memory'?
794 if (!(['delete', 'truncate', 'persist', 'memory', 'wal'].includes(obj.sqliteConfig.journalMode))) { obj.sqliteConfig.journalMode = 'delete'};
795 obj.sqliteConfig.journalSize = configParams.journalsize ? configParams.journalsize : 409600;
796 //wal can use the more performant 'normal' mode, see https://www.sqlite.org/pragma.html#pragma_synchronous
797 obj.sqliteConfig.synchronous = (obj.sqliteConfig.journalMode == 'wal') ? 'normal' : 'full';
798 if (obj.sqliteConfig.journalMode == 'wal') {obj.sqliteConfig.maintenance += 'PRAGMA wal_checkpoint(PASSIVE);'};
799 if (obj.sqliteConfig.autoVacuum == 'incremental') {obj.sqliteConfig.maintenance += 'PRAGMA incremental_vacuum(' + obj.sqliteConfig.incrementalVacuum + ');'};
800 obj.sqliteConfig.maintenance += 'PRAGMA optimize;';
801
802 parent.debug('db', 'SQlite config options: ' + JSON.stringify(obj.sqliteConfig, null, 4));
803 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') };
804 //.cached not usefull
805 obj.file = new sqlite3.Database(path.join(parent.datapath, databaseName + '.sqlite'), sqlite3.OPEN_READWRITE, function (err) {
806 if (err && (err.code == 'SQLITE_CANTOPEN')) {
807 // Database needs to be created
808 obj.file = new sqlite3.Database(path.join(parent.datapath, databaseName + '.sqlite'), function (err) {
809 if (err) { console.log("SQLite Error: " + err); process.exit(1); }
810 obj.file.exec(`
811 CREATE TABLE main (id VARCHAR(256) PRIMARY KEY NOT NULL, type CHAR(32), domain CHAR(64), extra CHAR(255), extraex CHAR(255), doc JSON);
812 CREATE TABLE events(id INTEGER PRIMARY KEY, time TIMESTAMP, domain CHAR(64), action CHAR(255), nodeid CHAR(255), userid CHAR(255), doc JSON);
813 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);
814 CREATE TABLE serverstats (time TIMESTAMP PRIMARY KEY, expire TIMESTAMP, doc JSON);
815 CREATE TABLE power (id INTEGER PRIMARY KEY, time TIMESTAMP, nodeid CHAR(255), doc JSON);
816 CREATE TABLE smbios (id CHAR(255) PRIMARY KEY, time TIMESTAMP, expire TIMESTAMP, doc JSON);
817 CREATE TABLE plugin (id INTEGER PRIMARY KEY, doc JSON);
818 CREATE TABLE pluginpermissions (id VARCHAR(255) PRIMARY KEY, doc JSON);
819 CREATE INDEX ndxtypedomainextra ON main (type, domain, extra);
820 CREATE INDEX ndxextra ON main (extra);
821 CREATE INDEX ndxextraex ON main (extraex);
822 CREATE INDEX ndxeventstime ON events(time);
823 CREATE INDEX ndxeventsusername ON events(domain, userid, time);
824 CREATE INDEX ndxeventsdomainnodeidtime ON events(domain, nodeid, time);
825 CREATE INDEX ndxeventids ON eventids(target);
826 CREATE INDEX ndxserverstattime ON serverstats (time);
827 CREATE INDEX ndxserverstatexpire ON serverstats (expire);
828 CREATE INDEX ndxpowernodeidtime ON power (nodeid, time);
829 CREATE INDEX ndxsmbiostime ON smbios (time);
830 CREATE INDEX ndxsmbiosexpire ON smbios (expire);
831 `, function (err) {
832 // Completed DB creation of SQLite3
833 sqliteSetOptions(func);
834 //setupFunctions could be put in the sqliteSetupOptions, but left after it for clarity
835 setupFunctions(func);
836 }
837 );
838 });
839 return;
840 } else if (err) { console.log("SQLite Error: " + err); process.exit(0); }
841
842 //for existing db's
843 sqliteSetOptions();
844 // Create any missing tables (e.g., pluginpermissions added in updates)
845 obj.file.exec(`
846 CREATE TABLE IF NOT EXISTS pluginpermissions (id VARCHAR(255) PRIMARY KEY, doc JSON)
847 `, function (err) {
848 if (err) { console.log("SQLite Error creating pluginpermissions table: " + err); }
849 //setupFunctions could be put in the sqliteSetupOptions, but left after it for clarity
850 setupFunctions(func);
851 });
852 });
853 } else if (parent.args.acebase) {
854 // AceBase database setup
855 obj.databaseType = DB_ACEBASE;
856 const { AceBase } = require('acebase');
857 // For information on AceBase sponsor: https://github.com/appy-one/acebase/discussions/100
858 obj.file = new AceBase('meshcentral', { sponsor: ((typeof parent.args.acebase == 'object') && (parent.args.acebase.sponsor)), logLevel: 'error', storage: { path: parent.datapath } });
859 // Get all the databases ready
860 obj.file.ready(function () {
861 // Create AceBase indexes
862 obj.file.indexes.create('meshcentral', 'type', { include: ['domain', 'meshid'] });
863 obj.file.indexes.create('meshcentral', 'email');
864 obj.file.indexes.create('meshcentral', 'meshid');
865 obj.file.indexes.create('meshcentral', 'intelamt.uuid');
866 obj.file.indexes.create('events', 'userid', { include: ['action'] });
867 obj.file.indexes.create('events', 'domain', { include: ['nodeid', 'time'] });
868 obj.file.indexes.create('events', 'ids', { include: ['time'] });
869 obj.file.indexes.create('events', 'time');
870 obj.file.indexes.create('power', 'nodeid', { include: ['time'] });
871 obj.file.indexes.create('power', 'time');
872 obj.file.indexes.create('stats', 'time');
873 obj.file.indexes.create('stats', 'expire');
874 // Completed setup of AceBase
875 setupFunctions(func);
876 });
877 } else if (parent.args.mariadb || parent.args.mysql) {
878 var connectinArgs = (parent.args.mariadb) ? parent.args.mariadb : parent.args.mysql;
879 if (typeof connectinArgs == 'string') {
880 const parts = connectinArgs.split(/[:@/]+/);
881 var connectionObject = {
882 "user": parts[1],
883 "password": parts[2],
884 "host": parts[3],
885 "port": parts[4],
886 "database": parts[5]
887 };
888 var dbname = (connectionObject.database != null) ? connectionObject.database : 'meshcentral';
889 } else {
890 var dbname = (connectinArgs.database != null) ? connectinArgs.database : 'meshcentral';
891
892 // Including the db name in the connection obj will cause a connection faliure if it does not exist
893 var connectionObject = Clone(connectinArgs);
894 delete connectionObject.database;
895
896 try {
897 if (connectinArgs.ssl) {
898 if (connectinArgs.ssl.dontcheckserveridentity == true) { connectionObject.ssl.checkServerIdentity = function (name, cert) { return undefined; } };
899 if (connectinArgs.ssl.cacertpath) { connectionObject.ssl.ca = [require('fs').readFileSync(connectinArgs.ssl.cacertpath, 'utf8')]; }
900 if (connectinArgs.ssl.clientcertpath) { connectionObject.ssl.cert = [require('fs').readFileSync(connectinArgs.ssl.clientcertpath, 'utf8')]; }
901 if (connectinArgs.ssl.clientkeypath) { connectionObject.ssl.key = [require('fs').readFileSync(connectinArgs.ssl.clientkeypath, 'utf8')]; }
902 }
903 } catch (ex) {
904 console.log('Error loading SQL Connector certificate: ' + ex);
905 process.exit();
906 }
907 }
908
909 if (parent.args.mariadb) {
910 // Use MariaDB
911 obj.databaseType = DB_MARIADB;
912 var tempDatastore = require('mariadb').createPool(connectionObject);
913 tempDatastore.getConnection().then(function (conn) {
914 conn.query('CREATE DATABASE IF NOT EXISTS ' + dbname).then(function (result) {
915 conn.release();
916 }).catch(function (ex) { console.log('Auto-create database failed: ' + ex); });
917 }).catch(function (ex) { console.log('Auto-create database failed: ' + ex); });
918 setTimeout(function () { tempDatastore.end(); }, 2000);
919
920 connectionObject.database = dbname;
921 Datastore = require('mariadb').createPool(connectionObject);
922 createTablesIfNotExist(dbname);
923 } else if (parent.args.mysql) {
924 // Use MySQL
925 obj.databaseType = DB_MYSQL;
926 var tempDatastore = require('mysql2').createPool(connectionObject);
927 tempDatastore.query('CREATE DATABASE IF NOT EXISTS ' + dbname, function (error) {
928 if (error != null) {
929 console.log('Auto-create database failed: ' + error);
930 }
931 connectionObject.database = dbname;
932 Datastore = require('mysql2').createPool(connectionObject);
933 createTablesIfNotExist(dbname);
934 });
935 setTimeout(function () { tempDatastore.end(); }, 2000);
936 }
937 } else if (parent.args.postgres) {
938 // Postgres SQL
939 let connectionArgs = parent.args.postgres;
940 connectionArgs.database = (databaseName = (connectionArgs.database != null) ? connectionArgs.database : 'meshcentral');
941
942 let DatastoreTest;
943 obj.databaseType = DB_POSTGRESQL;
944 const { Client } = require('pg');
945 Datastore = new Client(connectionArgs);
946 // Check if we should skip database creation check
947 if (connectionArgs.createdatabase === false ) {
948 // Skip database check/creation, just connect and run the SELECT query
949 Datastore.connect();
950 Datastore.query('SELECT doc FROM main WHERE id = $1', ['DatabaseIdentifier'], function (err, res) {
951 if (err == null) {
952 // Always call postgreSqlCreateTables since it uses CREATE TABLE IF NOT EXISTS
953 // This ensures new tables (like pluginpermissions) get created on upgrades
954 postgreSqlCreateTables(func);
955 } else if (err.code == '42P01') { //42P01 = undefined table
956 postgreSqlCreateTables(func);
957 } else {
958 console.log('Postgresql connection error: ', err.message);
959 process.exit(0);
960 }
961 });
962 } else {
963 //Connect to and check pg db first to check if own db exists. Otherwise errors out on 'database does not exist'
964 connectionArgs.database = 'postgres';
965 DatastoreTest = new Client(connectionArgs);
966 DatastoreTest.connect();
967 connectionArgs.database = databaseName; //put the name back for backupconfig info
968 DatastoreTest.query('SELECT 1 FROM pg_catalog.pg_database WHERE datname = $1', [databaseName], function (err, res) { // check database exists first before creating
969 if (res.rowCount != 0) { // database exists now check tables exists
970 DatastoreTest.end();
971 Datastore.connect();
972 Datastore.query('SELECT doc FROM main WHERE id = $1', ['DatabaseIdentifier'], function (err, res) {
973 if (err == null) {
974 // Always call postgreSqlCreateTables since it uses CREATE TABLE IF NOT EXISTS
975 // This ensures new tables (like pluginpermissions) get created on upgrades
976 postgreSqlCreateTables(func);
977 } else
978 if (err.code == '42P01') { //42P01 = undefined table, https://www.postgresql.org/docs/current/errcodes-appendix.html
979 postgreSqlCreateTables(func);
980 } else {
981 console.log('Postgresql database exists, other error: ', err.message); process.exit(0);
982 };
983 });
984 } else { // If not present, create the tables and indexes
985 //not needed, just use a create db statement: const pgtools = require('pgtools');
986 DatastoreTest.query('CREATE DATABASE "'+ databaseName + '";', [], function (err, res) {
987 if (err == null) {
988 // Create the tables and indexes
989 DatastoreTest.end();
990 Datastore.connect();
991 postgreSqlCreateTables(func);
992 } else {
993 console.log('Postgresql database create error: ', err.message);
994 process.exit(0);
995 }
996 });
997 }
998 });
999 }
1000 } else if (parent.args.mongodb) {
1001 // Use MongoDB
1002 obj.databaseType = DB_MONGODB;
1003
1004 // If running an older NodeJS version, TextEncoder/TextDecoder is required
1005 if (global.TextEncoder == null) { global.TextEncoder = require('util').TextEncoder; }
1006 if (global.TextDecoder == null) { global.TextDecoder = require('util').TextDecoder; }
1007
1008 require('mongodb').MongoClient.connect(parent.args.mongodb, { useNewUrlParser: true, useUnifiedTopology: true, enableUtf8Validation: false }, function (err, client) {
1009 if (err != null) { console.log("Unable to connect to database: " + err); process.exit(); return; }
1010 Datastore = client;
1011 parent.debug('db', 'Connected to MongoDB database...');
1012
1013 // Get the database name and setup the database client
1014 var dbname = 'meshcentral';
1015 if (parent.args.mongodbname) { dbname = parent.args.mongodbname; }
1016 const dbcollectionname = (parent.args.mongodbcol) ? (parent.args.mongodbcol) : 'meshcentral';
1017 const db = client.db(dbname);
1018
1019 // Check the database version
1020 db.admin().serverInfo(function (err, info) {
1021 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')) {
1022 console.log('WARNING: Unable to check MongoDB version.');
1023 } else {
1024 if ((info.versionArray[0] < 3) || ((info.versionArray[0] == 3) && (info.versionArray[1] < 6))) {
1025 // We are running with mongoDB older than 3.6, this is not good.
1026 parent.addServerWarning("Current version of MongoDB (" + info.version + ") is too old, please upgrade to MongoDB 3.6 or better.", true);
1027 }
1028 }
1029 });
1030
1031 // Setup MongoDB main collection and indexes
1032 obj.file = db.collection(dbcollectionname);
1033 obj.file.indexes(function (err, indexes) {
1034 // Check if we need to reset indexes
1035 var indexesByName = {}, indexCount = 0;
1036 for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
1037 if ((indexCount != 5) || (indexesByName['TypeDomainMesh1'] == null) || (indexesByName['Email1'] == null) || (indexesByName['Mesh1'] == null) || (indexesByName['AmtUuid1'] == null)) {
1038 console.log('Resetting main indexes...');
1039 obj.file.dropIndexes(function (err) {
1040 obj.file.createIndex({ type: 1, domain: 1, meshid: 1 }, { sparse: 1, name: 'TypeDomainMesh1' }); // Speeds up GetAllTypeNoTypeField() and GetAllTypeNoTypeFieldMeshFiltered()
1041 obj.file.createIndex({ email: 1 }, { sparse: 1, name: 'Email1' }); // Speeds up GetUserWithEmail() and GetUserWithVerifiedEmail()
1042 obj.file.createIndex({ meshid: 1 }, { sparse: 1, name: 'Mesh1' }); // Speeds up RemoveMesh()
1043 obj.file.createIndex({ 'intelamt.uuid': 1 }, { sparse: 1, name: 'AmtUuid1' }); // Speeds up getAmtUuidMeshNode()
1044 });
1045 }
1046 });
1047
1048 // Setup the changeStream on the MongoDB main collection if possible
1049 if (parent.args.mongodbchangestream == true) {
1050 obj.dbCounters.changeStream = { change: 0, update: 0, insert: 0, delete: 0 };
1051 if (typeof obj.file.watch != 'function') {
1052 console.log('WARNING: watch() is not a function, MongoDB ChangeStream not supported.');
1053 } else {
1054 obj.fileChangeStream = obj.file.watch([{ $match: { $or: [{ 'fullDocument.type': { $in: ['node', 'mesh', 'user', 'ugrp'] } }, { 'operationType': 'delete' }] } }], { fullDocument: 'updateLookup' });
1055 obj.fileChangeStream.on('change', function (change) {
1056 obj.dbCounters.changeStream.change++;
1057 if ((change.operationType == 'update') || (change.operationType == 'replace')) {
1058 obj.dbCounters.changeStream.update++;
1059 switch (change.fullDocument.type) {
1060 case 'node': { dbNodeChange(change, false); break; } // A node has changed
1061 case 'mesh': { dbMeshChange(change, false); break; } // A device group has changed
1062 case 'user': { dbUserChange(change, false); break; } // A user account has changed
1063 case 'ugrp': { dbUGrpChange(change, false); break; } // A user account has changed
1064 }
1065 } else if (change.operationType == 'insert') {
1066 obj.dbCounters.changeStream.insert++;
1067 switch (change.fullDocument.type) {
1068 case 'node': { dbNodeChange(change, true); break; } // A node has added
1069 case 'mesh': { dbMeshChange(change, true); break; } // A device group has created
1070 case 'user': { dbUserChange(change, true); break; } // A user account has created
1071 case 'ugrp': { dbUGrpChange(change, true); break; } // A user account has created
1072 }
1073 } else if (change.operationType == 'delete') {
1074 obj.dbCounters.changeStream.delete++;
1075 if ((change.documentKey == null) || (change.documentKey._id == null)) return;
1076 var splitId = change.documentKey._id.split('/');
1077 switch (splitId[0]) {
1078 case 'node': {
1079 //Not Good: Problem here is that we don't know what meshid the node belonged to before the delete.
1080 //parent.DispatchEvent(['*', node.meshid], obj, { etype: 'node', action: 'removenode', nodeid: change.documentKey._id, domain: splitId[1] });
1081 break;
1082 }
1083 case 'mesh': {
1084 parent.DispatchEvent(['*', change.documentKey._id], obj, { etype: 'mesh', action: 'deletemesh', meshid: change.documentKey._id, domain: splitId[1] });
1085 break;
1086 }
1087 case 'user': {
1088 //Not Good: This is not a perfect user removal because we don't know what groups the user was in.
1089 //parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', action: 'accountremove', userid: change.documentKey._id, domain: splitId[1], username: splitId[2] });
1090 break;
1091 }
1092 case 'ugrp': {
1093 parent.DispatchEvent(['*', change.documentKey._id], obj, { etype: 'ugrp', action: 'deleteusergroup', ugrpid: change.documentKey._id, domain: splitId[1] });
1094 break;
1095 }
1096 }
1097 }
1098 });
1099 obj.changeStream = true;
1100 }
1101 }
1102
1103 // Setup MongoDB events collection and indexes
1104 obj.eventsfile = db.collection('events'); // Collection containing all events
1105 obj.eventsfile.indexes(function (err, indexes) {
1106 // Check if we need to reset indexes
1107 var indexesByName = {}, indexCount = 0;
1108 for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
1109 if ((indexCount != 5) || (indexesByName['UseridAction1'] == null) || (indexesByName['DomainNodeTime1'] == null) || (indexesByName['IdsAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
1110 // Reset all indexes
1111 console.log("Resetting events indexes...");
1112 obj.eventsfile.dropIndexes(function (err) {
1113 obj.eventsfile.createIndex({ userid: 1, action: 1 }, { sparse: 1, name: 'UseridAction1' });
1114 obj.eventsfile.createIndex({ domain: 1, nodeid: 1, time: -1 }, { sparse: 1, name: 'DomainNodeTime1' });
1115 obj.eventsfile.createIndex({ ids: 1, time: -1 }, { sparse: 1, name: 'IdsAndTime1' });
1116 obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
1117 });
1118 } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireEventsSeconds) {
1119 // Reset the timeout index
1120 console.log("Resetting events expire index...");
1121 obj.eventsfile.dropIndex('ExpireTime1', function (err) {
1122 obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
1123 });
1124 }
1125 });
1126
1127 // Setup MongoDB power events collection and indexes
1128 obj.powerfile = db.collection('power'); // Collection containing all power events
1129 obj.powerfile.indexes(function (err, indexes) {
1130 // Check if we need to reset indexes
1131 var indexesByName = {}, indexCount = 0;
1132 for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
1133 if ((indexCount != 3) || (indexesByName['NodeIdAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
1134 // Reset all indexes
1135 console.log("Resetting power events indexes...");
1136 obj.powerfile.dropIndexes(function (err) {
1137 // Create all indexes
1138 obj.powerfile.createIndex({ nodeid: 1, time: 1 }, { sparse: 1, name: 'NodeIdAndTime1' });
1139 obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
1140 });
1141 } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expirePowerEventsSeconds) {
1142 // Reset the timeout index
1143 console.log("Resetting power events expire index...");
1144 obj.powerfile.dropIndex('ExpireTime1', function (err) {
1145 // Reset the expire power events index
1146 obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
1147 });
1148 }
1149 });
1150
1151 // Setup MongoDB smbios collection, no indexes needed
1152 obj.smbiosfile = db.collection('smbios'); // Collection containing all smbios information
1153
1154 // Setup MongoDB server stats collection
1155 obj.serverstatsfile = db.collection('serverstats'); // Collection of server stats
1156 obj.serverstatsfile.indexes(function (err, indexes) {
1157 // Check if we need to reset indexes
1158 var indexesByName = {}, indexCount = 0;
1159 for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
1160 if ((indexCount != 3) || (indexesByName['ExpireTime1'] == null)) {
1161 // Reset all indexes
1162 console.log("Resetting server stats indexes...");
1163 obj.serverstatsfile.dropIndexes(function (err) {
1164 // Create all indexes
1165 obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
1166 obj.serverstatsfile.createIndex({ 'expire': 1 }, { expireAfterSeconds: 0, name: 'ExpireTime2' }); // Auto-expire events
1167 });
1168 } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireServerStatsSeconds) {
1169 // Reset the timeout index
1170 console.log("Resetting server stats expire index...");
1171 obj.serverstatsfile.dropIndex('ExpireTime1', function (err) {
1172 // Reset the expire server stats index
1173 obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
1174 });
1175 }
1176 });
1177
1178 // Setup plugin info collection
1179 if (obj.pluginsActive) { obj.pluginsfile = db.collection('plugins'); obj.pluginpermissionsfile = db.collection('pluginpermissions'); }
1180
1181 setupFunctions(func); // Completed setup of MongoDB
1182 });
1183 } else if (parent.args.xmongodb) {
1184 // Use MongoJS, this is the old system.
1185 obj.databaseType = DB_MONGOJS;
1186 Datastore = require('mongojs');
1187 var db = Datastore(parent.args.xmongodb);
1188 var dbcollection = 'meshcentral';
1189 if (parent.args.mongodbcol) { dbcollection = parent.args.mongodbcol; }
1190
1191 // Setup MongoDB main collection and indexes
1192 obj.file = db.collection(dbcollection);
1193 obj.file.getIndexes(function (err, indexes) {
1194 // Check if we need to reset indexes
1195 var indexesByName = {}, indexCount = 0;
1196 for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
1197 if ((indexCount != 5) || (indexesByName['TypeDomainMesh1'] == null) || (indexesByName['Email1'] == null) || (indexesByName['Mesh1'] == null) || (indexesByName['AmtUuid1'] == null)) {
1198 console.log("Resetting main indexes...");
1199 obj.file.dropIndexes(function (err) {
1200 obj.file.createIndex({ type: 1, domain: 1, meshid: 1 }, { sparse: 1, name: 'TypeDomainMesh1' }); // Speeds up GetAllTypeNoTypeField() and GetAllTypeNoTypeFieldMeshFiltered()
1201 obj.file.createIndex({ email: 1 }, { sparse: 1, name: 'Email1' }); // Speeds up GetUserWithEmail() and GetUserWithVerifiedEmail()
1202 obj.file.createIndex({ meshid: 1 }, { sparse: 1, name: 'Mesh1' }); // Speeds up RemoveMesh()
1203 obj.file.createIndex({ 'intelamt.uuid': 1 }, { sparse: 1, name: 'AmtUuid1' }); // Speeds up getAmtUuidMeshNode()
1204 });
1205 }
1206 });
1207
1208 // Setup MongoDB events collection and indexes
1209 obj.eventsfile = db.collection('events'); // Collection containing all events
1210 obj.eventsfile.getIndexes(function (err, indexes) {
1211 // Check if we need to reset indexes
1212 var indexesByName = {}, indexCount = 0;
1213 for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
1214 if ((indexCount != 5) || (indexesByName['UseridAction1'] == null) || (indexesByName['DomainNodeTime1'] == null) || (indexesByName['IdsAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
1215 // Reset all indexes
1216 console.log("Resetting events indexes...");
1217 obj.eventsfile.dropIndexes(function (err) {
1218 obj.eventsfile.createIndex({ userid: 1, action: 1 }, { sparse: 1, name: 'UseridAction1' });
1219 obj.eventsfile.createIndex({ domain: 1, nodeid: 1, time: -1 }, { sparse: 1, name: 'DomainNodeTime1' });
1220 obj.eventsfile.createIndex({ ids: 1, time: -1 }, { sparse: 1, name: 'IdsAndTime1' });
1221 obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
1222 });
1223 } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireEventsSeconds) {
1224 // Reset the timeout index
1225 console.log("Resetting events expire index...");
1226 obj.eventsfile.dropIndex('ExpireTime1', function (err) {
1227 obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
1228 });
1229 }
1230 });
1231
1232 // Setup MongoDB power events collection and indexes
1233 obj.powerfile = db.collection('power'); // Collection containing all power events
1234 obj.powerfile.getIndexes(function (err, indexes) {
1235 // Check if we need to reset indexes
1236 var indexesByName = {}, indexCount = 0;
1237 for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
1238 if ((indexCount != 3) || (indexesByName['NodeIdAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
1239 // Reset all indexes
1240 console.log("Resetting power events indexes...");
1241 obj.powerfile.dropIndexes(function (err) {
1242 // Create all indexes
1243 obj.powerfile.createIndex({ nodeid: 1, time: 1 }, { sparse: 1, name: 'NodeIdAndTime1' });
1244 obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
1245 });
1246 } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expirePowerEventsSeconds) {
1247 // Reset the timeout index
1248 console.log("Resetting power events expire index...");
1249 obj.powerfile.dropIndex('ExpireTime1', function (err) {
1250 // Reset the expire power events index
1251 obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
1252 });
1253 }
1254 });
1255
1256 // Setup MongoDB smbios collection, no indexes needed
1257 obj.smbiosfile = db.collection('smbios'); // Collection containing all smbios information
1258
1259 // Setup MongoDB server stats collection
1260 obj.serverstatsfile = db.collection('serverstats'); // Collection of server stats
1261 obj.serverstatsfile.getIndexes(function (err, indexes) {
1262 // Check if we need to reset indexes
1263 var indexesByName = {}, indexCount = 0;
1264 for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
1265 if ((indexCount != 3) || (indexesByName['ExpireTime1'] == null)) {
1266 // Reset all indexes
1267 console.log("Resetting server stats indexes...");
1268 obj.serverstatsfile.dropIndexes(function (err) {
1269 // Create all indexes
1270 obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
1271 obj.serverstatsfile.createIndex({ 'expire': 1 }, { expireAfterSeconds: 0, name: 'ExpireTime2' }); // Auto-expire events
1272 });
1273 } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireServerStatsSeconds) {
1274 // Reset the timeout index
1275 console.log("Resetting server stats expire index...");
1276 obj.serverstatsfile.dropIndex('ExpireTime1', function (err) {
1277 // Reset the expire server stats index
1278 obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
1279 });
1280 }
1281 });
1282
1283 // Setup plugin info collection
1284 if (obj.pluginsActive) { obj.pluginsfile = db.collection('plugins'); }
1285
1286 setupFunctions(func); // Completed setup of MongoJS
1287 } else {
1288 // Use NeDB (The default)
1289 obj.databaseType = DB_NEDB;
1290 try { Datastore = require('@seald-io/nedb'); } catch (ex) { } // This is the NeDB with Node 23 support.
1291 if (Datastore == null) {
1292 try { Datastore = require('@yetzt/nedb'); } catch (ex) { } // This is the NeDB with fixed security dependencies.
1293 if (Datastore == null) { Datastore = require('nedb'); } // So not to break any existing installations, if the old NeDB is present, use it.
1294 }
1295 var datastoreOptions = { filename: parent.getConfigFilePath('meshcentral.db'), autoload: true };
1296
1297 // If a DB encryption key is provided, perform database encryption
1298 if ((typeof parent.args.dbencryptkey == 'string') && (parent.args.dbencryptkey.length != 0)) {
1299 // Hash the database password into a AES256 key and setup encryption and decryption.
1300 obj.dbKey = parent.crypto.createHash('sha384').update(parent.args.dbencryptkey).digest('raw').slice(0, 32);
1301 datastoreOptions.afterSerialization = function (plaintext) {
1302 const iv = parent.crypto.randomBytes(16);
1303 const aes = parent.crypto.createCipheriv('aes-256-cbc', obj.dbKey, iv);
1304 var ciphertext = aes.update(plaintext);
1305 ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
1306 return ciphertext.toString('base64');
1307 }
1308 datastoreOptions.beforeDeserialization = function (ciphertext) {
1309 const ciphertextBytes = Buffer.from(ciphertext, 'base64');
1310 const iv = ciphertextBytes.slice(0, 16);
1311 const data = ciphertextBytes.slice(16);
1312 const aes = parent.crypto.createDecipheriv('aes-256-cbc', obj.dbKey, iv);
1313 var plaintextBytes = Buffer.from(aes.update(data));
1314 plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
1315 return plaintextBytes.toString();
1316 }
1317 }
1318
1319 // Start NeDB main collection and setup indexes
1320 obj.file = new Datastore(datastoreOptions);
1321 obj.file.setAutocompactionInterval(86400000); // Compact once a day
1322 obj.file.ensureIndex({ fieldName: 'type' });
1323 obj.file.ensureIndex({ fieldName: 'domain' });
1324 obj.file.ensureIndex({ fieldName: 'meshid', sparse: true });
1325 obj.file.ensureIndex({ fieldName: 'nodeid', sparse: true });
1326 obj.file.ensureIndex({ fieldName: 'email', sparse: true });
1327
1328 // Setup the events collection and setup indexes
1329 obj.eventsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-events.db'), autoload: true, corruptAlertThreshold: 1 });
1330 obj.eventsfile.setAutocompactionInterval(86400000); // Compact once a day
1331 obj.eventsfile.ensureIndex({ fieldName: 'ids' }); // TODO: Not sure if this is a good index, this is a array field.
1332 obj.eventsfile.ensureIndex({ fieldName: 'nodeid', sparse: true });
1333 obj.eventsfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: expireEventsSeconds });
1334 obj.eventsfile.remove({ time: { '$lt': new Date(Date.now() - (expireEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
1335
1336 // Setup the power collection and setup indexes
1337 obj.powerfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-power.db'), autoload: true, corruptAlertThreshold: 1 });
1338 obj.powerfile.setAutocompactionInterval(86400000); // Compact once a day
1339 obj.powerfile.ensureIndex({ fieldName: 'nodeid' });
1340 obj.powerfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: expirePowerEventsSeconds });
1341 obj.powerfile.remove({ time: { '$lt': new Date(Date.now() - (expirePowerEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
1342
1343 // Setup the SMBIOS collection, for NeDB we don't setup SMBIOS since NeDB will corrupt the database. Remove any existing ones.
1344 //obj.smbiosfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-smbios.db'), autoload: true, corruptAlertThreshold: 1 });
1345 fs.unlink(parent.getConfigFilePath('meshcentral-smbios.db'), function () { });
1346
1347 // Setup the server stats collection and setup indexes
1348 obj.serverstatsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-stats.db'), autoload: true, corruptAlertThreshold: 1 });
1349 obj.serverstatsfile.setAutocompactionInterval(86400000); // Compact once a day
1350 obj.serverstatsfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: expireServerStatsSeconds });
1351 obj.serverstatsfile.ensureIndex({ fieldName: 'expire', expireAfterSeconds: 0 }); // Auto-expire events
1352 obj.serverstatsfile.remove({ time: { '$lt': new Date(Date.now() - (expireServerStatsSeconds * 1000)) } }, { multi: true }); // Force delete older events
1353
1354 // Setup plugin info collection
1355 if (obj.pluginsActive) {
1356 obj.pluginsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-plugins.db'), autoload: true });
1357 obj.pluginsfile.setAutocompactionInterval(86400000); // Compact once a day
1358 }
1359
1360 setupFunctions(func); // Completed setup of NeDB
1361 }
1362
1363 function sqliteSetOptions(func) {
1364 //get current auto_vacuum mode for comparison
1365 obj.file.get('PRAGMA auto_vacuum;', function(err, current){
1366 let pragma = 'PRAGMA journal_mode=' + obj.sqliteConfig.journalMode + ';' +
1367 'PRAGMA synchronous='+ obj.sqliteConfig.synchronous + ';' +
1368 'PRAGMA journal_size_limit=' + obj.sqliteConfig.journalSize + ';' +
1369 'PRAGMA auto_vacuum=' + obj.sqliteConfig.autoVacuum + ';' +
1370 'PRAGMA incremental_vacuum=' + obj.sqliteConfig.incrementalVacuum + ';' +
1371 'PRAGMA optimize=0x10002;';
1372 //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
1373 if ( obj.sqliteConfig.startupVacuum
1374 || (current.auto_vacuum == 0 && obj.sqliteConfig.autoVacuum !='none')
1375 || (current.auto_vacuum != 0 && obj.sqliteConfig.autoVacuum =='none'))
1376 {
1377 pragma += 'VACUUM;';
1378 };
1379 parent.debug ('db', 'Config statement: ' + pragma);
1380
1381 obj.file.exec( pragma,
1382 function (err) {
1383 if (err) { parent.debug('db', 'Config pragma error: ' + (err.message)) };
1384 sqliteGetPragmas(['journal_mode', 'journal_size_limit', 'freelist_count', 'auto_vacuum', 'page_size', 'wal_autocheckpoint', 'synchronous'], function (pragma, pragmaValue) {
1385 parent.debug('db', 'PRAGMA: ' + pragma + '=' + pragmaValue);
1386 });
1387 });
1388 });
1389 //setupFunctions(func);
1390 }
1391
1392 function sqliteGetPragmas (pragmas, func){
1393 //pragmas can only be gotting one by one
1394 pragmas.forEach (function (pragma) {
1395 obj.file.get('PRAGMA ' + pragma + ';', function(err, res){
1396 if (pragma == 'auto_vacuum') { res[pragma] = SQLITE_AUTOVACUUM[res[pragma]] };
1397 if (pragma == 'synchronous') { res[pragma] = SQLITE_SYNCHRONOUS[res[pragma]] };
1398 if (func) { func (pragma, res[pragma]); }
1399 });
1400 });
1401 }
1402 // Create the PostgreSQL tables
1403 function postgreSqlCreateTables(func) {
1404 // Database was created, create the tables
1405 parent.debug('db', 'Creating tables...');
1406 sqlDbBatchExec([
1407 '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)',
1408 '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)',
1409 '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)',
1410 'CREATE TABLE IF NOT EXISTS serverstats (time TIMESTAMP PRIMARY KEY, expire TIMESTAMP, doc JSON)',
1411 'CREATE TABLE IF NOT EXISTS power (id SERIAL PRIMARY KEY, time TIMESTAMP, nodeid CHAR(255), doc JSON)',
1412 'CREATE TABLE IF NOT EXISTS smbios (id CHAR(255) PRIMARY KEY, time TIMESTAMP, expire TIMESTAMP, doc JSON)',
1413 'CREATE TABLE IF NOT EXISTS plugin (id SERIAL PRIMARY KEY, doc JSON)',
1414 'CREATE TABLE IF NOT EXISTS pluginpermissions (id VARCHAR(255) PRIMARY KEY, doc JSON)'
1415 ], function (results) {
1416 parent.debug('db', 'Creating indexes...');
1417 sqlDbExec('CREATE INDEX ndxtypedomainextra ON main (type, domain, extra)', null, function (err, response) { });
1418 sqlDbExec('CREATE INDEX ndxextra ON main (extra)', null, function (err, response) { });
1419 sqlDbExec('CREATE INDEX ndxextraex ON main (extraex)', null, function (err, response) { });
1420 sqlDbExec('CREATE INDEX ndxeventstime ON events(time)', null, function (err, response) { });
1421 sqlDbExec('CREATE INDEX ndxeventsusername ON events(domain, userid, time)', null, function (err, response) { });
1422 sqlDbExec('CREATE INDEX ndxeventsdomainnodeidtime ON events(domain, nodeid, time)', null, function (err, response) { });
1423 sqlDbExec('CREATE INDEX ndxeventids ON eventids(target)', null, function (err, response) { });
1424 sqlDbExec('CREATE INDEX ndxserverstattime ON serverstats (time)', null, function (err, response) { });
1425 sqlDbExec('CREATE INDEX ndxserverstatexpire ON serverstats (expire)', null, function (err, response) { });
1426 sqlDbExec('CREATE INDEX ndxpowernodeidtime ON power (nodeid, time)', null, function (err, response) { });
1427 sqlDbExec('CREATE INDEX ndxsmbiostime ON smbios (time)', null, function (err, response) { });
1428 sqlDbExec('CREATE INDEX ndxsmbiosexpire ON smbios (expire)', null, function (err, response) { });
1429 setupFunctions(func);
1430 });
1431 }
1432
1433 // Check the object names for a "."
1434 function checkObjectNames(r, tag) {
1435 if (typeof r != 'object') return;
1436 for (var i in r) {
1437 if (i.indexOf('.') >= 0) { throw ('BadDbName (' + tag + '): ' + JSON.stringify(r)); }
1438 checkObjectNames(r[i], tag);
1439 }
1440 }
1441
1442 // Query the database
1443 function sqlDbQuery(query, args, func, debug) {
1444 if (obj.databaseType == DB_SQLITE) { // SQLite
1445 if (args == null) { args = []; }
1446 obj.file.all(query, args, function (err, docs) {
1447 if (err != null) { console.log(query, args, err, docs); }
1448 if (docs != null) {
1449 for (var i in docs) {
1450 if (typeof docs[i].doc == 'string') {
1451 try { docs[i] = JSON.parse(docs[i].doc); } catch (ex) {
1452 console.log(query, args, docs[i]);
1453 }
1454 }
1455 }
1456 }
1457 if (func) { func(err, docs); }
1458 });
1459 } else if (obj.databaseType == DB_MARIADB) { // MariaDB
1460 Datastore.getConnection()
1461 .then(function (conn) {
1462 conn.query(query, args)
1463 .then(function (rows) {
1464 conn.release();
1465 var docs = [];
1466 for (var i in rows) {
1467 if (rows[i].doc) {
1468 docs.push(performTypedRecordDecrypt((typeof rows[i].doc == 'object') ? rows[i].doc : JSON.parse(rows[i].doc)));
1469 } else if ((rows.length == 1) && (rows[i]['COUNT(doc)'] != null)) {
1470 // This is a SELECT COUNT() operation
1471 docs = parseInt(rows[i]['COUNT(doc)']);
1472 }
1473 }
1474 if (func) try { func(null, docs); } catch (ex) { console.log('SQLERR1', ex); }
1475 })
1476 .catch(function (err) { conn.release(); if (func) try { func(err); } catch (ex) { console.log('SQLERR2', ex); } });
1477 }).catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log('SQLERR3', ex); } } });
1478 } else if (obj.databaseType == DB_MYSQL) { // MySQL
1479 Datastore.query(query, args, function (error, results, fields) {
1480 if (error != null) {
1481 if (func) try { func(error); } catch (ex) { console.log('SQLERR4', ex); }
1482 } else {
1483 var docs = [];
1484 for (var i in results) {
1485 if (results[i].doc) {
1486 if (typeof results[i].doc == 'string') {
1487 docs.push(JSON.parse(results[i].doc));
1488 } else {
1489 docs.push(results[i].doc);
1490 }
1491 } else if ((results.length == 1) && (results[i]['COUNT(doc)'] != null)) {
1492 // This is a SELECT COUNT() operation
1493 docs = results[i]['COUNT(doc)'];
1494 }
1495 }
1496 if (func) { try { func(null, docs); } catch (ex) { console.log('SQLERR5', ex); } }
1497 }
1498 });
1499 } else if (obj.databaseType == DB_POSTGRESQL) { // Postgres SQL
1500 Datastore.query(query, args, function (error, results) {
1501 if (error != null) {
1502 if (func) try { func(error); } catch (ex) { console.log('SQLERR4', ex); }
1503 } else {
1504 var docs = [];
1505 if ((results.command == 'INSERT') && (results.rows != null) && (results.rows.length == 1)) { docs = results.rows[0]; }
1506 else if (results.command == 'SELECT') {
1507 for (var i in results.rows) {
1508 if (results.rows[i].doc) {
1509 if (typeof results.rows[i].doc == 'string') {
1510 docs.push(JSON.parse(results.rows[i].doc));
1511 } else {
1512 docs.push(results.rows[i].doc);
1513 }
1514 } else if (results.rows[i].count && (results.rows.length == 1)) {
1515 // This is a SELECT COUNT() operation
1516 docs = parseInt(results.rows[i].count);
1517 }
1518 }
1519 }
1520 if (func) { try { func(null, docs, results); } catch (ex) { console.log('SQLERR5', ex); } }
1521 }
1522 });
1523 }
1524 }
1525
1526 // Exec on the database
1527 function sqlDbExec(query, args, func) {
1528 if (obj.databaseType == DB_MARIADB) { // MariaDB
1529 Datastore.getConnection()
1530 .then(function (conn) {
1531 conn.query(query, args)
1532 .then(function (rows) {
1533 conn.release();
1534 if (func) try { func(null, rows[0]); } catch (ex) { console.log(ex); }
1535 })
1536 .catch(function (err) { conn.release(); if (func) try { func(err); } catch (ex) { console.log(ex); } });
1537 }).catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } });
1538 } else if ((obj.databaseType == DB_MYSQL) || (obj.databaseType == DB_POSTGRESQL)) { // MySQL or Postgres SQL
1539 Datastore.query(query, args, function (error, results, fields) {
1540 if (func) try { func(error, results ? results[0] : null); } catch (ex) { console.log(ex); }
1541 });
1542 }
1543 }
1544
1545 // Execute a batch of commands on the database
1546 function sqlDbBatchExec(queries, func) {
1547 if (obj.databaseType == DB_MARIADB) { // MariaDB
1548 Datastore.getConnection()
1549 .then(function (conn) {
1550 var Promises = [];
1551 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])); } }
1552 Promise.all(Promises)
1553 .then(function (rows) { conn.release(); if (func) { try { func(null); } catch (ex) { console.log(ex); } } })
1554 .catch(function (err) { conn.release(); if (func) { try { func(err); } catch (ex) { console.log(ex); } } });
1555 })
1556 .catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } });
1557 } else if (obj.databaseType == DB_MYSQL) { // MySQL
1558 Datastore.getConnection(function(err, connection) {
1559 if (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } return; }
1560 var Promises = [];
1561 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])); } }
1562 Promise.all(Promises)
1563 .then(function (error, results, fields) { connection.release(); if (func) { try { func(error, results); } catch (ex) { console.log(ex); } } })
1564 .catch(function (error, results, fields) { connection.release(); if (func) { try { func(error); } catch (ex) { console.log(ex); } } });
1565 });
1566 } else if (obj.databaseType == DB_POSTGRESQL) { // Postgres
1567 var Promises = [];
1568 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])); } }
1569 Promise.all(Promises)
1570 .then(function (error, results, fields) { if (func) { try { func(error, results); } catch (ex) { console.log(ex); } } })
1571 .catch(function (error, results, fields) { if (func) { try { func(error); } catch (ex) { console.log(ex); } } });
1572 }
1573 }
1574
1575 function setupFunctions(func) {
1576 if (obj.databaseType == DB_SQLITE) {
1577 // Database actions on the main collection. SQLite3: https://www.linode.com/docs/guides/getting-started-with-nodejs-sqlite/
1578 obj.Set = function (value, func) {
1579 obj.dbCounters.fileSet++;
1580 var extra = null, extraex = null;
1581 value = common.escapeLinksFieldNameEx(value);
1582 if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
1583 if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
1584 if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
1585 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);
1586 }
1587 obj.SetRaw = function (value, func) {
1588 obj.dbCounters.fileSet++;
1589 var extra = null, extraex = null;
1590 if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
1591 if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
1592 if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
1593 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);
1594 }
1595 obj.Get = function (_id, func) {
1596 sqlDbQuery('SELECT doc FROM main WHERE id = $1', [_id], function (err, docs) {
1597 if ((docs != null) && (docs.length > 0)) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
1598 func(err, performTypedRecordDecrypt(docs));
1599 });
1600 }
1601 obj.GetAll = function (func) {
1602 sqlDbQuery('SELECT domain, doc FROM main', null, function (err, docs) {
1603 if ((docs != null) && (docs.length > 0)) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
1604 func(err, performTypedRecordDecrypt(docs));
1605 });
1606 }
1607 obj.GetHash = function (id, func) {
1608 sqlDbQuery('SELECT doc FROM main WHERE id = $1', [id], function (err, docs) {
1609 if ((docs != null) && (docs.length > 0)) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
1610 func(err, performTypedRecordDecrypt(docs));
1611 });
1612 }
1613 obj.GetAllTypeNoTypeField = function (type, domain, func) {
1614 sqlDbQuery('SELECT doc FROM main WHERE type = $1 AND domain = $2', [type, domain], function (err, docs) {
1615 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]); } } }
1616 func(err, performTypedRecordDecrypt(docs));
1617 });
1618 };
1619 obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
1620 if (limit == 0) { limit = -1; } // In SQLite, no limit is -1
1621 if (id && (id != '')) {
1622 sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra IN (' + dbMergeSqlArray(meshes) + ')) ORDER BY LOWER(json_extract(doc, \'$.name\')) LIMIT $4 OFFSET $5', [id, type, domain, limit, skip], function (err, docs) {
1623 if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
1624 func(err, performTypedRecordDecrypt(docs));
1625 });
1626 } else {
1627 if (extrasids == null) {
1628 sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra IN (' + dbMergeSqlArray(meshes) + ')) ORDER BY LOWER(json_extract(doc, \'$.name\')) LIMIT $3 OFFSET $4', [type, domain, limit, skip], function (err, docs) {
1629 if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
1630 func(err, performTypedRecordDecrypt(docs));
1631 });
1632 } else {
1633 sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra IN (' + dbMergeSqlArray(meshes) + ')) OR (id IN (' + dbMergeSqlArray(extrasids) + '))) ORDER BY LOWER(json_extract(doc, \'$.name\')) LIMIT $3 OFFSET $4', [type, domain, limit, skip], function (err, docs) {
1634 if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
1635 func(err, performTypedRecordDecrypt(docs));
1636 });
1637 }
1638 }
1639 };
1640 obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
1641 if (id && (id != '')) {
1642 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) {
1643 func(err, (err == null) ? docs[0]['COUNT(doc)'] : null);
1644 });
1645 } else {
1646 if (extrasids == null) {
1647 sqlDbQuery('SELECT COUNT(doc) FROM main WHERE (type = $1) AND (domain = $2) AND (extra IN (' + dbMergeSqlArray(meshes) + '))', [type, domain], function (err, docs) {
1648 func(err, (err == null) ? docs[0]['COUNT(doc)'] : null);
1649 });
1650 } else {
1651 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) {
1652 func(err, (err == null) ? docs[0]['COUNT(doc)'] : null);
1653 });
1654 }
1655 }
1656 };
1657 obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
1658 if (id && (id != '')) {
1659 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) {
1660 if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
1661 func(err, performTypedRecordDecrypt(docs));
1662 });
1663 } else {
1664 sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra IN (' + dbMergeSqlArray(nodes) + '))', [type, domain], function (err, docs) {
1665 if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
1666 func(err, performTypedRecordDecrypt(docs));
1667 });
1668 }
1669 };
1670 obj.GetAllType = function (type, func) {
1671 sqlDbQuery('SELECT doc FROM main WHERE type = $1', [type], function (err, docs) {
1672 if (docs != null) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
1673 func(err, performTypedRecordDecrypt(docs));
1674 });
1675 }
1676 obj.GetAllIdsOfType = function (ids, domain, type, func) {
1677 sqlDbQuery('SELECT doc FROM main WHERE (id IN (' + dbMergeSqlArray(ids) + ')) AND domain = $1 AND type = $2', [domain, type], function (err, docs) {
1678 if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
1679 func(err, performTypedRecordDecrypt(docs));
1680 });
1681 }
1682 obj.GetUserWithEmail = function (domain, email, func) {
1683 sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extra = $2', [domain, 'email/' + email], function (err, docs) {
1684 if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
1685 func(err, performTypedRecordDecrypt(docs));
1686 });
1687 }
1688 obj.GetUserWithVerifiedEmail = function (domain, email, func) {
1689 sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extra = $2', [domain, 'email/' + email], function (err, docs) {
1690 if (docs != null) { for (var i in docs) { delete docs[i].type; if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
1691 func(err, performTypedRecordDecrypt(docs));
1692 });
1693 }
1694 obj.Remove = function (id, func) { sqlDbQuery('DELETE FROM main WHERE id = $1', [id], func); };
1695 obj.RemoveAll = function (func) { sqlDbQuery('DELETE FROM main', null, func); };
1696 obj.RemoveAllOfType = function (type, func) { sqlDbQuery('DELETE FROM main WHERE type = $1', [type], func); };
1697 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
1698 obj.RemoveMeshDocuments = function (id, func) { sqlDbQuery('DELETE FROM main WHERE extra = $1', [id], function () { sqlDbQuery('DELETE FROM main WHERE id = $1', ['nt' + id], func); }); };
1699 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]); } }); };
1700 obj.DeleteDomain = function (domain, func) { sqlDbQuery('DELETE FROM main WHERE domain = $1', [domain], func); };
1701 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); } };
1702 obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
1703 obj.getLocalAmtNodes = function (func) {
1704 sqlDbQuery('SELECT doc FROM main WHERE (type = \'node\') AND (extraex IS NULL)', null, function (err, docs) {
1705 if (docs != null) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
1706 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);
1707 });
1708 };
1709 obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) {
1710 sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extraex = $2', [domainid, 'uuid/' + uuid], function (err, docs) {
1711 if (docs != null) { for (var i in docs) { if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); } } }
1712 func(err, docs);
1713 });
1714 };
1715 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)']) }); } }
1716
1717 // Database actions on the events collection
1718 obj.GetAllEvents = function (func) {
1719 sqlDbQuery('SELECT doc FROM events', null, func);
1720 };
1721 obj.StoreEvent = function (event, func) {
1722 obj.dbCounters.eventsSet++;
1723 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) {
1724 if(func){ func(); }
1725 if ((err == null) && (docs[0].id)) {
1726 for (var i in event.ids) {
1727 if (event.ids[i] != '*') {
1728 obj.pendingTransfer++;
1729 sqlDbQuery('INSERT INTO eventids VALUES ($1, $2)', [docs[0].id, event.ids[i]], function(){ if(func){ func(); } });
1730 }
1731 }
1732 }
1733 });
1734 };
1735 obj.GetEvents = function (ids, domain, filter, func) {
1736 var query = "SELECT doc FROM events ";
1737 var dataarray = [domain];
1738 if (ids.indexOf('*') >= 0) {
1739 query = query + "WHERE (domain = $1";
1740 if (filter != null) {
1741 query = query + " AND action = $2";
1742 dataarray.push(filter);
1743 }
1744 query = query + ") ORDER BY time DESC";
1745 } else {
1746 query = query + 'JOIN eventids ON id = fkid WHERE (domain = $1 AND (target IN (' + dbMergeSqlArray(ids) + '))';
1747 if (filter != null) {
1748 query = query + " AND action = $2";
1749 dataarray.push(filter);
1750 }
1751 query = query + ") GROUP BY id ORDER BY time DESC ";
1752 }
1753 sqlDbQuery(query, dataarray, func);
1754 };
1755 obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
1756 var query = "SELECT doc FROM events ";
1757 var dataarray = [domain];
1758 if (ids.indexOf('*') >= 0) {
1759 query = query + "WHERE (domain = $1";
1760 if (filter != null) {
1761 query = query + " AND action = $2) ORDER BY time DESC LIMIT $3";
1762 dataarray.push(filter);
1763 } else {
1764 query = query + ") ORDER BY time DESC LIMIT $2";
1765 }
1766 } else {
1767 query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND (target IN (" + dbMergeSqlArray(ids) + "))";
1768 if (filter != null) {
1769 query = query + " AND action = $2) GROUP BY id ORDER BY time DESC LIMIT $3";
1770 dataarray.push(filter);
1771 } else {
1772 query = query + ") GROUP BY id ORDER BY time DESC LIMIT $2";
1773 }
1774 }
1775 dataarray.push(limit);
1776 sqlDbQuery(query, dataarray, func);
1777 };
1778 obj.GetUserEvents = function (ids, domain, userid, filter, func) {
1779 var query = "SELECT doc FROM events ";
1780 var dataarray = [domain, userid];
1781 if (ids.indexOf('*') >= 0) {
1782 query = query + "WHERE (domain = $1 AND userid = $2";
1783 if (filter != null) {
1784 query = query + " AND action = $3";
1785 dataarray.push(filter);
1786 }
1787 query = query + ") ORDER BY time DESC";
1788 } else {
1789 query = query + 'JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target IN (' + dbMergeSqlArray(ids) + '))';
1790 if (filter != null) {
1791 query = query + " AND action = $3";
1792 dataarray.push(filter);
1793 }
1794 query = query + ") GROUP BY id ORDER BY time DESC";
1795 }
1796 sqlDbQuery(query, dataarray, func);
1797 };
1798 obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
1799 var query = "SELECT doc FROM events ";
1800 var dataarray = [domain, userid];
1801 if (ids.indexOf('*') >= 0) {
1802 query = query + "WHERE (domain = $1 AND userid = $2";
1803 if (filter != null) {
1804 query = query + " AND action = $3) ORDER BY time DESC LIMIT $4";
1805 dataarray.push(filter);
1806 } else {
1807 query = query + ") ORDER BY time DESC LIMIT $3";
1808 }
1809 } else {
1810 query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target IN (" + dbMergeSqlArray(ids) + "))";
1811 if (filter != null) {
1812 query = query + " AND action = $3) GROUP BY id ORDER BY time DESC LIMIT $4";
1813 dataarray.push(filter);
1814 } else {
1815 query = query + ") GROUP BY id ORDER BY time DESC LIMIT $3";
1816 }
1817 }
1818 dataarray.push(limit);
1819 sqlDbQuery(query, dataarray, func);
1820 };
1821 obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
1822 if (ids.indexOf('*') >= 0) {
1823 sqlDbQuery('SELECT doc FROM events WHERE ((domain = $1) AND (time BETWEEN $2 AND $3)) ORDER BY time', [domain, start, end], func);
1824 } else {
1825 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);
1826 }
1827 };
1828 //obj.GetUserLoginEvents = function (domain, userid, func) { } // TODO
1829 obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
1830 var query = "SELECT doc FROM events WHERE (nodeid = $1 AND domain = $2";
1831 var dataarray = [nodeid, domain];
1832 if (filter != null) {
1833 query = query + " AND action = $3) ORDER BY time DESC LIMIT $4";
1834 dataarray.push(filter);
1835 } else {
1836 query = query + ") ORDER BY time DESC LIMIT $3";
1837 }
1838 dataarray.push(limit);
1839 sqlDbQuery(query, dataarray, func);
1840 };
1841 obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
1842 var query = "SELECT doc FROM events WHERE (nodeid = $1) AND (domain = $2) AND ((userid = $3) OR (userid IS NULL)) ";
1843 var dataarray = [nodeid, domain, userid];
1844 if (filter != null) {
1845 query = query + "AND (action = $4) ORDER BY time DESC LIMIT $5";
1846 dataarray.push(filter);
1847 } else {
1848 query = query + "ORDER BY time DESC LIMIT $4";
1849 }
1850 dataarray.push(limit);
1851 sqlDbQuery(query, dataarray, func);
1852 };
1853 obj.RemoveAllEvents = function (domain) { sqlDbQuery('DELETE FROM events', null, function (err, docs) { }); };
1854 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) { }); };
1855 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) { }); };
1856 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); }); }
1857
1858 // Database actions on the power collection
1859 obj.getAllPower = function (func) { sqlDbQuery('SELECT doc FROM power', null, func); };
1860 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); };
1861 obj.getPowerTimeline = function (nodeid, func) { sqlDbQuery('SELECT doc FROM power WHERE ((nodeid = $1) OR (nodeid = \'*\')) ORDER BY time ASC', [nodeid], func); };
1862 obj.removeAllPowerEvents = function () { sqlDbQuery('DELETE FROM power', null, function (err, docs) { }); };
1863 obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; sqlDbQuery('DELETE FROM power WHERE nodeid = $1', [nodeid], function (err, docs) { }); };
1864
1865 // Database actions on the SMBIOS collection
1866 obj.GetAllSMBIOS = function (func) { sqlDbQuery('SELECT doc FROM smbios', null, func); };
1867 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); };
1868 obj.RemoveSMBIOS = function (id) { sqlDbQuery('DELETE FROM smbios WHERE id = $1', [id], function (err, docs) { }); };
1869 obj.GetSMBIOS = function (id, func) { sqlDbQuery('SELECT doc FROM smbios WHERE id = $1', [id], func); };
1870
1871 // Database actions on the Server Stats collection
1872 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); };
1873 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
1874
1875 // Read a configuration file from the database
1876 obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
1877
1878 // Write a configuration file to the database
1879 obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
1880
1881 // List all configuration files
1882 obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM main WHERE type = "cfile" ORDER BY id', func); }
1883
1884 // Get database information (TODO: Complete this)
1885 obj.getDbStats = function (func) {
1886 obj.stats = { c: 4 };
1887 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); } });
1888 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); } });
1889 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); } });
1890 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); } });
1891 }
1892
1893 // Plugin operations
1894 if (obj.pluginsActive) {
1895 obj.addPlugin = function (plugin, func) { sqlDbQuery('INSERT INTO plugin VALUES (NULL, $1)', [JSON.stringify(plugin)], func); }; // Add a plugin
1896 obj.getPlugins = function (func) { sqlDbQuery('SELECT JSON_INSERT(doc, "$._id", id) as doc FROM plugin', null, func); }; // Get all plugins
1897 obj.getPlugin = function (id, func) { sqlDbQuery('SELECT JSON_INSERT(doc, "$._id", id) as doc FROM plugin WHERE id = $1', [id], func); }; // Get plugin
1898 obj.deletePlugin = function (id, func) { sqlDbQuery('DELETE FROM plugin WHERE id = $1', [id], func); }; // Delete plugin
1899 obj.setPluginStatus = function (id, status, func) { sqlDbQuery('UPDATE plugin SET doc=JSON_SET(doc,"$.status",$1) WHERE id=$2', [status,id], func); };
1900 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); };
1901 obj.getPluginPermissions = function (pluginName, func) { sqlDbQuery('SELECT doc FROM pluginpermissions WHERE id = $1', ['pluginpermission//' + pluginName], function(err, docs) { if (docs && docs.length > 0) { func(null, [docs[0].doc]); } else { func(null, []); } }); };
1902 obj.setPluginPermissions = function (pluginName, data, func) { delete data._id; sqlDbQuery('INSERT INTO pluginpermissions VALUES ($1, $2) ON DUPLICATE KEY UPDATE doc = $2', ['pluginpermission//' + pluginName, JSON.stringify(data)], func); };
1903 }
1904 } else if (obj.databaseType == DB_ACEBASE) {
1905 // Database actions on the main collection. AceBase: https://github.com/appy-one/acebase
1906 obj.Set = function (data, func) {
1907 data = common.escapeLinksFieldNameEx(data);
1908 var xdata = performTypedRecordEncrypt(data);
1909 obj.dbCounters.fileSet++;
1910 obj.file.ref('meshcentral').child(encodeURIComponent(xdata._id)).set(common.aceEscapeFieldNames(xdata)).then(function (ref) { if (func) { func(); } })
1911 };
1912 obj.Get = function (id, func) {
1913 obj.file.ref('meshcentral').child(encodeURIComponent(id)).get(function (snapshot) {
1914 if (snapshot.exists()) { func(null, performTypedRecordDecrypt([common.aceUnEscapeFieldNames(snapshot.val())])); } else { func(null, []); }
1915 });
1916 };
1917 obj.GetAll = function (func) {
1918 obj.file.ref('meshcentral').get(function(snapshot) {
1919 const val = snapshot.val();
1920 const docs = Object.keys(val).map(function(key) { return val[key]; });
1921 func(null, common.aceUnEscapeAllFieldNames(docs));
1922 });
1923 };
1924 obj.GetHash = function (id, func) {
1925 obj.file.ref('meshcentral').child(encodeURIComponent(id)).get({ include: ['hash'] }, function (snapshot) {
1926 if (snapshot.exists()) { func(null, snapshot.val()); } else { func(null, null); }
1927 });
1928 };
1929 obj.GetAllTypeNoTypeField = function (type, domain, func) {
1930 obj.file.query('meshcentral').filter('type', '==', type).filter('domain', '==', domain).get({ exclude: ['type'] }, function (snapshots) {
1931 const docs = [];
1932 for (var i in snapshots) { const x = snapshots[i].val(); docs.push(x); }
1933 func(null, common.aceUnEscapeAllFieldNames(docs));
1934 });
1935 }
1936 obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
1937 if (meshes.length == 0) { func(null, []); return; }
1938 var query = obj.file.query('meshcentral').sort('name', true).skip(skip).take(limit).filter('type', '==', type).filter('domain', '==', domain);
1939 if (id) { query = query.filter('_id', '==', id); }
1940 if (extrasids == null) {
1941 query = query.filter('meshid', 'in', meshes);
1942 query.get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); });
1943 } else {
1944 // TODO: This is a slow query as we did not find a filter-or-filter, so we query everything and filter manualy.
1945 query.get(function (snapshots) {
1946 const docs = [];
1947 for (var i in snapshots) { const x = snapshots[i].val(); if ((extrasids.indexOf(x._id) >= 0) || (meshes.indexOf(x.meshid) >= 0)) { docs.push(x); } }
1948 func(null, performTypedRecordDecrypt(docs));
1949 });
1950 }
1951 };
1952 obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
1953 var query = obj.file.query('meshcentral').filter('type', '==', type).filter('domain', '==', domain).filter('nodeid', 'in', nodes);
1954 if (id) { query = query.filter('_id', '==', id); }
1955 query.get(function (snapshots) { const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, performTypedRecordDecrypt(docs)); });
1956 };
1957 obj.GetAllType = function (type, func) {
1958 obj.file.query('meshcentral').filter('type', '==', type).get(function (snapshots) {
1959 const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); }
1960 func(null, common.aceUnEscapeAllFieldNames(performTypedRecordDecrypt(docs)));
1961 });
1962 };
1963 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)); }); };
1964 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)); }); };
1965 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)); }); };
1966 obj.Remove = function (id, func) { obj.file.ref('meshcentral').child(encodeURIComponent(id)).remove().then(function () { if (func) { func(); } }); };
1967 obj.RemoveAll = function (func) { obj.file.query('meshcentral').remove().then(function () { if (func) { func(); } }); };
1968 obj.RemoveAllOfType = function (type, func) { obj.file.query('meshcentral').filter('type', '==', type).remove().then(function () { if (func) { func(); } }); };
1969 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
1970 obj.RemoveMeshDocuments = function (id) { obj.file.query('meshcentral').filter('meshid', '==', id).remove(); obj.file.ref('meshcentral').child(encodeURIComponent('nt' + id)).remove(); };
1971 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]); } }); };
1972 obj.DeleteDomain = function (domain, func) { obj.file.query('meshcentral').filter('domain', '==', domain).remove().then(function () { if (func) { func(); } }); };
1973 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); } };
1974 obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
1975 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)); }); };
1976 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)); }); };
1977 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); }); } }
1978
1979 // Database actions on the events collection
1980 obj.GetAllEvents = function (func) {
1981 obj.file.ref('events').get(function (snapshot) {
1982 const val = snapshot.val();
1983 const docs = Object.keys(val).map(function(key) { return val[key]; });
1984 func(null, docs);
1985 })
1986 };
1987 obj.StoreEvent = function (event, func) {
1988 if (typeof event.account == 'object') { event = Object.assign({}, event); event.account = common.aceEscapeFieldNames(event.account); }
1989 obj.dbCounters.eventsSet++;
1990 obj.file.ref('events').push(event).then(function (userRef) { if (func) { func(); } });
1991 };
1992 obj.GetEvents = function (ids, domain, filter, func) {
1993 // This request is slow since we have not found a .filter() that will take two arrays and match a single item.
1994 if (filter != null) {
1995 obj.file.query('events').filter('domain', '==', domain).filter('action', '==', filter).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) {
1996 const docs = [];
1997 for (var i in snapshots) {
1998 const doc = snapshots[i].val();
1999 if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue;
2000 var found = false;
2001 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
2002 if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); }
2003 }
2004 func(null, docs);
2005 });
2006 } else {
2007 obj.file.query('events').filter('domain', '==', domain).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) {
2008 const docs = [];
2009 for (var i in snapshots) {
2010 const doc = snapshots[i].val();
2011 if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue;
2012 var found = false;
2013 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
2014 if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); }
2015 }
2016 func(null, docs);
2017 });
2018 }
2019 };
2020 obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
2021 // This request is slow since we have not found a .filter() that will take two arrays and match a single item.
2022 // TODO: Request a new AceBase feature for a 'array:contains-one-of' filter:
2023 // obj.file.indexes.create('events', 'ids', { type: 'array' });
2024 // db.query('events').filter('ids', 'array:contains-one-of', ids)
2025 if (filter != null) {
2026 obj.file.query('events').filter('domain', '==', domain).filter('action', '==', filter).take(limit).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) {
2027 const docs = [];
2028 for (var i in snapshots) {
2029 const doc = snapshots[i].val();
2030 if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue;
2031 var found = false;
2032 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
2033 if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); }
2034 }
2035 func(null, docs);
2036 });
2037 } else {
2038 obj.file.query('events').filter('domain', '==', domain).take(limit).sort('time', false).get({ exclude: ['_id', 'domain', 'node', 'type'] }, function (snapshots) {
2039 const docs = [];
2040 for (var i in snapshots) {
2041 const doc = snapshots[i].val();
2042 if ((doc.ids == null) || (!Array.isArray(doc.ids))) continue;
2043 var found = false;
2044 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
2045 if (found) { delete doc.ids; if (typeof doc.account == 'object') { doc.account = common.aceUnEscapeFieldNames(doc.account); } docs.push(doc); }
2046 }
2047 func(null, docs);
2048 });
2049 }
2050 };
2051 obj.GetUserEvents = function (ids, domain, userid, filter, func) {
2052 if (filter != null) {
2053 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); });
2054 } else {
2055 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); });
2056 }
2057 };
2058 obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
2059 if (filter != null) {
2060 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); });
2061 } else {
2062 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); });
2063 }
2064 };
2065 obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
2066 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); });
2067 };
2068 obj.GetUserLoginEvents = function (domain, userid, func) {
2069 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); });
2070 };
2071 obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
2072 if (filter != null) {
2073 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); });
2074 } else {
2075 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); });
2076 }
2077 };
2078 obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
2079 if (filter != null) {
2080 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); });
2081 } else {
2082 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); });
2083 }
2084 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); });
2085 };
2086 obj.RemoveAllEvents = function (domain) {
2087 obj.file.query('events').filter('domain', '==', domain).remove().then(function () { if (func) { func(); } });;
2088 };
2089 obj.RemoveAllNodeEvents = function (domain, nodeid) {
2090 if ((domain == null) || (nodeid == null)) return;
2091 obj.file.query('events').filter('domain', '==', domain).filter('nodeid', '==', nodeid).remove().then(function () { if (func) { func(); } });;
2092 };
2093 obj.RemoveAllUserEvents = function (domain, userid) {
2094 if ((domain == null) || (userid == null)) return;
2095 obj.file.query('events').filter('domain', '==', domain).filter('userid', '==', userid).remove().then(function () { if (func) { func(); } });;
2096 };
2097 obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) {
2098 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); });
2099 }
2100
2101 // Database actions on the power collection
2102 obj.getAllPower = function (func) {
2103 obj.file.ref('power').get(function (snapshot) {
2104 const val = snapshot.val();
2105 const docs = Object.keys(val).map(function(key) { return val[key]; });
2106 func(null, docs);
2107 });
2108 };
2109 obj.storePowerEvent = function (event, multiServer, func) {
2110 if (multiServer != null) { event.server = multiServer.serverid; }
2111 obj.file.ref('power').push(event).then(function (userRef) { if (func) { func(); } });
2112 };
2113 obj.getPowerTimeline = function (nodeid, func) {
2114 obj.file.query('power').filter('nodeid', 'in', ['*', nodeid]).sort('time').get({ exclude: ['_id', 'nodeid', 's'] }, function (snapshots) {
2115 const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs);
2116 });
2117 };
2118 obj.removeAllPowerEvents = function () {
2119 obj.file.ref('power').remove().then(function () { if (func) { func(); } });
2120 };
2121 obj.removeAllPowerEventsForNode = function (nodeid) {
2122 if (nodeid == null) return;
2123 obj.file.query('power').filter('nodeid', '==', nodeid).remove().then(function () { if (func) { func(); } });
2124 };
2125
2126 // Database actions on the SMBIOS collection
2127 if (obj.smbiosfile != null) {
2128 obj.GetAllSMBIOS = function (func) {
2129 obj.file.ref('smbios').get(function (snapshot) {
2130 const val = snapshot.val();
2131 const docs = Object.keys(val).map(function(key) { return val[key]; });
2132 func(null, docs);
2133 });
2134 };
2135 obj.SetSMBIOS = function (smbios, func) {
2136 obj.file.ref('meshcentral/' + encodeURIComponent(smbios._id)).set(smbios).then(function (ref) { if (func) { func(); } })
2137 };
2138 obj.RemoveSMBIOS = function (id) {
2139 obj.file.query('smbios').filter('_id', '==', id).remove().then(function () { if (func) { func(); } });
2140 };
2141 obj.GetSMBIOS = function (id, func) {
2142 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); });
2143 };
2144 }
2145
2146 // Database actions on the Server Stats collection
2147 obj.SetServerStats = function (data, func) {
2148 obj.file.ref('stats').push(data).then(function (userRef) { if (func) { func(); } });
2149 };
2150 obj.GetServerStats = function (hours, func) {
2151 var t = new Date();
2152 t.setTime(t.getTime() - (60 * 60 * 1000 * hours));
2153 obj.file.query('stats').filter('time', '>', t).get({ exclude: ['_id', 'cpu'] }, function (snapshots) {
2154 const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs);
2155 });
2156 };
2157
2158 // Read a configuration file from the database
2159 obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
2160
2161 // Write a configuration file to the database
2162 obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
2163
2164 // List all configuration files
2165 obj.listConfigFiles = function (func) {
2166 obj.file.query('meshcentral').filter('type', '==', 'cfile').sort('_id').get(function (snapshots) {
2167 const docs = []; for (var i in snapshots) { docs.push(snapshots[i].val()); } func(null, docs);
2168 });
2169 }
2170
2171 // Get database information
2172 obj.getDbStats = function (func) {
2173 obj.stats = { c: 5 };
2174 obj.file.ref('meshcentral').count().then(function (count) { obj.stats.meshcentral = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
2175 obj.file.ref('events').count().then(function (count) { obj.stats.events = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
2176 obj.file.ref('power').count().then(function (count) { obj.stats.power = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
2177 obj.file.ref('smbios').count().then(function (count) { obj.stats.smbios = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
2178 obj.file.ref('stats').count().then(function (count) { obj.stats.serverstats = count; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
2179 }
2180
2181 // Plugin operations
2182 if (obj.pluginsActive) {
2183 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
2184 obj.getPlugins = function (func) {
2185 obj.file.ref('plugin').get({ exclude: ['type'] }, function (snapshot) {
2186 const val = snapshot.val();
2187 const docs = Object.keys(val).map(function(key) { return val[key]; }).sort(function(a, b) { return a.name < b.name ? -1 : 1 });
2188 func(null, docs);
2189 });
2190 }; // Get all plugins
2191 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
2192 obj.deletePlugin = function (id, func) { obj.file.ref('plugin').child(encodeURIComponent(id)).remove().then(function () { if (func) { func(); } }); }; // Delete plugin
2193 obj.setPluginStatus = function (id, status, func) { obj.file.ref('plugin').child(encodeURIComponent(id)).update({ status: status }).then(function (ref) { if (func) { func(); } }) };
2194 obj.updatePlugin = function (id, args, func) { delete args._id; obj.file.ref('plugin').child(encodeURIComponent(id)).set(args).then(function (ref) { if (func) { func(); } }) };
2195 obj.getPluginPermissions = function (pluginName, func) { obj.file.ref('pluginpermissions').child('pluginpermission//' + pluginName).get(function(snapshot) { if (snapshot.exists()) { func(null, [snapshot.val()]); } else { func(null, []); } }); };
2196 obj.setPluginPermissions = function (pluginName, data, func) { delete data._id; obj.file.ref('pluginpermissions').child('pluginpermission//' + pluginName).set(data, func); };
2197 }
2198 } else if (obj.databaseType == DB_POSTGRESQL) {
2199 // Database actions on the main collection (Postgres)
2200 obj.Set = function (value, func) {
2201 obj.dbCounters.fileSet++;
2202 var extra = null, extraex = null;
2203 value = common.escapeLinksFieldNameEx(value);
2204 if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
2205 if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
2206 if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
2207 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);
2208 }
2209 obj.SetRaw = function (value, func) {
2210 obj.dbCounters.fileSet++;
2211 var extra = null, extraex = null;
2212 if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
2213 if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
2214 if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
2215 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);
2216 }
2217 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)); }); }
2218 obj.GetAll = function (func) { sqlDbQuery('SELECT domain, doc FROM main', null, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
2219 obj.GetHash = function (id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = $1', [id], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
2220 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)); }); };
2221 obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
2222 if (limit == 0) { limit = 0xFFFFFFFF; }
2223 if (id && (id != '')) {
2224 sqlDbQuery('SELECT doc FROM main WHERE (id = $1) AND (type = $2) AND (domain = $3) AND (extra = ANY ($4)) ORDER BY LOWER(doc->>\'name\') 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)); });
2225 } else {
2226 if (extrasids == null) {
2227 sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND (extra = ANY ($3)) ORDER BY LOWER(doc->>\'name\') 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);
2228 } else {
2229 sqlDbQuery('SELECT doc FROM main WHERE (type = $1) AND (domain = $2) AND ((extra = ANY ($3)) OR (id = ANY ($4))) ORDER BY LOWER(doc->>\'name\') 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)); });
2230 }
2231 }
2232 };
2233 obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
2234 if (id && (id != '')) {
2235 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); });
2236 } else {
2237 if (extrasids == null) {
2238 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);
2239 } else {
2240 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); });
2241 }
2242 }
2243 };
2244 obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
2245 if (id && (id != '')) {
2246 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)); });
2247 } else {
2248 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)); });
2249 }
2250 };
2251 obj.GetAllType = function (type, func) { sqlDbQuery('SELECT doc FROM main WHERE type = $1', [type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
2252 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)); }); }
2253 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)); }); }
2254 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)); }); }
2255 obj.Remove = function (id, func) { sqlDbQuery('DELETE FROM main WHERE id = $1', [id], func); };
2256 obj.RemoveAll = function (func) { sqlDbQuery('DELETE FROM main', null, func); };
2257 obj.RemoveAllOfType = function (type, func) { sqlDbQuery('DELETE FROM main WHERE type = $1', [type], func); };
2258 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
2259 obj.RemoveMeshDocuments = function (id, func) { sqlDbQuery('DELETE FROM main WHERE extra = $1', [id], function () { sqlDbQuery('DELETE FROM main WHERE id = $1', ['nt' + id], func); }); };
2260 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]); } }); };
2261 obj.DeleteDomain = function (domain, func) { sqlDbQuery('DELETE FROM main WHERE domain = $1', [domain], func); };
2262 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); } };
2263 obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
2264 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); }); };
2265 obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = $1 AND extraex = $2', [domainid, 'uuid/' + uuid], func); };
2266 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)']) }); } }
2267
2268 // Database actions on the events collection
2269 obj.GetAllEvents = function (func) { sqlDbQuery('SELECT doc FROM events', null, func); };
2270 obj.StoreEvent = function (event, func) {
2271 obj.dbCounters.eventsSet++;
2272 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) {
2273 if(func){ func(); }
2274 if (docs.id) {
2275 for (var i in event.ids) {
2276 if (event.ids[i] != '*') {
2277 obj.pendingTransfer++;
2278 sqlDbQuery('INSERT INTO eventids VALUES ($1, $2)', [docs.id, event.ids[i]], function(){ if(func){ func(); } });
2279 }
2280 }
2281 }
2282 });
2283 };
2284 obj.GetEvents = function (ids, domain, filter, func) {
2285 var query = "SELECT doc FROM events ";
2286 var dataarray = [domain];
2287 if (ids.indexOf('*') >= 0) {
2288 query = query + "WHERE (domain = $1";
2289 if (filter != null) {
2290 query = query + " AND action = $2";
2291 dataarray.push(filter);
2292 }
2293 query = query + ") ORDER BY time DESC";
2294 } else {
2295 query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND (target = ANY ($2))";
2296 dataarray.push(ids);
2297 if (filter != null) {
2298 query = query + " AND action = $3";
2299 dataarray.push(filter);
2300 }
2301 query = query + ") GROUP BY id ORDER BY time DESC";
2302 }
2303 sqlDbQuery(query, dataarray, func);
2304 };
2305 obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
2306 var query = "SELECT doc FROM events ";
2307 var dataarray = [domain];
2308 if (ids.indexOf('*') >= 0) {
2309 query = query + "WHERE (domain = $1";
2310 if (filter != null) {
2311 query = query + " AND action = $2) ORDER BY time DESC LIMIT $3";
2312 dataarray.push(filter);
2313 } else {
2314 query = query + ") ORDER BY time DESC LIMIT $2";
2315 }
2316 } else {
2317 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.
2318 query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND (target = ANY ($2))";
2319 dataarray.push(ids);
2320 if (filter != null) {
2321 query = query + " AND action = $3) ORDER BY time DESC LIMIT $4";
2322 dataarray.push(filter);
2323 } else {
2324 query = query + ") ORDER BY time DESC LIMIT $3";
2325 }
2326 }
2327 dataarray.push(limit);
2328 sqlDbQuery(query, dataarray, func);
2329 };
2330 obj.GetUserEvents = function (ids, domain, userid, filter, func) {
2331 var query = "SELECT doc FROM events ";
2332 var dataarray = [domain, userid];
2333 if (ids.indexOf('*') >= 0) {
2334 query = query + "WHERE (domain = $1 AND userid = $2";
2335 if (filter != null) {
2336 query = query + " AND action = $3";
2337 dataarray.push(filter);
2338 }
2339 query = query + ") ORDER BY time DESC";
2340 } else {
2341 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.
2342 query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target = ANY ($3))";
2343 dataarray.push(ids);
2344 if (filter != null) {
2345 query = query + " AND action = $4";
2346 dataarray.push(filter);
2347 }
2348 query = query + ") GROUP BY id ORDER BY time DESC";
2349 }
2350 sqlDbQuery(query, dataarray, func);
2351 };
2352 obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
2353 var query = "SELECT doc FROM events ";
2354 var dataarray = [domain, userid];
2355 if (ids.indexOf('*') >= 0) {
2356 query = query + "WHERE (domain = $1 AND userid = $2";
2357 if (filter != null) {
2358 query = query + " AND action = $3) ORDER BY time DESC LIMIT $4 ";
2359 dataarray.push(filter);
2360 } else {
2361 query = query + ") ORDER BY time DESC LIMIT $3";
2362 }
2363 } else {
2364 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.
2365 query = query + "JOIN eventids ON id = fkid WHERE (domain = $1 AND userid = $2 AND (target = ANY ($3))";
2366 dataarray.push(ids);
2367 if (filter != null) {
2368 query = query + " AND action = $4) GROUP BY id ORDER BY time DESC LIMIT $5";
2369 dataarray.push(filter);
2370 } else {
2371 query = query + ") GROUP BY id ORDER BY time DESC LIMIT $4";
2372 }
2373 }
2374 dataarray.push(limit);
2375 sqlDbQuery(query, dataarray, func);
2376 };
2377 obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
2378 if (ids.indexOf('*') >= 0) {
2379 sqlDbQuery('SELECT doc FROM events WHERE ((domain = $1) AND (time BETWEEN $2 AND $3)) ORDER BY time', [domain, start, end], func);
2380 } else {
2381 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);
2382 }
2383 };
2384 //obj.GetUserLoginEvents = function (domain, userid, func) { } // TODO
2385 obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
2386 var query = "SELECT doc FROM events WHERE (nodeid = $1 AND domain = $2";
2387 var dataarray = [nodeid, domain];
2388 if (filter != null) {
2389 query = query + " AND action = $3) ORDER BY time DESC LIMIT $4";
2390 dataarray.push(filter);
2391 } else {
2392 query = query + ") ORDER BY time DESC LIMIT $3";
2393 }
2394 dataarray.push(limit);
2395 sqlDbQuery(query, dataarray, func);
2396 };
2397 obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
2398 var query = "SELECT doc FROM events WHERE (nodeid = $1 AND domain = $2 AND ((userid = $3) OR (userid IS NULL))";
2399 var dataarray = [nodeid, domain, userid];
2400 if (filter != null) {
2401 query = query + " AND action = $4) ORDER BY time DESC LIMIT $5";
2402 dataarray.push(filter);
2403 } else {
2404 query = query + ") ORDER BY time DESC LIMIT $4";
2405 }
2406 dataarray.push(limit);
2407 sqlDbQuery(query, dataarray, func);
2408 };
2409 obj.RemoveAllEvents = function (domain) { sqlDbQuery('DELETE FROM events', null, function (err, docs) { }); };
2410 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) { }); };
2411 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) { }); };
2412 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); }); }
2413
2414 // Database actions on the power collection
2415 obj.getAllPower = function (func) { sqlDbQuery('SELECT doc FROM power', null, func); };
2416 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); };
2417 obj.getPowerTimeline = function (nodeid, func) { sqlDbQuery('SELECT doc FROM power WHERE ((nodeid = $1) OR (nodeid = \'*\')) ORDER BY time ASC', [nodeid], func); };
2418 obj.removeAllPowerEvents = function () { sqlDbQuery('DELETE FROM power', null, function (err, docs) { }); };
2419 obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; sqlDbQuery('DELETE FROM power WHERE nodeid = $1', [nodeid], function (err, docs) { }); };
2420
2421 // Database actions on the SMBIOS collection
2422 obj.GetAllSMBIOS = function (func) { sqlDbQuery('SELECT doc FROM smbios', null, func); };
2423 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); };
2424 obj.RemoveSMBIOS = function (id) { sqlDbQuery('DELETE FROM smbios WHERE id = $1', [id], function (err, docs) { }); };
2425 obj.GetSMBIOS = function (id, func) { sqlDbQuery('SELECT doc FROM smbios WHERE id = $1', [id], func); };
2426
2427 // Database actions on the Server Stats collection
2428 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); };
2429 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
2430
2431 // Read a configuration file from the database
2432 obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
2433
2434 // Write a configuration file to the database
2435 obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
2436
2437 // List all configuration files
2438 obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM main WHERE type = "cfile" ORDER BY id', func); }
2439
2440 // Get database information (TODO: Complete this)
2441 obj.getDbStats = function (func) {
2442 obj.stats = { c: 4 };
2443 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); } });
2444 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); } });
2445 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); } });
2446 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); } });
2447 }
2448
2449 // Plugin operations
2450 if (obj.pluginsActive) {
2451 obj.addPlugin = function (plugin, func) { sqlDbQuery('INSERT INTO plugin VALUES (DEFAULT, $1)', [plugin], func); }; // Add a plugin
2452 obj.getPlugins = function (func) { sqlDbQuery("SELECT doc::jsonb || ('{\"_id\":' || plugin.id || '}')::jsonb as doc FROM plugin", null, func); }; // Get all plugins
2453 obj.getPlugin = function (id, func) { sqlDbQuery("SELECT doc::jsonb || ('{\"_id\":' || plugin.id || '}')::jsonb as doc FROM plugin WHERE id = $1", [id], func); }; // Get plugin
2454 obj.deletePlugin = function (id, func) { sqlDbQuery('DELETE FROM plugin WHERE id = $1', [id], func); }; // Delete plugin
2455 obj.setPluginStatus = function (id, status, func) { sqlDbQuery("UPDATE plugin SET doc= jsonb_set(doc::jsonb,'{status}',$1) WHERE id=$2", [status,id], func); };
2456 obj.updatePlugin = function (id, args, func) { delete args._id; sqlDbQuery('UPDATE plugin SET doc= doc::jsonb || ($1) WHERE id=$2', [args,id], func); };
2457 obj.getPluginPermissions = function (pluginName, func) { sqlDbQuery('SELECT doc FROM pluginpermissions WHERE id = $1', ['pluginpermission//' + pluginName], function(err, docs) { if (docs && docs.length > 0 && docs[0].doc) { func(null, [typeof docs[0].doc === 'string' ? JSON.parse(docs[0].doc) : docs[0].doc]); } else { func(null, []); } }); };
2458 obj.setPluginPermissions = function (pluginName, data, func) { delete data._id; sqlDbQuery('INSERT INTO pluginpermissions VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET doc = $2', ['pluginpermission//' + pluginName, JSON.stringify(data)], func); };
2459 }
2460 } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
2461 // Database actions on the main collection (MariaDB or MySQL)
2462 obj.Set = function (value, func) {
2463 obj.dbCounters.fileSet++;
2464 var extra = null, extraex = null;
2465 value = common.escapeLinksFieldNameEx(value);
2466 if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
2467 if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
2468 if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
2469 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);
2470 }
2471 obj.SetRaw = function (value, func) {
2472 obj.dbCounters.fileSet++;
2473 var extra = null, extraex = null;
2474 if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
2475 if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
2476 if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
2477 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);
2478 }
2479 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)); }); }
2480 obj.GetAll = function (func) { sqlDbQuery('SELECT domain, doc FROM main', null, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
2481 obj.GetHash = function (id, func) { sqlDbQuery('SELECT doc FROM main WHERE id = ?', [id], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
2482 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)); }); };
2483 obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
2484 if (limit == 0) { limit = 0xFFFFFFFF; }
2485 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.
2486 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.
2487 if (id && (id != '')) {
2488 sqlDbQuery('SELECT doc FROM main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?) ORDER BY LOWER(JSON_UNQUOTE(JSON_EXTRACT(doc, \'$.name\'))) 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)); });
2489 } else {
2490 sqlDbQuery('SELECT doc FROM main WHERE type = ? AND domain = ? AND (extra IN (?) OR id IN (?)) ORDER BY LOWER(JSON_UNQUOTE(JSON_EXTRACT(doc, \'$.name\'))) 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)); });
2491 }
2492 };
2493 obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
2494 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.
2495 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.
2496 if (id && (id != '')) {
2497 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); });
2498 } else {
2499 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); });
2500 }
2501 };
2502 obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
2503 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.
2504 if (id && (id != '')) {
2505 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)); });
2506 } else {
2507 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)); });
2508 }
2509 };
2510 obj.GetAllType = function (type, func) { sqlDbQuery('SELECT doc FROM main WHERE type = ?', [type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); }
2511 obj.GetAllIdsOfType = function (ids, domain, type, func) {
2512 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.
2513 sqlDbQuery('SELECT doc FROM main WHERE id IN (?) AND domain = ? AND type = ?', [ids, domain, type], function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
2514 }
2515 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)); }); }
2516 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)); }); }
2517 obj.Remove = function (id, func) { sqlDbQuery('DELETE FROM main WHERE id = ?', [id], func); };
2518 obj.RemoveAll = function (func) { sqlDbQuery('DELETE FROM main', null, func); };
2519 obj.RemoveAllOfType = function (type, func) { sqlDbQuery('DELETE FROM main WHERE type = ?', [type], func); };
2520 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
2521 obj.RemoveMeshDocuments = function (id, func) { sqlDbQuery('DELETE FROM main WHERE extra = ?', [id], function () { sqlDbQuery('DELETE FROM main WHERE id = ?', ['nt' + id], func); } ); };
2522 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]); } }); };
2523 obj.DeleteDomain = function (domain, func) { sqlDbQuery('DELETE FROM main WHERE domain = ?', [domain], func); };
2524 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); } };
2525 obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
2526 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); }); };
2527 obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { sqlDbQuery('SELECT doc FROM main WHERE domain = ? AND extraex = ?', [domainid, 'uuid/' + uuid], func); };
2528 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)']) }); } }
2529
2530 // Database actions on the events collection
2531 obj.GetAllEvents = function (func) { sqlDbQuery('SELECT doc FROM events', null, func); };
2532 obj.StoreEvent = function (event, func) {
2533 obj.dbCounters.eventsSet++;
2534 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)]]];
2535 for (var i in event.ids) { if (event.ids[i] != '*') { batchQuery.push(['INSERT INTO eventids VALUE (LAST_INSERT_ID(), ?)', [event.ids[i]]]); } }
2536 sqlDbBatchExec(batchQuery, function (err, docs) { if (func != null) { func(err, docs); } });
2537 };
2538 obj.GetEvents = function (ids, domain, filter, func) {
2539 var query = "SELECT doc FROM events ";
2540 var dataarray = [domain];
2541 if (ids.indexOf('*') >= 0) {
2542 query = query + "WHERE (domain = ?";
2543 if (filter != null) {
2544 query = query + " AND action = ?";
2545 dataarray.push(filter);
2546 }
2547 query = query + ") ORDER BY time DESC";
2548 } else {
2549 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.
2550 query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND target IN (?)";
2551 dataarray.push(ids);
2552 if (filter != null) {
2553 query = query + " AND action = ?";
2554 dataarray.push(filter);
2555 }
2556 query = query + ") GROUP BY id ORDER BY time DESC";
2557 }
2558 sqlDbQuery(query, dataarray, func);
2559 };
2560 obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
2561 var query = "SELECT doc FROM events ";
2562 var dataarray = [domain];
2563 if (ids.indexOf('*') >= 0) {
2564 query = query + "WHERE (domain = ?";
2565 if (filter != null) {
2566 query = query + " AND action = ? ";
2567 dataarray.push(filter);
2568 }
2569 query = query + ") ORDER BY time DESC LIMIT ?";
2570 } else {
2571 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.
2572 query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND target IN (?)";
2573 dataarray.push(ids);
2574 if (filter != null) {
2575 query = query + " AND action = ?";
2576 dataarray.push(filter);
2577 }
2578 query = query + ") GROUP BY id ORDER BY time DESC LIMIT ?";
2579 }
2580 dataarray.push(limit);
2581 sqlDbQuery(query, dataarray, func);
2582 };
2583 obj.GetUserEvents = function (ids, domain, userid, filter, func) {
2584 var query = "SELECT doc FROM events ";
2585 var dataarray = [domain, userid];
2586 if (ids.indexOf('*') >= 0) {
2587 query = query + "WHERE (domain = ? AND userid = ?";
2588 if (filter != null) {
2589 query = query + " AND action = ?";
2590 dataarray.push(filter);
2591 }
2592 query = query + ") ORDER BY time DESC";
2593 } else {
2594 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.
2595 query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND userid = ? AND target IN (?)";
2596 dataarray.push(ids);
2597 if (filter != null) {
2598 query = query + " AND action = ?";
2599 dataarray.push(filter);
2600 }
2601 query = query + ") GROUP BY id ORDER BY time DESC";
2602 }
2603 sqlDbQuery(query, dataarray, func);
2604 };
2605 obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
2606 var query = "SELECT doc FROM events ";
2607 var dataarray = [domain, userid];
2608 if (ids.indexOf('*') >= 0) {
2609 query = query + "WHERE (domain = ? AND userid = ?";
2610 if (filter != null) {
2611 query = query + " AND action = ?";
2612 dataarray.push(filter);
2613 }
2614 query = query + ") ORDER BY time DESC LIMIT ?";
2615 } else {
2616 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.
2617 query = query + "JOIN eventids ON id = fkid WHERE (domain = ? AND userid = ? AND target IN (?)";
2618 dataarray.push(ids);
2619 if (filter != null) {
2620 query = query + " AND action = ?";
2621 dataarray.push(filter);
2622 }
2623 query = query + ") GROUP BY id ORDER BY time DESC LIMIT ?";
2624 }
2625 dataarray.push(limit);
2626 sqlDbQuery(query, dataarray, func);
2627 };
2628 obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
2629 if (ids.indexOf('*') >= 0) {
2630 sqlDbQuery('SELECT doc FROM events WHERE ((domain = ?) AND (time BETWEEN ? AND ?)) ORDER BY time', [domain, start, end], func);
2631 } else {
2632 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.
2633 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);
2634 }
2635 };
2636 //obj.GetUserLoginEvents = function (domain, userid, func) { } // TODO
2637 obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
2638 var query = "SELECT doc FROM events WHERE (nodeid = ? AND domain = ?";
2639 var dataarray = [nodeid, domain];
2640 if (filter != null) {
2641 query = query + " AND action = ?) ORDER BY time DESC LIMIT ?";
2642 dataarray.push(filter);
2643 } else {
2644 query = query + ") ORDER BY time DESC LIMIT ?";
2645 }
2646 dataarray.push(limit);
2647 sqlDbQuery(query, dataarray, func);
2648 };
2649 obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
2650 var query = "SELECT doc FROM events WHERE (nodeid = ? AND domain = ? AND ((userid = ?) OR (userid IS NULL))";
2651 var dataarray = [nodeid, domain, userid];
2652 if (filter != null) {
2653 query = query + " AND action = ?) ORDER BY time DESC LIMIT ?";
2654 dataarray.push(filter);
2655 } else {
2656 query = query + ") ORDER BY time DESC LIMIT ?";
2657 }
2658 dataarray.push(limit);
2659 sqlDbQuery(query, dataarray, func);
2660 };
2661 obj.RemoveAllEvents = function (domain) { sqlDbQuery('DELETE FROM events', null, function (err, docs) { }); };
2662 obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = ? AND nodeid = ?', [domain, nodeid], function (err, docs) { }); };
2663 obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; sqlDbQuery('DELETE FROM events WHERE domain = ? AND userid = ?', [domain, userid], function (err, docs) { }); };
2664 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); }); }
2665
2666 // Database actions on the power collection
2667 obj.getAllPower = function (func) { sqlDbQuery('SELECT doc FROM power', null, func); };
2668 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); };
2669 obj.getPowerTimeline = function (nodeid, func) { sqlDbQuery('SELECT doc FROM power WHERE ((nodeid = ?) OR (nodeid = "*")) ORDER BY time ASC', [nodeid], func); };
2670 obj.removeAllPowerEvents = function () { sqlDbQuery('DELETE FROM power', null, function (err, docs) { }); };
2671 obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; sqlDbQuery('DELETE FROM power WHERE nodeid = ?', [nodeid], function (err, docs) { }); };
2672
2673 // Database actions on the SMBIOS collection
2674 obj.GetAllSMBIOS = function (func) { sqlDbQuery('SELECT doc FROM smbios', null, func); };
2675 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); };
2676 obj.RemoveSMBIOS = function (id) { sqlDbQuery('DELETE FROM smbios WHERE id = ?', [id], function (err, docs) { }); };
2677 obj.GetSMBIOS = function (id, func) { sqlDbQuery('SELECT doc FROM smbios WHERE id = ?', [id], func); };
2678
2679 // Database actions on the Server Stats collection
2680 obj.SetServerStats = function (data, func) { sqlDbQuery('REPLACE INTO serverstats VALUE (?, ?, ?)', [data.time, data.expire, JSON.stringify(data)], func); };
2681 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); };
2682
2683 // Read a configuration file from the database
2684 obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
2685
2686 // Write a configuration file to the database
2687 obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
2688
2689 // List all configuration files
2690 obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM main WHERE type = "cfile" ORDER BY id', func); }
2691
2692 // Get database information (TODO: Complete this)
2693 obj.getDbStats = function (func) {
2694 obj.stats = { c: 4 };
2695 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); } });
2696 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); } });
2697 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); } });
2698 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); } });
2699 }
2700
2701 // Plugin operations
2702 if (obj.pluginsActive) {
2703 obj.addPlugin = function (plugin, func) { sqlDbQuery('INSERT INTO plugin VALUE (?, ?)', [null, JSON.stringify(plugin)], func); }; // Add a plugin
2704 obj.getPlugins = function (func) { sqlDbQuery('SELECT JSON_INSERT(doc, "$._id", id) as doc FROM plugin', null, func); }; // Get all plugins
2705 obj.getPlugin = function (id, func) { sqlDbQuery('SELECT JSON_INSERT(doc, "$._id", id) as doc FROM plugin WHERE id = ?', [id], func); }; // Get plugin
2706 obj.deletePlugin = function (id, func) { sqlDbQuery('DELETE FROM plugin WHERE id = ?', [id], func); }; // Delete plugin
2707 obj.setPluginStatus = function (id, status, func) { sqlDbQuery('UPDATE meshcentral.plugin SET doc=JSON_SET(doc,"$.status",?) WHERE id=?', [status,id], func); };
2708 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); };
2709 }
2710 } else if (obj.databaseType == DB_MONGODB) {
2711 // Database actions on the main collection (MongoDB)
2712
2713 // Bulk operations
2714 if (parent.config.settings.mongodbbulkoperations) {
2715 obj.Set = function (data, func) { // Fast Set operation using bulkWrite(), this is much faster then using replaceOne()
2716 if (obj.filePendingSet == false) {
2717 // Perform the operation now
2718 obj.dbCounters.fileSet++;
2719 obj.filePendingSet = true; obj.filePendingSets = null;
2720 if (func != null) { obj.filePendingCbs = [func]; }
2721 obj.file.bulkWrite([{ replaceOne: { filter: { _id: data._id }, replacement: performTypedRecordEncrypt(common.escapeLinksFieldNameEx(data)), upsert: true } }], fileBulkWriteCompleted);
2722 } else {
2723 // Add this operation to the pending list
2724 obj.dbCounters.fileSetPending++;
2725 if (obj.filePendingSets == null) { obj.filePendingSets = {} }
2726 obj.filePendingSets[data._id] = data;
2727 if (func != null) { if (obj.filePendingCb == null) { obj.filePendingCb = [func]; } else { obj.filePendingCb.push(func); } }
2728 }
2729 };
2730
2731 obj.Get = function (id, func) { // Fast Get operation using a bulk find() to reduce round trips to the database.
2732 // Encode arguments into return function if any are present.
2733 var func2 = func;
2734 if (arguments.length > 2) {
2735 var parms = [func];
2736 for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
2737 var func2 = function _func2(arg1, arg2) {
2738 var userCallback = _func2.userArgs.shift();
2739 _func2.userArgs.unshift(arg2);
2740 _func2.userArgs.unshift(arg1);
2741 userCallback.apply(obj, _func2.userArgs);
2742 };
2743 func2.userArgs = parms;
2744 }
2745
2746 if (obj.filePendingGets == null) {
2747 // No pending gets, perform the operation now.
2748 obj.filePendingGets = {};
2749 obj.filePendingGets[id] = [func2];
2750 obj.file.find({ _id: id }).toArray(fileBulkReadCompleted);
2751 } else {
2752 // Add get to pending list.
2753 if (obj.filePendingGet == null) { obj.filePendingGet = {}; }
2754 if (obj.filePendingGet[id] == null) { obj.filePendingGet[id] = [func2]; } else { obj.filePendingGet[id].push(func2); }
2755 }
2756 };
2757 } else {
2758 obj.Set = function (data, func) {
2759 obj.dbCounters.fileSet++;
2760 data = common.escapeLinksFieldNameEx(data);
2761 obj.file.replaceOne({ _id: data._id }, performTypedRecordEncrypt(data), { upsert: true }, func);
2762 };
2763 obj.Get = function (id, func) {
2764 if (arguments.length > 2) {
2765 var parms = [func];
2766 for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
2767 var func2 = function _func2(arg1, arg2) {
2768 var userCallback = _func2.userArgs.shift();
2769 _func2.userArgs.unshift(arg2);
2770 _func2.userArgs.unshift(arg1);
2771 userCallback.apply(obj, _func2.userArgs);
2772 };
2773 func2.userArgs = parms;
2774 obj.file.find({ _id: id }).toArray(function (err, docs) {
2775 if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
2776 func2(err, performTypedRecordDecrypt(docs));
2777 });
2778 } else {
2779 obj.file.find({ _id: id }).toArray(function (err, docs) {
2780 if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
2781 func(err, performTypedRecordDecrypt(docs));
2782 });
2783 }
2784 };
2785 }
2786 obj.GetAll = function (func) { obj.file.find({}).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
2787 obj.GetHash = function (id, func) { obj.file.find({ _id: id }).project({ _id: 0, hash: 1 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
2788 obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }).project({ type: 0 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
2789 obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
2790 if (extrasids == null) {
2791 const x = { type: type, domain: domain, meshid: { $in: meshes } };
2792 if (id) { x._id = id; }
2793 var f = obj.file.find(x, { type: 0 }).collation({ locale: 'en', strength: 2 }).sort({ name: 1 });
2794 if (skip > 0) f = f.skip(skip); // Skip records
2795 if (limit > 0) f = f.limit(limit); // Limit records
2796 f.toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
2797 } else {
2798 const x = { type: type, domain: domain, $or: [ { meshid: { $in: meshes } }, { _id: { $in: extrasids } } ] };
2799 if (id) { x._id = id; }
2800 var f = obj.file.find(x, { type: 0 }).collation({ locale: 'en', strength: 2 }).sort({ name: 1 });
2801 if (skip > 0) f = f.skip(skip); // Skip records
2802 if (limit > 0) f = f.limit(limit); // Limit records
2803 f.toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
2804 }
2805 };
2806 obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
2807 if (extrasids == null) {
2808 const x = { type: type, domain: domain, meshid: { $in: meshes } };
2809 if (id) { x._id = id; }
2810 var f = obj.file.find(x, { type: 0 });
2811 if (f.countDocuments){
2812 f.countDocuments(function (err, count) { func(err, count); });
2813 } else {
2814 f.count(function (err, count) { func(err, count); });
2815 }
2816 } else {
2817 const x = { type: type, domain: domain, $or: [{ meshid: { $in: meshes } }, { _id: { $in: extrasids } }] };
2818 if (id) { x._id = id; }
2819 var f = obj.file.find(x, { type: 0 });
2820 if (f.countDocuments){
2821 f.countDocuments(function (err, count) { func(err, count); });
2822 } else {
2823 f.count(function (err, count) { func(err, count); });
2824 }
2825 }
2826 };
2827 obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
2828 var x = { type: type, domain: domain, nodeid: { $in: nodes } };
2829 if (id) { x._id = id; }
2830 obj.file.find(x, { type: 0 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
2831 };
2832 obj.GetAllType = function (type, func) { obj.file.find({ type: type }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
2833 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)); }); };
2834 obj.GetUserWithEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
2835 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)); }); };
2836
2837 // Bulk operations
2838 if (parent.config.settings.mongodbbulkoperations) {
2839 obj.Remove = function (id, func) { // Fast remove operation using a bulk find() to reduce round trips to the database.
2840 if (obj.filePendingRemoves == null) {
2841 // No pending removes, perform the operation now.
2842 obj.dbCounters.fileRemove++;
2843 obj.filePendingRemoves = {};
2844 obj.filePendingRemoves[id] = [func];
2845 obj.file.deleteOne({ _id: id }, fileBulkRemoveCompleted);
2846 } else {
2847 // Add remove to pending list.
2848 obj.dbCounters.fileRemovePending++;
2849 if (obj.filePendingRemove == null) { obj.filePendingRemove = {}; }
2850 if (obj.filePendingRemove[id] == null) { obj.filePendingRemove[id] = [func]; } else { obj.filePendingRemove[id].push(func); }
2851 }
2852 };
2853 } else {
2854 obj.Remove = function (id, func) { obj.dbCounters.fileRemove++; obj.file.deleteOne({ _id: id }, func); };
2855 }
2856
2857 obj.RemoveAll = function (func) { obj.file.deleteMany({}, { multi: true }, func); };
2858 obj.RemoveAllOfType = function (type, func) { obj.file.deleteMany({ type: type }, { multi: true }, func); };
2859 obj.InsertMany = function (data, func) { obj.file.insertMany(data, func); }; // Insert records directly, no link escaping
2860 obj.RemoveMeshDocuments = function (id) { obj.file.deleteMany({ meshid: id }, { multi: true }); obj.file.deleteOne({ _id: 'nt' + id }); };
2861 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]); } }); };
2862 obj.DeleteDomain = function (domain, func) { obj.file.deleteMany({ domain: domain }, { multi: true }, func); };
2863 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); } };
2864 obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
2865 obj.getLocalAmtNodes = function (func) { obj.file.find({ type: 'node', host: { $exists: true, $ne: null }, intelamt: { $exists: true } }).toArray(func); };
2866 obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { obj.file.find({ type: 'node', domain: domainid, mtype: mtype, 'intelamt.uuid': uuid }).toArray(func); };
2867
2868 // TODO: Starting in MongoDB 4.0.3, you should use countDocuments() instead of count() that is deprecated. We should detect MongoDB version and switch.
2869 // https://docs.mongodb.com/manual/reference/method/db.collection.countDocuments/
2870 //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)); }); } }
2871 obj.isMaxType = function (max, type, domainid, func) {
2872 if (obj.file.countDocuments) {
2873 if (max == null) { func(false); } else { obj.file.countDocuments({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); }
2874 } else {
2875 if (max == null) { func(false); } else { obj.file.count({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); }
2876 }
2877 }
2878
2879 // Database actions on the events collection
2880 obj.GetAllEvents = function (func) { obj.eventsfile.find({}).toArray(func); };
2881
2882 // Bulk operations
2883 if (parent.config.settings.mongodbbulkoperations) {
2884 obj.StoreEvent = function (event, func) { // Fast MongoDB event store using bulkWrite()
2885 if (obj.eventsFilePendingSet == false) {
2886 // Perform the operation now
2887 obj.dbCounters.eventsSet++;
2888 obj.eventsFilePendingSet = true; obj.eventsFilePendingSets = null;
2889 if (func != null) { obj.eventsFilePendingCbs = [func]; }
2890 obj.eventsfile.bulkWrite([{ insertOne: { document: event } }], eventsFileBulkWriteCompleted);
2891 } else {
2892 // Add this operation to the pending list
2893 obj.dbCounters.eventsSetPending++;
2894 if (obj.eventsFilePendingSets == null) { obj.eventsFilePendingSets = [] }
2895 obj.eventsFilePendingSets.push(event);
2896 if (func != null) { if (obj.eventsFilePendingCb == null) { obj.eventsFilePendingCb = [func]; } else { obj.eventsFilePendingCb.push(func); } }
2897 }
2898 };
2899 } else {
2900 obj.StoreEvent = function (event, func) { obj.dbCounters.eventsSet++; obj.eventsfile.insertOne(event, func); };
2901 }
2902
2903 obj.GetEvents = function (ids, domain, filter, func) {
2904 var finddata = { domain: domain, ids: { $in: ids } };
2905 if (filter != null) finddata.action = filter;
2906 obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).toArray(func);
2907 };
2908 obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
2909 var finddata = { domain: domain, ids: { $in: ids } };
2910 if (filter != null) finddata.action = filter;
2911 obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).toArray(func);
2912 };
2913 obj.GetUserEvents = function (ids, domain, userid, filter, func) {
2914 var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
2915 if (filter != null) finddata.action = filter;
2916 obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).toArray(func);
2917 };
2918 obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
2919 var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
2920 if (filter != null) finddata.action = filter;
2921 obj.eventsfile.find(finddata).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).toArray(func);
2922 };
2923 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); };
2924 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); };
2925 obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
2926 var finddata = { domain: domain, nodeid: nodeid };
2927 if (filter != null) finddata.action = filter;
2928 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);
2929 };
2930 obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
2931 var finddata = { domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } };
2932 if (filter != null) finddata.action = filter;
2933 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);
2934 };
2935 obj.RemoveAllEvents = function (domain) { obj.eventsfile.deleteMany({ domain: domain }, { multi: true }); };
2936 obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; obj.eventsfile.deleteMany({ domain: domain, nodeid: nodeid }, { multi: true }); };
2937 obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; obj.eventsfile.deleteMany({ domain: domain, userid: userid }, { multi: true }); };
2938 obj.GetFailedLoginCount = function (userid, domainid, lastlogin, func) {
2939 if (obj.eventsfile.countDocuments) {
2940 obj.eventsfile.countDocuments({ action: 'authfail', userid: userid, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null) ? count : 0); });
2941 } else {
2942 obj.eventsfile.count({ action: 'authfail', userid: userid, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null) ? count : 0); });
2943 }
2944 }
2945
2946 // Database actions on the power collection
2947 obj.getAllPower = function (func) { obj.powerfile.find({}).toArray(func); };
2948
2949 // Bulk operations
2950 if (parent.config.settings.mongodbbulkoperations) {
2951 obj.storePowerEvent = function (event, multiServer, func) { // Fast MongoDB event store using bulkWrite()
2952 if (multiServer != null) { event.server = multiServer.serverid; }
2953 if (obj.powerFilePendingSet == false) {
2954 // Perform the operation now
2955 obj.dbCounters.powerSet++;
2956 obj.powerFilePendingSet = true; obj.powerFilePendingSets = null;
2957 if (func != null) { obj.powerFilePendingCbs = [func]; }
2958 obj.powerfile.bulkWrite([{ insertOne: { document: event } }], powerFileBulkWriteCompleted);
2959 } else {
2960 // Add this operation to the pending list
2961 obj.dbCounters.powerSetPending++;
2962 if (obj.powerFilePendingSets == null) { obj.powerFilePendingSets = [] }
2963 obj.powerFilePendingSets.push(event);
2964 if (func != null) { if (obj.powerFilePendingCb == null) { obj.powerFilePendingCb = [func]; } else { obj.powerFilePendingCb.push(func); } }
2965 }
2966 };
2967 } else {
2968 obj.storePowerEvent = function (event, multiServer, func) { obj.dbCounters.powerSet++; if (multiServer != null) { event.server = multiServer.serverid; } obj.powerfile.insertOne(event, func); };
2969 }
2970
2971 obj.getPowerTimeline = function (nodeid, func) { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }).project({ _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }).toArray(func); };
2972 obj.removeAllPowerEvents = function () { obj.powerfile.deleteMany({}, { multi: true }); };
2973 obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; obj.powerfile.deleteMany({ nodeid: nodeid }, { multi: true }); };
2974
2975 // Database actions on the SMBIOS collection
2976 obj.GetAllSMBIOS = function (func) { obj.smbiosfile.find({}).toArray(func); };
2977 obj.SetSMBIOS = function (smbios, func) { obj.smbiosfile.updateOne({ _id: smbios._id }, { $set: smbios }, { upsert: true }, func); };
2978 obj.RemoveSMBIOS = function (id) { obj.smbiosfile.deleteOne({ _id: id }); };
2979 obj.GetSMBIOS = function (id, func) { obj.smbiosfile.find({ _id: id }).toArray(func); };
2980
2981 // Database actions on the Server Stats collection
2982 obj.SetServerStats = function (data, func) { obj.serverstatsfile.insertOne(data, func); };
2983 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); };
2984
2985 // Read a configuration file from the database
2986 obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
2987
2988 // Write a configuration file to the database
2989 obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
2990
2991 // List all configuration files
2992 obj.listConfigFiles = function (func) { obj.file.find({ type: 'cfile' }).sort({ _id: 1 }).toArray(func); }
2993
2994 // Get database information
2995 obj.getDbStats = function (func) {
2996 obj.stats = { c: 6 };
2997 obj.getStats(function (r) { obj.stats.recordTypes = r; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } })
2998 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)); } });
2999 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)); } });
3000 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)); } });
3001 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)); } });
3002 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)); } });
3003 }
3004
3005 // Correct database information of obj.getDbStats before returning it
3006 function getDbStatsEx(data) {
3007 var r = {};
3008 if (data.recordTypes != null) { r = data.recordTypes; }
3009 try { r.smbios = data['meshcentral.smbios'].count; } catch (ex) { }
3010 try { r.power = data['meshcentral.power'].count; } catch (ex) { }
3011 try { r.events = data['meshcentral.events'].count; } catch (ex) { }
3012 try { r.serverstats = data['meshcentral.serverstats'].count; } catch (ex) { }
3013 return r;
3014 }
3015
3016 // Plugin operations
3017 if (obj.pluginsActive) {
3018 obj.addPlugin = function (plugin, func) { plugin.type = 'plugin'; obj.pluginsfile.insertOne(plugin, func); }; // Add a plugin
3019 obj.getPlugins = function (func) { obj.pluginsfile.find({ type: 'plugin' }).project({ type: 0 }).sort({ name: 1 }).toArray(func); }; // Get all plugins
3020 obj.getPlugin = function (id, func) { id = require('mongodb').ObjectId(id); obj.pluginsfile.find({ _id: id }).sort({ name: 1 }).toArray(func); }; // Get plugin
3021 obj.deletePlugin = function (id, func) { id = require('mongodb').ObjectId(id); obj.pluginsfile.deleteOne({ _id: id }, func); }; // Delete plugin
3022 obj.setPluginStatus = function (id, status, func) { id = require('mongodb').ObjectId(id); obj.pluginsfile.updateOne({ _id: id }, { $set: { status: status } }, func); };
3023 obj.updatePlugin = function (id, args, func) { delete args._id; id = require('mongodb').ObjectId(id); obj.pluginsfile.updateOne({ _id: id }, { $set: args }, func); };
3024 obj.getPluginPermissions = function (pluginName, func) { obj.pluginpermissionsfile.findOne({ _id: 'pluginpermission//' + pluginName }, function(err, doc) { func(err, doc ? [doc] : []); }); };
3025 obj.setPluginPermissions = function (pluginName, data, func) { delete data._id; obj.pluginpermissionsfile.updateOne({ _id: 'pluginpermission//' + pluginName }, { $set: data }, { upsert: true }, func); };
3026 }
3027
3028 } else {
3029 // Database actions on the main collection (NeDB and MongoJS)
3030 obj.Set = function (data, func) {
3031 obj.dbCounters.fileSet++;
3032 data = common.escapeLinksFieldNameEx(data);
3033 var xdata = performTypedRecordEncrypt(data); obj.file.update({ _id: xdata._id }, xdata, { upsert: true }, func);
3034 };
3035 obj.Get = function (id, func) {
3036 if (arguments.length > 2) {
3037 var parms = [func];
3038 for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
3039 var func2 = function _func2(arg1, arg2) {
3040 var userCallback = _func2.userArgs.shift();
3041 _func2.userArgs.unshift(arg2);
3042 _func2.userArgs.unshift(arg1);
3043 userCallback.apply(obj, _func2.userArgs);
3044 };
3045 func2.userArgs = parms;
3046 obj.file.find({ _id: id }, function (err, docs) {
3047 if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
3048 func2(err, performTypedRecordDecrypt(docs));
3049 });
3050 } else {
3051 obj.file.find({ _id: id }, function (err, docs) {
3052 if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
3053 func(err, performTypedRecordDecrypt(docs));
3054 });
3055 }
3056 };
3057 obj.GetAll = function (func) { obj.file.find({}, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
3058 obj.GetHash = function (id, func) { obj.file.find({ _id: id }, { _id: 0, hash: 1 }, func); };
3059 obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
3060 //obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, domain, type, id, skip, limit, func) {
3061 //var x = { type: type, domain: domain, meshid: { $in: meshes } };
3062 //if (id) { x._id = id; }
3063 //obj.file.find(x, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
3064 //};
3065 obj.CountAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
3066 if (extrasids == null) {
3067 const x = { type: type, domain: domain, meshid: { $in: meshes } };
3068 if (id) { x._id = id; }
3069 obj.file.count(x, function (err, count) { func(err, count); });
3070 } else {
3071 const x = { type: type, domain: domain, $or: [{ meshid: { $in: meshes } }, { _id: { $in: extrasids } }] };
3072 if (id) { x._id = id; }
3073 obj.file.count(x, function (err, count) { func(err, count); });
3074 }
3075 };
3076 obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, skip, limit, func) {
3077 if (extrasids == null) {
3078 const x = { type: type, domain: domain, meshid: { $in: meshes } };
3079 if (id) { x._id = id; }
3080 obj.file.find(x).sort({ name: 1 }).skip(skip).limit(limit).exec(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
3081 } else {
3082 const x = { type: type, domain: domain, $or: [{ meshid: { $in: meshes } }, { _id: { $in: extrasids } }] };
3083 if (id) { x._id = id; }
3084 obj.file.find(x).sort({ name: 1 }).skip(skip).limit(limit).exec(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
3085 }
3086 };
3087 obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
3088 var x = { type: type, domain: domain, nodeid: { $in: nodes } };
3089 if (id) { x._id = id; }
3090 obj.file.find(x, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
3091 };
3092 obj.GetAllType = function (type, func) { obj.file.find({ type: type }, function (err, docs) { func(err, common.unEscapeAllLinksFieldName(performTypedRecordDecrypt(docs))); }); };
3093 obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.find({ type: type, domain: domain, _id: { $in: ids } }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
3094 obj.GetUserWithEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email }, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
3095 obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email, emailVerified: true }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
3096 obj.Remove = function (id, func) { obj.file.remove({ _id: id }, func); };
3097 obj.RemoveAll = function (func) { obj.file.remove({}, { multi: true }, func); };
3098 obj.RemoveAllOfType = function (type, func) { obj.file.remove({ type: type }, { multi: true }, func); };
3099 obj.InsertMany = function (data, func) { obj.file.insert(data, func); }; // Insert records directly, no link escaping
3100 obj.RemoveMeshDocuments = function (id) { obj.file.remove({ meshid: id }, { multi: true }); obj.file.remove({ _id: 'nt' + id }); };
3101 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]); } }); };
3102 obj.DeleteDomain = function (domain, func) { obj.file.remove({ domain: domain }, { multi: true }, func); };
3103 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); } };
3104 obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
3105 obj.getLocalAmtNodes = function (func) { obj.file.find({ type: 'node', host: { $exists: true, $ne: null }, intelamt: { $exists: true } }, func); };
3106 obj.getAmtUuidMeshNode = function (domainid, mtype, uuid, func) { obj.file.find({ type: 'node', domain: domainid, mtype: mtype, 'intelamt.uuid': uuid }, func); };
3107 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); }); } }
3108
3109 // Database actions on the events collection
3110 obj.GetAllEvents = function (func) { obj.eventsfile.find({}, func); };
3111 obj.StoreEvent = function (event, func) { obj.eventsfile.insert(event, func); };
3112 obj.GetEvents = function (ids, domain, filter, func) {
3113 var finddata = { domain: domain, ids: { $in: ids } };
3114 if (filter != null) finddata.action = filter;
3115 if (obj.databaseType == DB_NEDB) {
3116 obj.eventsfile.find(finddata, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func);
3117 } else {
3118 obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func);
3119 }
3120 };
3121 obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
3122 var finddata = { domain: domain, ids: { $in: ids } };
3123 if (filter != null) finddata.action = filter;
3124 if (obj.databaseType == DB_NEDB) {
3125 obj.eventsfile.find(finddata, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func);
3126 } else {
3127 obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func);
3128 }
3129 };
3130 obj.GetUserEvents = function (ids, domain, userid, filter, func) {
3131 var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
3132 if (filter != null) finddata.action = filter;
3133 if (obj.databaseType == DB_NEDB) {
3134 obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func);
3135 } else {
3136 obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func);
3137 }
3138 };
3139 obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
3140 var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
3141 if (filter != null) finddata.action = filter;
3142 if (obj.databaseType == DB_NEDB) {
3143 obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func);
3144 } else {
3145 obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func);
3146 }
3147 };
3148 obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
3149 if (obj.databaseType == DB_NEDB) {
3150 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);
3151 } else {
3152 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);
3153 }
3154 };
3155 obj.GetUserLoginEvents = function (domain, userid, func) {
3156 if (obj.databaseType == DB_NEDB) {
3157 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);
3158 } else {
3159 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);
3160 }
3161 };
3162 obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
3163 var finddata = { domain: domain, nodeid: nodeid };
3164 if (filter != null) finddata.action = filter;
3165 if (obj.databaseType == DB_NEDB) {
3166 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);
3167 } else {
3168 obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit, func);
3169 }
3170 };
3171 obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
3172 var finddata = { domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } };
3173 if (filter != null) finddata.action = filter;
3174 if (obj.databaseType == DB_NEDB) {
3175 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);
3176 } else {
3177 obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit, func);
3178 }
3179 };
3180 obj.RemoveAllEvents = function (domain) { obj.eventsfile.remove({ domain: domain }, { multi: true }); };
3181 obj.RemoveAllNodeEvents = function (domain, nodeid) { if ((domain == null) || (nodeid == null)) return; obj.eventsfile.remove({ domain: domain, nodeid: nodeid }, { multi: true }); };
3182 obj.RemoveAllUserEvents = function (domain, userid) { if ((domain == null) || (userid == null)) return; obj.eventsfile.remove({ domain: domain, userid: userid }, { multi: true }); };
3183 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); }); }
3184
3185 // Database actions on the power collection
3186 obj.getAllPower = function (func) { obj.powerfile.find({}, func); };
3187 obj.storePowerEvent = function (event, multiServer, func) { if (multiServer != null) { event.server = multiServer.serverid; } obj.powerfile.insert(event, func); };
3188 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); } };
3189 obj.removeAllPowerEvents = function () { obj.powerfile.remove({}, { multi: true }); };
3190 obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; obj.powerfile.remove({ nodeid: nodeid }, { multi: true }); };
3191
3192 // Database actions on the SMBIOS collection
3193 if (obj.smbiosfile != null) {
3194 obj.GetAllSMBIOS = function (func) { obj.smbiosfile.find({}, func); };
3195 obj.SetSMBIOS = function (smbios, func) { obj.smbiosfile.update({ _id: smbios._id }, smbios, { upsert: true }, func); };
3196 obj.RemoveSMBIOS = function (id) { obj.smbiosfile.remove({ _id: id }); };
3197 obj.GetSMBIOS = function (id, func) { obj.smbiosfile.find({ _id: id }, func); };
3198 }
3199
3200 // Database actions on the Server Stats collection
3201 obj.SetServerStats = function (data, func) { obj.serverstatsfile.insert(data, func); };
3202 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); };
3203
3204 // Read a configuration file from the database
3205 obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
3206
3207 // Write a configuration file to the database
3208 obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
3209
3210 // List all configuration files
3211 obj.listConfigFiles = function (func) { obj.file.find({ type: 'cfile' }).sort({ _id: 1 }).exec(func); }
3212
3213 // Get database information
3214 obj.getDbStats = function (func) {
3215 obj.stats = { c: 5 };
3216 obj.getStats(function (r) { obj.stats.recordTypes = r; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } })
3217 obj.file.count({}, function (err, count) { obj.stats.meshcentral = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
3218 obj.eventsfile.count({}, function (err, count) { obj.stats.events = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
3219 obj.powerfile.count({}, function (err, count) { obj.stats.power = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
3220 obj.serverstatsfile.count({}, function (err, count) { obj.stats.serverstats = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(getDbStatsEx(obj.stats)); } });
3221 }
3222
3223 // Correct database information of obj.getDbStats before returning it
3224 function getDbStatsEx(data) {
3225 var r = {};
3226 if (data.recordTypes != null) { r = data.recordTypes; }
3227 try { r.smbios = data['smbios'].count; } catch (ex) { }
3228 try { r.power = data['power'].count; } catch (ex) { }
3229 try { r.events = data['events'].count; } catch (ex) { }
3230 try { r.serverstats = data['serverstats'].count; } catch (ex) { }
3231 return r;
3232 }
3233
3234 // Plugin operations
3235 if (obj.pluginsActive) {
3236 obj.addPlugin = function (plugin, func) { plugin.type = 'plugin'; obj.pluginsfile.insert(plugin, func); }; // Add a plugin
3237 obj.getPlugins = function (func) { obj.pluginsfile.find({ 'type': 'plugin' }, { 'type': 0 }).sort({ name: 1 }).exec(func); }; // Get all plugins
3238 obj.getPlugin = function (id, func) { obj.pluginsfile.find({ _id: id }).sort({ name: 1 }).exec(func); }; // Get plugin
3239 obj.deletePlugin = function (id, func) { obj.pluginsfile.remove({ _id: id }, func); }; // Delete plugin
3240 obj.setPluginStatus = function (id, status, func) { obj.pluginsfile.update({ _id: id }, { $set: { status: status } }, func); };
3241 obj.updatePlugin = function (id, args, func) { delete args._id; obj.pluginsfile.update({ _id: id }, { $set: args }, func); };
3242 obj.getPluginPermissions = function (pluginName, func) { obj.pluginsfile.findOne({ _id: 'pluginpermission//' + pluginName }, function(err, doc) { func(err, doc ? [doc] : []); }); };
3243 obj.setPluginPermissions = function (pluginName, data, func) { delete data._id; obj.pluginsfile.update({ _id: 'pluginpermission//' + pluginName }, { $set: data }, { upsert: true }, func); };
3244 }
3245 }
3246
3247 // Get all configuration files
3248 obj.getAllConfigFiles = function (password, func) {
3249 obj.GetAllType('cfile', function (err, docs) {
3250 if (err != null) { func(null); return; }
3251 var r = null;
3252 for (var i = 0; i < docs.length; i++) {
3253 var name = docs[i]._id.split('/')[1];
3254 var data = obj.decryptData(password, docs[i].data);
3255 if (data != null) { if (r == null) { r = {}; } r[name] = data; }
3256 }
3257 func(r);
3258 });
3259 }
3260
3261 func(obj); // Completed function setup
3262 }
3263
3264 // Return a human readable string with current backup configuration
3265 obj.getBackupConfig = function () {
3266 var r = '', backupPath = parent.backuppath;
3267
3268 let dbname = 'meshcentral';
3269 if (parent.args.mongodbname) { dbname = parent.args.mongodbname; }
3270 else if ((typeof parent.args.mariadb == 'object') && (typeof parent.args.mariadb.database == 'string')) { dbname = parent.args.mariadb.database; }
3271 else if ((typeof parent.args.mysql == 'object') && (typeof parent.args.mysql.database == 'string')) { dbname = parent.args.mysql.database; }
3272 else if ((typeof parent.args.postgres == 'object') && (typeof parent.args.postgres.database == 'string')) { dbname = parent.args.postgres.database; }
3273 else if (typeof parent.config.settings.sqlite3 == 'string') {dbname = parent.config.settings.sqlite3 + '.sqlite'};
3274
3275 const currentDate = new Date();
3276 const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2);
3277 obj.newAutoBackupFile = parent.config.settings.autobackup.backupname + fileSuffix;
3278
3279 r += 'DB Name: ' + dbname + '\r\n';
3280 r += 'DB Type: ' + DB_LIST[obj.databaseType] + '\r\n';
3281
3282 if (parent.config.settings.autobackup.backupintervalhours == -1) {
3283 r += 'Backup disabled\r\n';
3284 } else {
3285 r += 'BackupPath: ' + backupPath + '\r\n';
3286 r += 'BackupFile: ' + obj.newAutoBackupFile + '.zip\r\n';
3287
3288 if (parent.config.settings.autobackup.backuphour != null && parent.config.settings.autobackup.backuphour != -1) {
3289 r += 'Backup between: ' + parent.config.settings.autobackup.backuphour + 'H-' + (parent.config.settings.autobackup.backuphour + 1) + 'H\r\n';
3290 }
3291 r += 'Backup Interval (Hours): ' + parent.config.settings.autobackup.backupintervalhours + '\r\n';
3292 if (parent.config.settings.autobackup.keeplastdaysbackup != null) {
3293 r += 'Keep Last Backups (Days): ' + parent.config.settings.autobackup.keeplastdaysbackup + '\r\n';
3294 }
3295 if (parent.config.settings.autobackup.zippassword != null) {
3296 r += 'ZIP Password: ';
3297 if (typeof parent.config.settings.autobackup.zippassword != 'string') { r += 'Bad zippassword type, Backups will not be encrypted\r\n'; }
3298 else if (parent.config.settings.autobackup.zippassword == "") { r += 'Blank zippassword, Backups will fail\r\n'; }
3299 else { r += 'Set\r\n'; }
3300 }
3301 if (parent.config.settings.autobackup.mongodumppath != null) {
3302 r += 'MongoDump Path: ';
3303 if (typeof parent.config.settings.autobackup.mongodumppath != 'string') { r += 'Bad mongodumppath type\r\n'; }
3304 else { r += parent.config.settings.autobackup.mongodumppath + '\r\n'; }
3305 }
3306 if (parent.config.settings.autobackup.mysqldumppath != null) {
3307 r += 'MySqlDump Path: ';
3308 if (typeof parent.config.settings.autobackup.mysqldumppath != 'string') { r += 'Bad mysqldump type\r\n'; }
3309 else { r += parent.config.settings.autobackup.mysqldumppath + '\r\n'; }
3310 }
3311 if (parent.config.settings.autobackup.pgdumppath != null) {
3312 r += 'pgDump Path: ';
3313 if (typeof parent.config.settings.autobackup.pgdumppath != 'string') { r += 'Bad pgdump type\r\n'; }
3314 else { r += parent.config.settings.autobackup.pgdumppath + '\r\n'; }
3315 }
3316 if (parent.config.settings.autobackup.backupotherfolders) {
3317 r += 'Backup other folders: ';
3318 r += parent.filespath + ', ' + parent.recordpath + '\r\n';
3319 }
3320 if (parent.config.settings.autobackup.backupwebfolders) {
3321 r += 'Backup webfolders: ';
3322 if (parent.webViewsOverridePath) {r += parent.webViewsOverridePath };
3323 if (parent.webPublicOverridePath) {r += ', '+ parent.webPublicOverridePath};
3324 if (parent.webEmailsOverridePath) {r += ',' + parent.webEmailsOverridePath};
3325 r+= '\r\n';
3326 }
3327 if (parent.config.settings.autobackup.backupignorefilesglob != []) {
3328 r += 'Backup IgnoreFilesGlob: ';
3329 { r += parent.config.settings.autobackup.backupignorefilesglob + '\r\n'; }
3330 }
3331 if (parent.config.settings.autobackup.backupskipfoldersglob != []) {
3332 r += 'Backup SkipFoldersGlob: ';
3333 { r += parent.config.settings.autobackup.backupskipfoldersglob + '\r\n'; }
3334 }
3335
3336 if (typeof parent.config.settings.autobackup.s3 == 'object') {
3337 r += 'S3 Backups: Enabled\r\n';
3338 }
3339 if (typeof parent.config.settings.autobackup.webdav == 'object') {
3340 r += 'WebDAV Backups: Enabled\r\n';
3341 r += 'WebDAV backup path: ' + ((typeof parent.config.settings.autobackup.webdav.foldername == 'string') ? parent.config.settings.autobackup.webdav.foldername : 'MeshCentral-Backups') + '\r\n';
3342 r += 'WebDAV maximum files: '+ ((typeof parent.config.settings.autobackup.webdav.maxfiles == 'number') ? parent.config.settings.autobackup.webdav.maxfiles : 'no limit') + '\r\n';
3343 }
3344 if (typeof parent.config.settings.autobackup.googledrive == 'object') {
3345 r += 'Google Drive Backups: Enabled\r\n';
3346 }
3347
3348
3349 }
3350
3351 return r;
3352 }
3353
3354 function buildSqlDumpCommand() {
3355 var props = (obj.databaseType == DB_MARIADB) ? parent.args.mariadb : parent.args.mysql;
3356
3357 var mysqldumpPath = 'mysqldump';
3358 if (parent.config.settings.autobackup && parent.config.settings.autobackup.mysqldumppath) {
3359 mysqldumpPath = path.normalize(parent.config.settings.autobackup.mysqldumppath);
3360 }
3361
3362 var cmd = '\"' + mysqldumpPath + '\" --user=\'' + props.user + '\'';
3363 // Windows will treat ' as part of the pw. Linux/Unix requires it to escape.
3364 cmd += (parent.platform == 'win32') ? ' --password=\"' + props.password + '\"' : ' --password=\'' + props.password + '\'';
3365 if (props.host) { cmd += ' -h ' + props.host; }
3366 if (props.port) { cmd += ' -P ' + props.port; }
3367
3368 if (props.awsrds) { cmd += ' --single-transaction'; }
3369
3370 // SSL options different on mariadb/mysql
3371 var sslOptions = '';
3372 if (obj.databaseType == DB_MARIADB) {
3373 if (props.ssl) {
3374 sslOptions = ' --ssl';
3375 if (props.ssl.cacertpath) sslOptions = ' --ssl-ca=' + props.ssl.cacertpath;
3376 if (props.ssl.dontcheckserveridentity != true) {sslOptions += ' --ssl-verify-server-cert'} else {sslOptions += ' --ssl-verify-server-cert=false'};
3377 if (props.ssl.clientcertpath) sslOptions += ' --ssl-cert=' + props.ssl.clientcertpath;
3378 if (props.ssl.clientkeypath) sslOptions += ' --ssl-key=' + props.ssl.clientkeypath;
3379 }
3380 } else {
3381 if (props.ssl) {
3382 sslOptions = ' --ssl-mode=required';
3383 if (props.ssl.cacertpath) sslOptions = ' --ssl-ca=' + props.ssl.cacertpath;
3384 if (props.ssl.dontcheckserveridentity != true) sslOptions += ' --ssl-mode=verify_identity';
3385 else sslOptions += ' --ssl-mode=required';
3386 if (props.ssl.clientcertpath) sslOptions += ' --ssl-cert=' + props.ssl.clientcertpath;
3387 if (props.ssl.clientkeypath) sslOptions += ' --ssl-key=' + props.ssl.clientkeypath;
3388 }
3389 }
3390 cmd += sslOptions;
3391
3392 var dbname = (props.database) ? props.database : 'meshcentral';
3393 cmd += ' ' + dbname
3394
3395 return cmd;
3396 }
3397
3398 function buildMongoDumpCommand() {
3399 const dburl = parent.args.mongodb;
3400
3401 var mongoDumpPath = 'mongodump';
3402 if (parent.config.settings.autobackup && parent.config.settings.autobackup.mongodumppath) {
3403 mongoDumpPath = path.normalize(parent.config.settings.autobackup.mongodumppath);
3404 }
3405
3406 var cmd = '"' + mongoDumpPath + '"';
3407 if (dburl) { cmd = '\"' + mongoDumpPath + '\" --uri=\"' + dburl + '\"'; }
3408 if (parent.config.settings.autobackup?.mongodumpargs) {
3409 cmd = '\"' + mongoDumpPath + '\" ' + parent.config.settings.autobackup.mongodumpargs;
3410 if (!parent.config.settings.autobackup.mongodumpargs.includes("--db=")) {cmd += ' --db=' + (parent.config.settings.mongodbname ? parent.config.settings.mongodbname : 'meshcentral')};
3411 }
3412 return cmd;
3413 }
3414
3415 // Check that the server is capable of performing a backup
3416 // Tries configured custom location with fallback to default location
3417 // Now runs after autobackup config init in meshcentral.js so config options are checked
3418 obj.checkBackupCapability = function (func) {
3419 if (parent.config.settings.autobackup.backupintervalhours == -1) { return; };
3420 //block backup until validated. Gets put back if all checks are ok.
3421 let backupInterval = parent.config.settings.autobackup.backupintervalhours;
3422 parent.config.settings.autobackup.backupintervalhours = -1;
3423 let backupPath = parent.backuppath;
3424
3425 if (backupPath.startsWith(parent.datapath)) {
3426 func(1, "Backup path can't be set within meshcentral-data folder. No backups will be made.");
3427 return;
3428 }
3429 // Check create/write backupdir
3430 try { fs.mkdirSync(backupPath); }
3431 catch (e) {
3432 // EEXIST error = dir already exists
3433 if (e.code != 'EEXIST' ) {
3434 //Unable to create backuppath
3435 console.error(e.message);
3436 func(1, 'Unable to create ' + backupPath + '. No backups will be made. Error: ' + e.message);
3437 return;
3438 }
3439 }
3440 const currentDate = new Date();
3441 const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2);
3442 const testFile = path.join(backupPath, parent.config.settings.autobackup.backupname + fileSuffix + '.zip');
3443 try { fs.writeFileSync( testFile, "DeleteMe"); }
3444 catch (e) {
3445 //Unable to create file
3446 console.error (e.message);
3447 func(1, "Backuppath (" + backupPath + ") can't be written to. No backups will be made. Error: " + e.message);
3448 return;
3449 }
3450 try { fs.unlinkSync(testFile); parent.debug('backup', 'Backuppath ' + backupPath + ' accesscheck successful');}
3451 catch (e) {
3452 console.error (e.message);
3453 func(1, "Backuppathtestfile (" + testFile + ") can't be deleted, check filerights. Error: " + e.message);
3454 // Assume write rights, no delete rights. Continue with warning.
3455 //return;
3456 }
3457
3458 // Check database dumptools
3459 if ((obj.databaseType == DB_MONGOJS) || (obj.databaseType == DB_MONGODB)) {
3460 // Check that we have access to MongoDump
3461 var cmd = buildMongoDumpCommand();
3462 cmd += (parent.platform == 'win32') ? ' --archive=\"nul\"' : ' --archive=\"/dev/null\"';
3463 const child_process = require('child_process');
3464 child_process.exec(cmd, { cwd: backupPath }, function (error, stdout, stderr) {
3465 if ((error != null) && (error != '')) {
3466 func(1, "Mongodump error, backup will not be performed. Check path or use mongodumppath & mongodumpargs");
3467
3468 let processedError = error;
3469 if (typeof parent?.config?.settings?.postgres?.password === "string" && parent.config.settings.postgres.password.length > 0) {
3470 processedError = encodeURIComponent(processedError.replaceAll(parent.config.settings.postgres.password, "****"));
3471 }
3472 parent.debug('backup', 'MongoDB/MongoJS DumpTool: ' + processedError);
3473
3474 return;
3475 } else {parent.config.settings.autobackup.backupintervalhours = backupInterval;}
3476 });
3477 } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
3478 // Check that we have access to mysqldump
3479 var cmd = buildSqlDumpCommand();
3480 cmd += ' > ' + ((parent.platform == 'win32') ? '\"nul\"' : '\"/dev/null\"');
3481 const child_process = require('child_process');
3482 child_process.exec(cmd, { cwd: backupPath, timeout: 1000*30 }, function(error, stdout, stdin) {
3483 if ((error != null) && (error != '')) {
3484 func(1, "mysqldump error, backup will not be performed. Check path or use mysqldumppath");
3485
3486 let processedError = error;
3487 if (typeof parent?.config?.settings?.postgres?.password === "string" && parent.config.settings.postgres.password.length > 0) {
3488 processedError = encodeURIComponent(processedError.replaceAll(parent.config.settings.postgres.password, "****"));
3489 }
3490 parent.debug('backup', 'MariaDB/MySQL DumpTool: ' + processedError);
3491
3492 return;
3493 } else {parent.config.settings.autobackup.backupintervalhours = backupInterval;}
3494
3495 });
3496 } else if (obj.databaseType == DB_POSTGRESQL) {
3497 // Check that we have access to pg_dump
3498 parent.config.settings.autobackup.pgdumppath = path.normalize(parent.config.settings.autobackup.pgdumppath ? parent.config.settings.autobackup.pgdumppath : 'pg_dump');
3499 let cmd = '"' + parent.config.settings.autobackup.pgdumppath + '"'
3500 + ' --dbname=postgresql://' + encodeURIComponent(parent.config.settings.postgres.user) + ":" + encodeURIComponent(parent.config.settings.postgres.password)
3501 + "@" + parent.config.settings.postgres.host + ":" + parent.config.settings.postgres.port + "/" + encodeURIComponent(databaseName)
3502 + ' > ' + ((parent.platform == 'win32') ? '\"nul\"' : '\"/dev/null\"');
3503 const child_process = require('child_process');
3504 child_process.exec(cmd, { cwd: backupPath }, function(error, stdout, stdin) {
3505 if ((error != null) && (error != '')) {
3506 func(1, "pg_dump error, backup will not be performed. Check path or use pgdumppath.");
3507
3508 let processedError = error;
3509 if (typeof parent?.config?.settings?.postgres?.password === "string" && parent.config.settings.postgres.password.length > 0) {
3510 processedError = encodeURIComponent(processedError.replaceAll(parent.config.settings.postgres.password, "****"));
3511 }
3512 parent.debug('backup', 'PostgreSQL DumpTool: ' + processedError);
3513
3514 return;
3515 } else {parent.config.settings.autobackup.backupintervalhours = backupInterval;}
3516 });
3517 } else {
3518 //all ok, enable backup
3519 parent.config.settings.autobackup.backupintervalhours = backupInterval;}
3520 }
3521
3522 // MongoDB pending bulk read operation, perform fast bulk document reads.
3523 function fileBulkReadCompleted(err, docs) {
3524 // Send out callbacks with results
3525 if (docs != null) {
3526 for (var i in docs) {
3527 if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); }
3528 const id = docs[i]._id;
3529 if (obj.filePendingGets[id] != null) {
3530 for (var j in obj.filePendingGets[id]) {
3531 if (typeof obj.filePendingGets[id][j] == 'function') { obj.filePendingGets[id][j](err, performTypedRecordDecrypt([docs[i]])); }
3532 }
3533 delete obj.filePendingGets[id];
3534 }
3535 }
3536 }
3537
3538 // If there are not results, send out a null callback
3539 for (var i in obj.filePendingGets) { for (var j in obj.filePendingGets[i]) { obj.filePendingGets[i][j](err, []); } }
3540
3541 // Move on to process any more pending get operations
3542 obj.filePendingGets = obj.filePendingGet;
3543 obj.filePendingGet = null;
3544 if (obj.filePendingGets != null) {
3545 var findlist = [];
3546 for (var i in obj.filePendingGets) { findlist.push(i); }
3547 obj.file.find({ _id: { $in: findlist } }).toArray(fileBulkReadCompleted);
3548 }
3549 }
3550
3551 // MongoDB pending bulk remove operation, perform fast bulk document removes.
3552 function fileBulkRemoveCompleted(err) {
3553 // Send out callbacks
3554 for (var i in obj.filePendingRemoves) {
3555 for (var j in obj.filePendingRemoves[i]) {
3556 if (typeof obj.filePendingRemoves[i][j] == 'function') { obj.filePendingRemoves[i][j](err); }
3557 }
3558 }
3559
3560 // Move on to process any more pending get operations
3561 obj.filePendingRemoves = obj.filePendingRemove;
3562 obj.filePendingRemove = null;
3563 if (obj.filePendingRemoves != null) {
3564 obj.dbCounters.fileRemoveBulk++;
3565 var findlist = [], count = 0;
3566 for (var i in obj.filePendingRemoves) { findlist.push(i); count++; }
3567 obj.file.deleteMany({ _id: { $in: findlist } }, { multi: true }, fileBulkRemoveCompleted);
3568 }
3569 }
3570
3571 // MongoDB pending bulk write operation, perform fast bulk document replacement.
3572 function fileBulkWriteCompleted() {
3573 // Callbacks
3574 if (obj.filePendingCbs != null) {
3575 for (var i in obj.filePendingCbs) { if (typeof obj.filePendingCbs[i] == 'function') { obj.filePendingCbs[i](); } }
3576 obj.filePendingCbs = null;
3577 }
3578 if (obj.filePendingSets != null) {
3579 // Perform pending operations
3580 obj.dbCounters.fileSetBulk++;
3581 var ops = [];
3582 obj.filePendingCbs = obj.filePendingCb;
3583 obj.filePendingCb = null;
3584 for (var i in obj.filePendingSets) { ops.push({ replaceOne: { filter: { _id: i }, replacement: performTypedRecordEncrypt(common.escapeLinksFieldNameEx(obj.filePendingSets[i])), upsert: true } }); }
3585 obj.file.bulkWrite(ops, fileBulkWriteCompleted);
3586 obj.filePendingSets = null;
3587 } else {
3588 // All done, no pending operations.
3589 obj.filePendingSet = false;
3590 }
3591 }
3592
3593 // MongoDB pending bulk write operation, perform fast bulk document replacement.
3594 function eventsFileBulkWriteCompleted() {
3595 // Callbacks
3596 if (obj.eventsFilePendingCbs != null) { for (var i in obj.eventsFilePendingCbs) { obj.eventsFilePendingCbs[i](); } obj.eventsFilePendingCbs = null; }
3597 if (obj.eventsFilePendingSets != null) {
3598 // Perform pending operations
3599 obj.dbCounters.eventsSetBulk++;
3600 var ops = [];
3601 for (var i in obj.eventsFilePendingSets) { ops.push({ insertOne: { document: obj.eventsFilePendingSets[i] } }); }
3602 obj.eventsFilePendingCbs = obj.eventsFilePendingCb;
3603 obj.eventsFilePendingCb = null;
3604 obj.eventsFilePendingSets = null;
3605 obj.eventsfile.bulkWrite(ops, eventsFileBulkWriteCompleted);
3606 } else {
3607 // All done, no pending operations.
3608 obj.eventsFilePendingSet = false;
3609 }
3610 }
3611
3612 // MongoDB pending bulk write operation, perform fast bulk document replacement.
3613 function powerFileBulkWriteCompleted() {
3614 // Callbacks
3615 if (obj.powerFilePendingCbs != null) { for (var i in obj.powerFilePendingCbs) { obj.powerFilePendingCbs[i](); } obj.powerFilePendingCbs = null; }
3616 if (obj.powerFilePendingSets != null) {
3617 // Perform pending operations
3618 obj.dbCounters.powerSetBulk++;
3619 var ops = [];
3620 for (var i in obj.powerFilePendingSets) { ops.push({ insertOne: { document: obj.powerFilePendingSets[i] } }); }
3621 obj.powerFilePendingCbs = obj.powerFilePendingCb;
3622 obj.powerFilePendingCb = null;
3623 obj.powerFilePendingSets = null;
3624 obj.powerfile.bulkWrite(ops, powerFileBulkWriteCompleted);
3625 } else {
3626 // All done, no pending operations.
3627 obj.powerFilePendingSet = false;
3628 }
3629 }
3630
3631 // Perform a server backup
3632 obj.performBackup = function (func) {
3633 parent.debug('backup','Entering performBackup');
3634 try {
3635 if (obj.performingBackup) return 'Backup alreay in progress.';
3636 if (parent.config.settings.autobackup.backupintervalhours == -1) { if (func) { func('Backup disabled.'); return 'Backup disabled.' }};
3637 obj.performingBackup = true;
3638 let backupPath = parent.backuppath;
3639 let dataPath = parent.datapath;
3640
3641 const currentDate = new Date();
3642 const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2);
3643 obj.newAutoBackupFile = path.join(backupPath, parent.config.settings.autobackup.backupname + fileSuffix + '.zip');
3644 parent.debug('backup','newAutoBackupFile=' + obj.newAutoBackupFile);
3645
3646 if ((obj.databaseType == DB_MONGOJS) || (obj.databaseType == DB_MONGODB)) {
3647 // Perform a MongoDump
3648 const dbname = (parent.args.mongodbname) ? (parent.args.mongodbname) : 'meshcentral';
3649 const dburl = parent.args.mongodb;
3650
3651 obj.newDBDumpFile = path.join(backupPath, (dbname + '-mongodump-' + fileSuffix + '.archive'));
3652
3653 var cmd = buildMongoDumpCommand();
3654 cmd += (dburl) ? ' --archive=\"' + obj.newDBDumpFile + '\"' :
3655 ' --db=\"' + dbname + '\" --archive=\"' + obj.newDBDumpFile + '\"';
3656 parent.debug('backup','Mongodump cmd: ' + cmd);
3657 const child_process = require('child_process');
3658 const dumpProcess = child_process.exec(
3659 cmd,
3660 { cwd: parent.parentpath },
3661 (error)=> {if (error) {obj.backupStatus |= BACKUPFAIL_DBDUMP; console.error('ERROR: Unable to perform MongoDB backup: ' + error + '\r\n'); obj.createBackupfile(func);}}
3662 );
3663
3664 dumpProcess.on('exit', (code) => {
3665 if (code != 0) {console.log(`Mongodump child process exited with code ${code}`); obj.backupStatus |= BACKUPFAIL_DBDUMP;}
3666 obj.createBackupfile(func);
3667 });
3668
3669 } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
3670 // Perform a MySqlDump backup
3671 const newBackupFile = 'mysqldump-' + fileSuffix;
3672 obj.newDBDumpFile = path.join(backupPath, newBackupFile + '.sql');
3673
3674 var cmd = buildSqlDumpCommand();
3675 cmd += ' --result-file=\"' + obj.newDBDumpFile + '\"';
3676 parent.debug('backup','Maria/MySQLdump cmd: ' + cmd);
3677
3678 const child_process = require('child_process');
3679 const dumpProcess = child_process.exec(
3680 cmd,
3681 { cwd: parent.parentpath },
3682 (error)=> {if (error) {obj.backupStatus |= BACKUPFAIL_DBDUMP; console.error('ERROR: Unable to perform MySQL backup: ' + error + '\r\n'); obj.createBackupfile(func);}}
3683 );
3684 dumpProcess.on('exit', (code) => {
3685 if (code != 0) {console.error(`MySQLdump child process exited with code ${code}`); obj.backupStatus |= BACKUPFAIL_DBDUMP;}
3686 obj.createBackupfile(func);
3687 });
3688
3689 } else if (obj.databaseType == DB_SQLITE) {
3690 //.db3 suffix to escape escape backupfile glob to exclude the sqlite db files
3691 obj.newDBDumpFile = path.join(backupPath, databaseName + '-sqlitedump-' + fileSuffix + '.db3');
3692 // do a VACUUM INTO in favor of the backup API to compress the export, see https://www.sqlite.org/backup.html
3693 parent.debug('backup','SQLitedump: VACUUM INTO ' + obj.newDBDumpFile);
3694 obj.file.exec('VACUUM INTO \'' + obj.newDBDumpFile + '\'', function (err) {
3695 if (err) { console.error('SQLite backup error: ' + err); obj.backupStatus |=BACKUPFAIL_DBDUMP;};
3696 //always finish/clean up
3697 obj.createBackupfile(func);
3698 });
3699 } else if (obj.databaseType == DB_POSTGRESQL) {
3700 // Perform a PostgresDump backup
3701 const newBackupFile = 'pgdump-' + fileSuffix + '.sql';
3702 obj.newDBDumpFile = path.join(backupPath, newBackupFile);
3703 let cmd = '"' + parent.config.settings.autobackup.pgdumppath + '"'
3704 + ' --dbname=postgresql://' + encodeURIComponent(parent.config.settings.postgres.user) + ":" + encodeURIComponent(parent.config.settings.postgres.password)
3705 + "@" + parent.config.settings.postgres.host + ":" + parent.config.settings.postgres.port + "/" + encodeURIComponent(databaseName)
3706 + " --file=" + obj.newDBDumpFile;
3707 parent.debug('backup','Postgresqldump cmd: ' + cmd);
3708 const child_process = require('child_process');
3709 const dumpProcess = child_process.exec(
3710 cmd,
3711 { cwd: dataPath },
3712 (error)=> {if (error) {obj.backupStatus |= BACKUPFAIL_DBDUMP; console.log('ERROR: Unable to perform PostgreSQL dump: ' + error.message + '\r\n'); obj.createBackupfile(func);}}
3713 );
3714 dumpProcess.on('exit', (code) => {
3715 if (code != 0) {console.log(`PostgreSQLdump child process exited with code: ` + code); obj.backupStatus |= BACKUPFAIL_DBDUMP;}
3716 obj.createBackupfile(func);
3717 });
3718 } else {
3719 // NeDB/Acebase backup, no db dump needed, just make a file backup
3720 obj.createBackupfile(func);
3721 }
3722 } catch (ex) { console.error(ex); parent.addServerWarning( 'Something went wrong during performBackup, check errorlog: ' +ex.message, true); };
3723 return 'Starting auto-backup...';
3724 };
3725
3726 obj.createBackupfile = function(func) {
3727 parent.debug('backup', 'Entering createBackupfile');
3728 let archiver = require('archiver');
3729 let archive = null;
3730 let zipLevel = Math.min(Math.max(Number(parent.config.settings.autobackup.zipcompression ? parent.config.settings.autobackup.zipcompression : 5),1),9);
3731
3732 //if password defined, create encrypted zip
3733 if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.zippassword == 'string')) {
3734 try {
3735 //Only register format once, otherwise it triggers an error
3736 if (archiver.isRegisteredFormat('zip-encrypted') == false) { archiver.registerFormat('zip-encrypted', require('archiver-zip-encrypted')); }
3737 archive = archiver.create('zip-encrypted', { zlib: { level: zipLevel }, encryptionMethod: 'aes256', password: parent.config.settings.autobackup.zippassword });
3738 if (func) { func('Creating encrypted ZIP'); }
3739 } 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
3740 obj.backupStatus |= BACKUPFAIL_ZIPMODULE;
3741 if (func) { func('Zipencryptionmodule failed, aborting');}
3742 console.error('Zipencryptionmodule failed, aborting');
3743 }
3744 } else {
3745 if (func) { func('Creating a NON-ENCRYPTED ZIP'); }
3746 archive = archiver('zip', { zlib: { level: zipLevel } });
3747 }
3748
3749 //original behavior, just a filebackup if dbdump fails : (obj.backupStatus == 0 || obj.backupStatus == BACKUPFAIL_DBDUMP)
3750 if (obj.backupStatus == 0) {
3751 // Zip the data directory with the dbdump|NeDB files
3752 let output = fs.createWriteStream(obj.newAutoBackupFile);
3753
3754 // Archive finalized and closed
3755 output.on('close', function () {
3756 if (obj.backupStatus == 0) {
3757 let mesg = 'Auto-backup completed: ' + obj.newAutoBackupFile + ', backup-size: ' + ((archive.pointer() / 1048576).toFixed(2)) + "Mb";
3758 console.log(mesg);
3759 if (func) { func(mesg); };
3760 obj.performCloudBackup(obj.newAutoBackupFile, func);
3761 obj.removeExpiredBackupfiles(func);
3762
3763 } else {
3764 let mesg = 'Zipbackup failed (' + obj.backupStatus.toString(2).slice(-8) + '), deleting incomplete backup: ' + obj.newAutoBackupFile;
3765 if (func) { func(mesg) }
3766 else { parent.addServerWarning(mesg, true ) };
3767 if (fs.existsSync(obj.newAutoBackupFile)) { fs.unlink(obj.newAutoBackupFile, function (err) { if (err) {console.error('Failed to clean up backupfile: ' + err.message)} }) };
3768 };
3769 if (obj.databaseType != DB_NEDB) {
3770 //remove dump archive file, because zipped and otherwise fills up
3771 if (fs.existsSync(obj.newDBDumpFile)) { fs.unlink(obj.newDBDumpFile, function (err) { if (err) {console.error('Failed to clean up dbdump file: ' + err.message) } }) };
3772 };
3773 obj.performingBackup = false;
3774 obj.backupStatus = 0x0;
3775 }
3776 );
3777 output.on('end', function () { });
3778 output.on('error', function (err) {
3779 if ((obj.backupStatus & BACKUPFAIL_ZIPCREATE) == 0) {
3780 console.error('Output error: ' + err.message);
3781 if (func) { func('Output error: ' + err.message); };
3782 obj.backupStatus |= BACKUPFAIL_ZIPCREATE;
3783 archive.abort();
3784 };
3785 });
3786 archive.on('warning', function (err) {
3787 //if files added to the archiver object aren't reachable anymore (e.g. sqlite-journal files)
3788 //an ENOENT warning is given, but the archiver module has no option to/does not skip/resume
3789 //so the backup needs te be aborted as it otherwise leaves an incomplete zip and never 'ends'
3790 if ((obj.backupStatus & BACKUPFAIL_ZIPCREATE) == 0) {
3791 console.log('Zip warning: ' + err.message);
3792 if (func) { func('Zip warning: ' + err.message); };
3793 obj.backupStatus |= BACKUPFAIL_ZIPCREATE;
3794 archive.abort();
3795 };
3796 });
3797 archive.on('error', function (err) {
3798 if ((obj.backupStatus & BACKUPFAIL_ZIPCREATE) == 0) {
3799 console.error('Zip error: ' + err.message);
3800 if (func) { func('Zip error: ' + err.message); };
3801 obj.backupStatus |= BACKUPFAIL_ZIPCREATE;
3802 archive.abort();
3803 }
3804 });
3805 archive.pipe(output);
3806
3807 let globIgnoreFiles;
3808 //slice in case exclusion gets pushed
3809 globIgnoreFiles = parent.config.settings.autobackup.backupignorefilesglob ? parent.config.settings.autobackup.backupignorefilesglob.slice() : [];
3810 if (parent.config.settings.sqlite3) { globIgnoreFiles.push (datapathFoldername + '/' + databaseName + '.sqlite*'); }; //skip sqlite database file, and temp files with ext -journal, -wal & -shm
3811 //archiver.glob doesn't seem to use the third param, archivesubdir. Bug?
3812 //workaround: go up a dir and add data dir explicitly to keep the zip tidy
3813 archive.glob((datapathFoldername + '/**'), {
3814 cwd: datapathParentPath,
3815 ignore: globIgnoreFiles,
3816 skip: (parent.config.settings.autobackup.backupskipfoldersglob ? parent.config.settings.autobackup.backupskipfoldersglob : [])
3817 });
3818
3819 if (parent.config.settings.autobackup.backupwebfolders) {
3820 if (parent.webViewsOverridePath) { archive.directory(parent.webViewsOverridePath, 'meshcentral-views'); }
3821 if (parent.webPublicOverridePath) { archive.directory(parent.webPublicOverridePath, 'meshcentral-public'); }
3822 if (parent.webEmailsOverridePath) { archive.directory(parent.webEmailsOverridePath, 'meshcentral-emails'); }
3823 };
3824 if (parent.config.settings.autobackup.backupotherfolders) {
3825 archive.directory(parent.filespath, 'meshcentral-files');
3826 archive.directory(parent.recordpath, 'meshcentral-recordings');
3827 };
3828 //add dbdump to the root of the zip
3829 if (obj.newDBDumpFile != null) archive.file(obj.newDBDumpFile, { name: path.basename(obj.newDBDumpFile) });
3830 archive.finalize();
3831 } else {
3832 //failed somewhere before zipping
3833 console.error('Backup failed ('+ obj.backupStatus.toString(2).slice(-8) + ')');
3834 if (func) { func('Backup failed ('+ obj.backupStatus.toString(2).slice(-8) + ')') }
3835 else {
3836 parent.addServerWarning('Backup failed ('+ obj.backupStatus.toString(2).slice(-8) + ')', true);
3837 }
3838 //Just in case something's there
3839 if (fs.existsSync(obj.newDBDumpFile)) { fs.unlink(obj.newDBDumpFile, function (err) { if (err) {console.error('Failed to clean up dbdump file: ' + err.message) } }); };
3840 obj.backupStatus = 0x0;
3841 obj.performingBackup = false;
3842 };
3843 };
3844
3845 // Remove expired backupfiles by filenamedate
3846 obj.removeExpiredBackupfiles = function (func) {
3847 if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.keeplastdaysbackup == 'number')) {
3848 let cutoffDate = new Date();
3849 cutoffDate.setDate(cutoffDate.getDate() - parent.config.settings.autobackup.keeplastdaysbackup);
3850 fs.readdir(parent.backuppath, function (err, dir) {
3851 try {
3852 if (err == null) {
3853 if (dir.length > 0) {
3854 let fileName = parent.config.settings.autobackup.backupname;
3855 let checked = 0;
3856 let removed = 0;
3857 for (var i in dir) {
3858 var name = dir[i];
3859 parent.debug('backup', "checking file: ", path.join(parent.backuppath, name));
3860 if (name.startsWith(fileName) && name.endsWith('.zip')) {
3861 var timex = name.substring(fileName.length, name.length - 4).split('-');
3862 if (timex.length == 5) {
3863 checked++;
3864 var fileDate = new Date(parseInt(timex[0]), parseInt(timex[1]) - 1, parseInt(timex[2]), parseInt(timex[3]), parseInt(timex[4]));
3865 if (fileDate && (cutoffDate > fileDate)) {
3866 console.log("Removing expired backup file: ", path.join(parent.backuppath, name));
3867 fs.unlink(path.join(parent.backuppath, name), function (err) { if (err) { console.error(err.message); if (func) {func('Error removing: ' + err.message); } } });
3868 removed++;
3869 }
3870 }
3871 else { parent.debug('backup', "file: " + name + " timestamp failure: ", timex); }
3872 }
3873 }
3874 let mesg= 'Checked ' + checked + ' candidates in ' + parent.backuppath + '. Removed ' + removed + ' expired backupfiles using cutoffDate: '+ cutoffDate.toLocaleString('default', { dateStyle: 'short', timeStyle: 'short' });
3875 parent.debug (mesg);
3876 if (func) { func(mesg); }
3877 } else { console.error('No files found in ' + parent.backuppath + '. There should be at least one.')}
3878 }
3879 else
3880 { console.error(err); parent.addServerWarning( 'Reading files in backup directory ' + parent.backuppath + ' failed, check errorlog: ' + err.message, true); }
3881 } catch (ex) { console.error(ex); parent.addServerWarning( 'Something went wrong during removeExpiredBackupfiles, check errorlog: ' +ex.message, true); }
3882 });
3883 }
3884 }
3885
3886 async function webDAVBackup(filename, func) {
3887 try {
3888 const webDAV = await import ('webdav');
3889 const wdConfig = parent.config.settings.autobackup.webdav;
3890 const client = webDAV.createClient(wdConfig.url, {
3891 username: wdConfig.username,
3892 password: wdConfig.password,
3893 maxContentLength: Infinity,
3894 maxBodyLength: Infinity
3895 });
3896 if (await client.exists(wdConfig.foldername) === false) {
3897 await client.createDirectory(wdConfig.foldername, { recursive: true});
3898 } else {
3899 // Clean up our WebDAV folder
3900 if ((typeof wdConfig.maxfiles == 'number') && (wdConfig.maxfiles > 1)) {
3901 const fileName = parent.config.settings.autobackup.backupname;
3902 //only files matching our backupfilename
3903 let files = await client.getDirectoryContents(wdConfig.foldername, { deep: false, glob: "/**/" + fileName + "*.zip" });
3904 const xdateTimeSort = function (a, b) { if (a.xdate > b.xdate) return 1; if (a.xdate < b.xdate) return -1; return 0; }
3905 for (const i in files) { files[i].xdate = new Date(files[i].lastmod); }
3906 files.sort(xdateTimeSort);
3907 while (files.length >= wdConfig.maxfiles) {
3908 let delFile = files.shift().filename;
3909 await client.deleteFile(delFile);
3910 console.log('WebDAV file deleted: ' + delFile); if (func) { func('WebDAV file deleted: ' + delFile); }
3911 }
3912 }
3913 }
3914 // Upload to the WebDAV folder
3915 const { pipeline } = require('stream/promises');
3916 await pipeline(fs.createReadStream(filename), client.createWriteStream( wdConfig.foldername + path.basename(filename)));
3917 console.log('WebDAV upload completed: ' + wdConfig.foldername + path.basename(filename)); if (func) { func('WebDAV upload completed: ' + wdConfig.foldername + path.basename(filename)); }
3918 }
3919 catch(err) {
3920 console.error('WebDAV error: ' + err.message); if (func) { func('WebDAV error: ' + err.message);}
3921 }
3922 }
3923
3924 // Perform cloud backup
3925 obj.performCloudBackup = function (filename, func) {
3926 // WebDAV Backup
3927 if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.webdav == 'object')) {
3928 parent.debug( 'backup', 'Entering WebDAV backup'); if (func) { func('Entering WebDAV backup.'); }
3929 webDAVBackup(filename, func);
3930 }
3931
3932 // Google Drive Backup
3933 if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.googledrive == 'object')) {
3934 parent.debug( 'backup', 'Entering Google Drive backup');
3935 obj.Get('GoogleDriveBackup', function (err, docs) {
3936 if ((err != null) || (docs.length != 1) || (docs[0].state != 3)) return;
3937 if (func) { func('Attempting Google Drive upload...'); }
3938 const {google} = require('googleapis');
3939 const oAuth2Client = new google.auth.OAuth2(docs[0].clientid, docs[0].clientsecret, "urn:ietf:wg:oauth:2.0:oob");
3940 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
3941 oAuth2Client.setCredentials(docs[0].token);
3942 const drive = google.drive({ version: 'v3', auth: oAuth2Client });
3943 const createdTimeSort = function (a, b) { if (a.createdTime > b.createdTime) return 1; if (a.createdTime < b.createdTime) return -1; return 0; }
3944
3945 // Called once we know our folder id, clean up and upload a backup.
3946 var useGoogleDrive = function (folderid) {
3947 // List files to see if we need to delete older ones
3948 if (typeof parent.config.settings.autobackup.googledrive.maxfiles == 'number') {
3949 drive.files.list({
3950 q: 'trashed = false and \'' + folderid + '\' in parents',
3951 fields: 'nextPageToken, files(id, name, size, createdTime)',
3952 }, function (err, res) {
3953 if (err) {
3954 console.log('GoogleDrive (files.list) error: ' + err);
3955 if (func) { func('GoogleDrive (files.list) error: ' + err); }
3956 return;
3957 }
3958 // Delete any old files if more than 10 files are present in the backup folder.
3959 res.data.files.sort(createdTimeSort);
3960 while (res.data.files.length >= parent.config.settings.autobackup.googledrive.maxfiles) { drive.files.delete({ fileId: res.data.files.shift().id }, function (err, res) { }); }
3961 });
3962 }
3963
3964 //console.log('Uploading...');
3965 if (func) { func('Uploading to Google Drive...'); }
3966
3967 // Upload the backup
3968 drive.files.create({
3969 requestBody: { name: require('path').basename(filename), mimeType: 'text/plain', parents: [folderid] },
3970 media: { mimeType: 'application/zip', body: require('fs').createReadStream(filename) },
3971 }, function (err, res) {
3972 if (err) {
3973 console.log('GoogleDrive (files.create) error: ' + err);
3974 if (func) { func('GoogleDrive (files.create) error: ' + err); }
3975 return;
3976 }
3977 //console.log('Upload done.');
3978 if (func) { func('Google Drive upload completed.'); }
3979 });
3980 }
3981
3982 // Fetch the folder name
3983 var folderName = 'MeshCentral-Backups';
3984 if (typeof parent.config.settings.autobackup.googledrive.foldername == 'string') { folderName = parent.config.settings.autobackup.googledrive.foldername; }
3985
3986 // Find our backup folder, create one if needed.
3987 drive.files.list({
3988 q: 'mimeType = \'application/vnd.google-apps.folder\' and name=\'' + folderName + '\' and trashed = false',
3989 fields: 'nextPageToken, files(id, name)',
3990 }, function (err, res) {
3991 if (err) {
3992 console.log('GoogleDrive error: ' + err);
3993 if (func) { func('GoogleDrive error: ' + err); }
3994 return;
3995 }
3996 if (res.data.files.length == 0) {
3997 // Create a folder
3998 drive.files.create({ resource: { 'name': folderName, 'mimeType': 'application/vnd.google-apps.folder' }, fields: 'id' }, function (err, file) {
3999 if (err) {
4000 console.log('GoogleDrive (folder.create) error: ' + err);
4001 if (func) { func('GoogleDrive (folder.create) error: ' + err); }
4002 return;
4003 }
4004 useGoogleDrive(file.data.id);
4005 });
4006 } else { useGoogleDrive(res.data.files[0].id); }
4007 });
4008 });
4009 }
4010
4011 // S3 Backup
4012 if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.s3 == 'object')) {
4013 parent.debug( 'backup', 'Entering S3 backup');
4014 var s3folderName = 'MeshCentral-Backups';
4015 if (typeof parent.config.settings.autobackup.s3.foldername == 'string') { s3folderName = parent.config.settings.autobackup.s3.foldername; }
4016 // Construct the config object
4017 var accessKey = parent.config.settings.autobackup.s3.accesskey,
4018 secretKey = parent.config.settings.autobackup.s3.secretkey,
4019 endpoint = parent.config.settings.autobackup.s3.endpoint ? parent.config.settings.autobackup.s3.endpoint : 's3.amazonaws.com',
4020 port = parent.config.settings.autobackup.s3.port ? parent.config.settings.autobackup.s3.port : 443,
4021 useSsl = parent.config.settings.autobackup.s3.ssl ? parent.config.settings.autobackup.s3.ssl : true,
4022 bucketName = parent.config.settings.autobackup.s3.bucketname,
4023 pathPrefix = s3folderName,
4024 threshold = parent.config.settings.autobackup.s3.maxfiles ? parent.config.settings.autobackup.s3.maxfiles : 0,
4025 fileToUpload = filename;
4026 // Create a MinIO client
4027 const Minio = require('minio');
4028 var minioClient = new Minio.Client({
4029 endPoint: endpoint,
4030 port: port,
4031 useSSL: useSsl,
4032 accessKey: accessKey,
4033 secretKey: secretKey
4034 });
4035 // List objects in the specified bucket and path prefix
4036 var listObjectsPromise = new Promise(function(resolve, reject) {
4037 var items = [];
4038 var stream = minioClient.listObjects(bucketName, pathPrefix, true);
4039 stream.on('data', function(item) {
4040 if (!item.name.endsWith('/')) { // Exclude directories
4041 items.push(item);
4042 }
4043 });
4044 stream.on('end', function() {
4045 resolve(items);
4046 });
4047 stream.on('error', function(err) {
4048 reject(err);
4049 });
4050 });
4051 listObjectsPromise.then(function(objects) {
4052 // Count the number of files
4053 var fileCount = objects.length;
4054 // Return if no files to carry on uploading
4055 if (fileCount === 0) { return Promise.resolve(); }
4056 // Sort the files by LastModified date (oldest first)
4057 objects.sort(function(a, b) { return new Date(a.lastModified) - new Date(b.lastModified); });
4058 // Check if the threshold is zero and return if
4059 if (threshold === 0) { return Promise.resolve(); }
4060 // Check if the number of files exceeds the threshold (maxfiles) is 0
4061 if (fileCount >= threshold) {
4062 // Calculate how many files need to be deleted to make space for the new file
4063 var filesToDelete = fileCount - threshold + 1; // +1 to make space for the new file
4064 if (func) { func('Deleting ' + filesToDelete + ' older ' + (filesToDelete == 1 ? 'file' : 'files') + ' from S3 ...'); }
4065 // Create an array of promises for deleting files
4066 var deletePromises = objects.slice(0, filesToDelete).map(function(fileToDelete) {
4067 return new Promise(function(resolve, reject) {
4068 minioClient.removeObject(bucketName, fileToDelete.name, function(err) {
4069 if (err) {
4070 reject(err);
4071 } else {
4072 if (func) { func('Deleted file: ' + fileToDelete.name + ' from S3'); }
4073 resolve();
4074 }
4075 });
4076 });
4077 });
4078 // Wait for all deletions to complete
4079 return Promise.all(deletePromises);
4080 } else {
4081 return Promise.resolve(); // No deletion needed
4082 }
4083 }).then(function() {
4084 // Determine the upload path by combining the pathPrefix with the filename
4085 var fileName = require('path').basename(fileToUpload);
4086 var uploadPath = require('path').join(pathPrefix, fileName);
4087 // Upload a new file
4088 var uploadPromise = new Promise(function(resolve, reject) {
4089 if (func) { func('Uploading file ' + uploadPath + ' to S3'); }
4090 minioClient.fPutObject(bucketName, uploadPath, fileToUpload, function(err, etag) {
4091 if (err) {
4092 reject(err);
4093 } else {
4094 if (func) { func('Uploaded file: ' + uploadPath + ' to S3'); }
4095 resolve(etag);
4096 }
4097 });
4098 });
4099 return uploadPromise;
4100 }).catch(function(error) {
4101 if (func) { func('Error managing files in S3: ' + error); }
4102 });
4103 }
4104 }
4105
4106 // Transfer NeDB data into the current database
4107 obj.nedbtodb = function (func) {
4108 var nedbDatastore = null;
4109 try { nedbDatastore = require('@seald-io/nedb'); } catch (ex) { } // This is the NeDB with Node 23 support.
4110 if (nedbDatastore == null) {
4111 try { nedbDatastore = require('@yetzt/nedb'); } catch (ex) { } // This is the NeDB with fixed security dependencies.
4112 if (nedbDatastore == null) { nedbDatastore = require('nedb'); } // So not to break any existing installations, if the old NeDB is present, use it.
4113 }
4114
4115 var datastoreOptions = { filename: parent.getConfigFilePath('meshcentral.db'), autoload: true };
4116
4117 // If a DB encryption key is provided, perform database encryption
4118 if ((typeof parent.args.dbencryptkey == 'string') && (parent.args.dbencryptkey.length != 0)) {
4119 // Hash the database password into a AES256 key and setup encryption and decryption.
4120 var nedbKey = parent.crypto.createHash('sha384').update(parent.args.dbencryptkey).digest('raw').slice(0, 32);
4121 datastoreOptions.afterSerialization = function (plaintext) {
4122 const iv = parent.crypto.randomBytes(16);
4123 const aes = parent.crypto.createCipheriv('aes-256-cbc', nedbKey, iv);
4124 var ciphertext = aes.update(plaintext);
4125 ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
4126 return ciphertext.toString('base64');
4127 }
4128 datastoreOptions.beforeDeserialization = function (ciphertext) {
4129 const ciphertextBytes = Buffer.from(ciphertext, 'base64');
4130 const iv = ciphertextBytes.slice(0, 16);
4131 const data = ciphertextBytes.slice(16);
4132 const aes = parent.crypto.createDecipheriv('aes-256-cbc', nedbKey, iv);
4133 var plaintextBytes = Buffer.from(aes.update(data));
4134 plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
4135 return plaintextBytes.toString();
4136 }
4137 }
4138
4139 // Setup all NeDB collections
4140 var nedbfile = new nedbDatastore(datastoreOptions);
4141 var nedbeventsfile = new nedbDatastore({ filename: parent.getConfigFilePath('meshcentral-events.db'), autoload: true, corruptAlertThreshold: 1 });
4142 var nedbpowerfile = new nedbDatastore({ filename: parent.getConfigFilePath('meshcentral-power.db'), autoload: true, corruptAlertThreshold: 1 });
4143 var nedbserverstatsfile = new nedbDatastore({ filename: parent.getConfigFilePath('meshcentral-stats.db'), autoload: true, corruptAlertThreshold: 1 });
4144
4145 // Transfered record counts
4146 var normalRecordsTransferCount = 0;
4147 var eventRecordsTransferCount = 0;
4148 var powerRecordsTransferCount = 0;
4149 var statsRecordsTransferCount = 0;
4150 obj.pendingTransfer = 0;
4151
4152 // Transfer the data from main database
4153 nedbfile.find({}, function (err, docs) {
4154 if ((err == null) && (docs.length > 0)) {
4155 performTypedRecordDecrypt(docs)
4156 for (var i in docs) {
4157 obj.pendingTransfer++;
4158 normalRecordsTransferCount++;
4159 obj.Set(common.unEscapeLinksFieldName(docs[i]), function () { obj.pendingTransfer--; });
4160 }
4161 }
4162
4163 // Transfer events
4164 nedbeventsfile.find({}, function (err, docs) {
4165 if ((err == null) && (docs.length > 0)) {
4166 for (var i in docs) {
4167 obj.pendingTransfer++;
4168 eventRecordsTransferCount++;
4169 obj.StoreEvent(docs[i], function () { obj.pendingTransfer--; });
4170 }
4171 }
4172
4173 // Transfer power events
4174 nedbpowerfile.find({}, function (err, docs) {
4175 if ((err == null) && (docs.length > 0)) {
4176 for (var i in docs) {
4177 obj.pendingTransfer++;
4178 powerRecordsTransferCount++;
4179 obj.storePowerEvent(docs[i], null, function () { obj.pendingTransfer--; });
4180 }
4181 }
4182
4183 // Transfer server stats
4184 nedbserverstatsfile.find({}, function (err, docs) {
4185 if ((err == null) && (docs.length > 0)) {
4186 for (var i in docs) {
4187 obj.pendingTransfer++;
4188 statsRecordsTransferCount++;
4189 obj.SetServerStats(docs[i], function () { obj.pendingTransfer--; });
4190 }
4191 }
4192
4193 // Only exit when all the records are stored.
4194 setInterval(function () {
4195 if (obj.pendingTransfer == 0) { func("Done. " + normalRecordsTransferCount + " record(s), " + eventRecordsTransferCount + " event(s), " + powerRecordsTransferCount + " power change(s), " + statsRecordsTransferCount + " stat(s)."); }
4196 }, 200)
4197 });
4198 });
4199 });
4200 });
4201 }
4202
4203 function padNumber(number, digits) { return Array(Math.max(digits - String(number).length + 1, 0)).join(0) + number; }
4204
4205 // Called when a node has changed
4206 function dbNodeChange(nodeChange, added) {
4207 if (parent.webserver == null) return;
4208 common.unEscapeLinksFieldName(nodeChange.fullDocument);
4209 const node = performTypedRecordDecrypt([nodeChange.fullDocument])[0];
4210 parent.DispatchEvent(['*', node.meshid], obj, { etype: 'node', action: (added ? 'addnode' : 'changenode'), node: parent.webserver.CloneSafeNode(node), nodeid: node._id, domain: node.domain, nolog: 1 });
4211 }
4212
4213 // Called when a device group has changed
4214 function dbMeshChange(meshChange, added) {
4215 if (parent.webserver == null) return;
4216 common.unEscapeLinksFieldName(meshChange.fullDocument);
4217 const mesh = performTypedRecordDecrypt([meshChange.fullDocument])[0];
4218
4219 // Update the mesh object in memory
4220 const mmesh = parent.webserver.meshes[mesh._id];
4221 if (mmesh != null) {
4222 // Update an existing device group
4223 for (var i in mesh) { mmesh[i] = mesh[i]; }
4224 for (var i in mmesh) { if (mesh[i] == null) { delete mmesh[i]; } }
4225 } else {
4226 // Device group not present, create it.
4227 parent.webserver.meshes[mesh._id] = mesh;
4228 }
4229
4230 // Send the mesh update
4231 var mesh2 = Object.assign({}, mesh); // Shallow clone
4232 if (mesh2.deleted) { mesh2.action = 'deletemesh'; } else { mesh2.action = (added ? 'createmesh' : 'meshchange'); }
4233 mesh2.meshid = mesh2._id;
4234 mesh2.nolog = 1;
4235 delete mesh2.type;
4236 delete mesh2._id;
4237 parent.DispatchEvent(['*', mesh2.meshid], obj, parent.webserver.CloneSafeMesh(mesh2));
4238 }
4239
4240 // Called when a user account has changed
4241 function dbUserChange(userChange, added) {
4242 if (parent.webserver == null) return;
4243 common.unEscapeLinksFieldName(userChange.fullDocument);
4244 const user = performTypedRecordDecrypt([userChange.fullDocument])[0];
4245
4246 // Update the user object in memory
4247 const muser = parent.webserver.users[user._id];
4248 if (muser != null) {
4249 // Update an existing user
4250 for (var i in user) { muser[i] = user[i]; }
4251 for (var i in muser) { if (user[i] == null) { delete muser[i]; } }
4252 } else {
4253 // User not present, create it.
4254 parent.webserver.users[user._id] = user;
4255 }
4256
4257 // Send the user update
4258 var targets = ['*', 'server-users', user._id];
4259 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
4260 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 });
4261 }
4262
4263 // Called when a user group has changed
4264 function dbUGrpChange(ugrpChange, added) {
4265 if (parent.webserver == null) return;
4266 common.unEscapeLinksFieldName(ugrpChange.fullDocument);
4267 const usergroup = ugrpChange.fullDocument;
4268
4269 // Update the user group object in memory
4270 const uusergroup = parent.webserver.userGroups[usergroup._id];
4271 if (uusergroup != null) {
4272 // Update an existing user group
4273 for (var i in usergroup) { uusergroup[i] = usergroup[i]; }
4274 for (var i in uusergroup) { if (usergroup[i] == null) { delete uusergroup[i]; } }
4275 } else {
4276 // Usergroup not present, create it.
4277 parent.webserver.userGroups[usergroup._id] = usergroup;
4278 }
4279
4280 // Send the user group update
4281 var usergroup2 = Object.assign({}, usergroup); // Shallow clone
4282 usergroup2.action = (added ? 'createusergroup' : 'usergroupchange');
4283 usergroup2.ugrpid = usergroup2._id;
4284 usergroup2.nolog = 1;
4285 delete usergroup2.type;
4286 delete usergroup2._id;
4287 parent.DispatchEvent(['*', usergroup2.ugrpid], obj, usergroup2);
4288 }
4289
4290 function dbMergeSqlArray(arr) {
4291 var x = '';
4292 for (var i in arr) { if (x != '') { x += ','; } x += '\'' + arr[i] + '\''; }
4293 return x;
4294 }
4295
4296 return obj;
4297 };