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