Started work on using the official MongoDB module.

Ylian Saint-Hilaire committed May 8, 2019 at 18:14 UTC f01b4f7ee0bf75d4a8e2d0362a2723b713a04716
3 files changed +1204 -488
db-test.js new
+487
@@ -0,0 +1,487 @@
1 +/**
2 +* @description MeshCentral database module
3 +* @author Ylian Saint-Hilaire
4 +* @copyright Intel Corporation 2018-2019
5 +* @license Apache-2.0
6 +* @version v0.0.2
7 +*/
8 +
9 +/*xjslint node: true */
10 +/*xjslint plusplus: true */
11 +/*xjslint maxlen: 256 */
12 +/*jshint node: true */
13 +/*jshint strict: false */
14 +/*jshint esversion: 6 */
15 +"use strict";
16 +
17 +//
18 +// Construct Meshcentral database object
19 +//
20 +// The default database is NeDB
21 +// https://github.com/louischatriot/nedb
22 +//
23 +// Alternativety, MongoDB can be used
24 +// https://www.mongodb.com/
25 +// Just run with --mongodb [connectionstring], where the connection string is documented here: https://docs.mongodb.com/manual/reference/connection-string/
26 +// The default collection is "meshcentral", but you can override it using --mongodbcol [collection]
27 +//
28 +module.exports.CreateDB = function (parent, func) {
29 + var obj = {};
30 + var Datastore = null;
31 + var expireEventsSeconds = (60 * 60 * 24 * 20); // By default, expire events after 20 days. (Seconds * Minutes * Hours * Days)
32 + var expirePowerEventsSeconds = (60 * 60 * 24 * 10); // By default, expire power events after 10 days. (Seconds * Minutes * Hours * Days)
33 + var expireServerStatsSeconds = (60 * 60 * 24 * 30); // By default, expire power events after 30 days. (Seconds * Minutes * Hours * Days)
34 + obj.path = require('path');
35 + obj.parent = parent;
36 + obj.identifier = null;
37 + obj.dbKey = null;
38 +
39 + // Read expiration time from configuration file
40 + if (typeof obj.parent.args.dbexpire == 'object') {
41 + if (typeof obj.parent.args.dbexpire.events == 'number') { expireEventsSeconds = obj.parent.args.dbexpire.events; }
42 + if (typeof obj.parent.args.dbexpire.powerevents == 'number') { expirePowerEventsSeconds = obj.parent.args.dbexpire.powerevents; }
43 + if (typeof obj.parent.args.dbexpire.statsevents == 'number') { expireServerStatsSeconds = obj.parent.args.dbexpire.statsevents; }
44 + }
45 +
46 + if (obj.parent.args.mongodb) {
47 + // Use MongoDB
48 + obj.databaseType = 2;
49 + Datastore = require('mongodb').MongoClient;
50 + Datastore.connect(obj.parent.args.mongodb, function (err, client) {
51 + if (err != null) { console.log("Unable to connect to database: " + err); process.exit(); return; }
52 + const db = client.db('meshcentral');
53 +
54 + var dbcollection = 'meshcentral';
55 + if (obj.parent.args.mongodbcol) { dbcollection = obj.parent.args.mongodbcol; }
56 +
57 + // Setup MongoDB main collection and indexes
58 + obj.file = db.collection(dbcollection);
59 +
60 + obj.file.find({ type: 'mesh' }, function (err, cursor) {
61 + cursor.each(function (err, item) {
62 + console.log(err, item);
63 + });
64 + });
65 +
66 +
67 + /*
68 + obj.file.getIndexes(function (err, indexes) {
69 + // Check if we need to reset indexes
70 + var indexesByName = {}, indexCount = 0;
71 + for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
72 + if ((indexCount != 4) || (indexesByName['TypeDomainMesh1'] == null) || (indexesByName['Email1'] == null) || (indexesByName['Mesh1'] == null)) {
73 + console.log('Resetting main indexes...');
74 + obj.file.dropIndexes(function (err) {
75 + obj.file.createIndex({ type: 1, domain: 1, meshid: 1 }, { sparse: 1, name: 'TypeDomainMesh1' }); // Speeds up GetAllTypeNoTypeField() and GetAllTypeNoTypeFieldMeshFiltered()
76 + obj.file.createIndex({ email: 1 }, { sparse: 1, name: 'Email1' }); // Speeds up GetUserWithEmail() and GetUserWithVerifiedEmail()
77 + obj.file.createIndex({ meshid: 1 }, { sparse: 1, name: 'Mesh1' }); // Speeds up RemoveMesh()
78 + });
79 + }
80 + });
81 + */
82 +
83 + /*
84 + // Setup the changeStream on the MongoDB main collection
85 + obj.fileChangeStream = obj.file.watch();
86 + obj.fileChangeStream.on('change', function (next) {
87 + // Process next document
88 + console.log('change', next);
89 + });
90 + */
91 +
92 + // Setup MongoDB events collection and indexes
93 + obj.eventsfile = db.collection('events'); // Collection containing all events
94 + /*
95 + obj.eventsfile.getIndexes(function (err, indexes) {
96 + // Check if we need to reset indexes
97 + var indexesByName = {}, indexCount = 0;
98 + for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
99 + if ((indexCount != 5) || (indexesByName['Username1'] == null) || (indexesByName['DomainNodeTime1'] == null) || (indexesByName['IdsAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
100 + // Reset all indexes
101 + console.log('Resetting events indexes...');
102 + obj.eventsfile.dropIndexes(function (err) {
103 + obj.eventsfile.createIndex({ username: 1 }, { sparse: 1, name: 'Username1' });
104 + obj.eventsfile.createIndex({ domain: 1, nodeid: 1, time: -1 }, { sparse: 1, name: 'DomainNodeTime1' });
105 + obj.eventsfile.createIndex({ ids: 1, time: -1 }, { sparse: 1, name: 'IdsAndTime1' });
106 + obj.eventsfile.createIndex({ "time": 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
107 + });
108 + } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireEventsSeconds) {
109 + // Reset the timeout index
110 + console.log('Resetting events expire index...');
111 + obj.eventsfile.dropIndex("ExpireTime1", function (err) {
112 + obj.eventsfile.createIndex({ "time": 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
113 + });
114 + }
115 + });
116 + */
117 +
118 + // Setup MongoDB power events collection and indexes
119 + obj.powerfile = db.collection('power'); // Collection containing all power events
120 + /*
121 + obj.powerfile.getIndexes(function (err, indexes) {
122 + // Check if we need to reset indexes
123 + var indexesByName = {}, indexCount = 0;
124 + for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
125 + if ((indexCount != 3) || (indexesByName['NodeIdAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
126 + // Reset all indexes
127 + console.log('Resetting power events indexes...');
128 + obj.powerfile.dropIndexes(function (err) {
129 + // Create all indexes
130 + obj.powerfile.createIndex({ nodeid: 1, time: 1 }, { sparse: 1, name: 'NodeIdAndTime1' });
131 + obj.powerfile.createIndex({ "time": 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
132 + });
133 + } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expirePowerEventsSeconds) {
134 + // Reset the timeout index
135 + console.log('Resetting power events expire index...');
136 + obj.powerfile.dropIndex("ExpireTime1", function (err) {
137 + // Reset the expire power events index
138 + obj.powerfile.createIndex({ "time": 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
139 + });
140 + }
141 + });
142 + */
143 +
144 + // Setup MongoDB smbios collection, no indexes needed
145 + obj.smbiosfile = db.collection('smbios'); // Collection containing all smbios information
146 +
147 + // Setup MongoDB server stats collection
148 + obj.serverstatsfile = db.collection('serverstats'); // Collection of server stats
149 + /*
150 + obj.serverstatsfile.getIndexes(function (err, indexes) {
151 + // Check if we need to reset indexes
152 + var indexesByName = {}, indexCount = 0;
153 + for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
154 + if ((indexCount != 3) || (indexesByName['ExpireTime1'] == null)) {
155 + // Reset all indexes
156 + console.log('Resetting server stats indexes...');
157 + obj.serverstatsfile.dropIndexes(function (err) {
158 + // Create all indexes
159 + obj.serverstatsfile.createIndex({ "time": 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
160 + obj.serverstatsfile.createIndex({ "expire": 1 }, { expireAfterSeconds: 0, name: 'ExpireTime2' }); // Auto-expire events
161 + });
162 + } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireServerStatsSeconds) {
163 + // Reset the timeout index
164 + console.log('Resetting server stats expire index...');
165 + obj.serverstatsfile.dropIndex("ExpireTime1", function (err) {
166 + // Reset the expire server stats index
167 + obj.serverstatsfile.createIndex({ "time": 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
168 + });
169 + }
170 + });
171 + */
172 + func(); // Completed MongoDB setup
173 + });
174 + } else {
175 + // Use NeDB (The default)
176 + obj.databaseType = 1;
177 + Datastore = require('nedb');
178 + var datastoreOptions = { filename: obj.parent.getConfigFilePath('meshcentral.db'), autoload: true };
179 +
180 + // If a DB encryption key is provided, perform database encryption
181 + if ((typeof obj.parent.args.dbencryptkey == 'string') && (obj.parent.args.dbencryptkey.length != 0)) {
182 + // Hash the database password into a AES256 key and setup encryption and decryption.
183 + obj.dbKey = obj.parent.crypto.createHash('sha384').update(obj.parent.args.dbencryptkey).digest("raw").slice(0, 32);
184 + datastoreOptions.afterSerialization = function (plaintext) {
185 + const iv = obj.parent.crypto.randomBytes(16);
186 + const aes = obj.parent.crypto.createCipheriv('aes-256-cbc', obj.dbKey, iv);
187 + var ciphertext = aes.update(plaintext);
188 + ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
189 + return ciphertext.toString('base64');
190 + }
191 + datastoreOptions.beforeDeserialization = function (ciphertext) {
192 + const ciphertextBytes = Buffer.from(ciphertext, 'base64');
193 + const iv = ciphertextBytes.slice(0, 16);
194 + const data = ciphertextBytes.slice(16);
195 + const aes = obj.parent.crypto.createDecipheriv('aes-256-cbc', obj.dbKey, iv);
196 + var plaintextBytes = Buffer.from(aes.update(data));
197 + plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
198 + return plaintextBytes.toString();
199 + }
200 + }
201 +
202 + // Start NeDB main collection and setup indexes
203 + obj.file = new Datastore(datastoreOptions);
204 + obj.file.persistence.setAutocompactionInterval(36000);
205 + obj.file.ensureIndex({ fieldName: 'type' });
206 + obj.file.ensureIndex({ fieldName: 'domain' });
207 + obj.file.ensureIndex({ fieldName: 'meshid', sparse: true });
208 + obj.file.ensureIndex({ fieldName: 'nodeid', sparse: true });
209 + obj.file.ensureIndex({ fieldName: 'email', sparse: true });
210 +
211 + // Setup the events collection and setup indexes
212 + obj.eventsfile = new Datastore({ filename: obj.parent.getConfigFilePath('meshcentral-events.db'), autoload: true });
213 + obj.eventsfile.persistence.setAutocompactionInterval(36000);
214 + obj.eventsfile.ensureIndex({ fieldName: 'ids' }); // TODO: Not sure if this is a good index, this is a array field.
215 + obj.eventsfile.ensureIndex({ fieldName: 'nodeid', sparse: true });
216 + obj.eventsfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: 60 * 60 * 24 * 20 }); // Limit the power event log to 20 days (Seconds * Minutes * Hours * Days)
217 +
218 + // Setup the power collection and setup indexes
219 + obj.powerfile = new Datastore({ filename: obj.parent.getConfigFilePath('meshcentral-power.db'), autoload: true });
220 + obj.powerfile.persistence.setAutocompactionInterval(36000);
221 + obj.powerfile.ensureIndex({ fieldName: 'nodeid' });
222 + obj.powerfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: 60 * 60 * 24 * 10 }); // Limit the power event log to 10 days (Seconds * Minutes * Hours * Days)
223 +
224 + // Setup the SMBIOS collection
225 + obj.smbiosfile = new Datastore({ filename: obj.parent.getConfigFilePath('meshcentral-smbios.db'), autoload: true });
226 +
227 + // Setup the server stats collection and setup indexes
228 + obj.serverstatsfile = new Datastore({ filename: obj.parent.getConfigFilePath('meshcentral-stats.db'), autoload: true });
229 + obj.serverstatsfile.persistence.setAutocompactionInterval(36000);
230 + obj.serverstatsfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: 60 * 60 * 24 * 30 }); // Limit the server stats log to 30 days (Seconds * Minutes * Hours * Days)
231 + obj.serverstatsfile.ensureIndex({ fieldName: 'expire', expireAfterSeconds: 0 }); // Auto-expire events
232 +
233 + func(); // Completed NeDB setup
234 + }
235 +
236 + obj.SetupDatabase = function (func) {
237 + // Check if the database unique identifier is present
238 + // This is used to check that in server peering mode, everyone is using the same database.
239 + obj.Get('DatabaseIdentifier', function (err, docs) {
240 + if ((docs.length == 1) && (docs[0].value != null)) {
241 + obj.identifier = docs[0].value;
242 + } else {
243 + obj.identifier = Buffer.from(require('crypto').randomBytes(48), 'binary').toString('hex');
244 + obj.Set({ _id: 'DatabaseIdentifier', value: obj.identifier });
245 + }
246 + });
247 +
248 + // Load database schema version and check if we need to update
249 + obj.Get('SchemaVersion', function (err, docs) {
250 + var ver = 0;
251 + if (docs && docs.length == 1) { ver = docs[0].value; }
252 + if (ver == 1) { console.log('This is an unsupported beta 1 database, delete it to create a new one.'); process.exit(0); }
253 +
254 + // TODO: Any schema upgrades here...
255 + obj.Set({ _id: 'SchemaVersion', value: 2 });
256 +
257 + func(ver);
258 + });
259 + };
260 +
261 + obj.cleanup = function (func) {
262 + // TODO: Remove all mesh links to invalid users
263 + // TODO: Remove all meshes that dont have any links
264 +
265 + // Remove all events, power events and SMBIOS data from the main collection. They are all in seperate collections now.
266 + obj.file.remove({ type: 'event' }, { multi: true });
267 + obj.file.remove({ type: 'power' }, { multi: true });
268 + obj.file.remove({ type: 'smbios' }, { multi: true });
269 +
270 + // Remove all objects that have a "meshid" that no longer points to a valid mesh.
271 + obj.GetAllType('mesh', function (err, docs) {
272 + var meshlist = [];
273 + if ((err == null) && (docs.length > 0)) { for (var i in docs) { meshlist.push(docs[i]._id); } }
274 + obj.file.remove({ meshid: { $exists: true, $nin: meshlist } }, { multi: true });
275 +
276 + // Fix all of the creating & login to ticks by seconds, not milliseconds.
277 + obj.GetAllType('user', function (err, docs) {
278 + if (err == null && docs.length > 0) {
279 + for (var i in docs) {
280 + var fixed = false;
281 +
282 + // Fix account creation
283 + if (docs[i].creation) {
284 + if (docs[i].creation > 1300000000000) { docs[i].creation = Math.floor(docs[i].creation / 1000); fixed = true; }
285 + if ((docs[i].creation % 1) != 0) { docs[i].creation = Math.floor(docs[i].creation); fixed = true; }
286 + }
287 +
288 + // Fix last account login
289 + if (docs[i].login) {
290 + if (docs[i].login > 1300000000000) { docs[i].login = Math.floor(docs[i].login / 1000); fixed = true; }
291 + if ((docs[i].login % 1) != 0) { docs[i].login = Math.floor(docs[i].login); fixed = true; }
292 + }
293 +
294 + // Fix last password change
295 + if (docs[i].passchange) {
296 + if (docs[i].passchange > 1300000000000) { docs[i].passchange = Math.floor(docs[i].passchange / 1000); fixed = true; }
297 + if ((docs[i].passchange % 1) != 0) { docs[i].passchange = Math.floor(docs[i].passchange); fixed = true; }
298 + }
299 +
300 + // Fix subscriptions
301 + if (docs[i].subscriptions != null) { delete docs[i].subscriptions; fixed = true; }
302 +
303 + // Save the user if needed
304 + if (fixed) { obj.Set(docs[i]); }
305 +
306 + // We are done
307 + if (func) { func(); }
308 + }
309 + }
310 + });
311 + });
312 + };
313 +
314 + // Database actions on the main collection
315 + obj.Set = function (data, func) { obj.file.update({ _id: data._id }, data, { upsert: true }, func); };
316 + obj.Get = function (id, func)
317 + {
318 + if (arguments.length > 2)
319 + {
320 + var parms = [func];
321 + for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
322 + var func2 = function _func2(arg1, arg2)
323 + {
324 + var userCallback = _func2.userArgs.shift();
325 + _func2.userArgs.unshift(arg2);
326 + _func2.userArgs.unshift(arg1);
327 + userCallback.apply(obj, _func2.userArgs);
328 + };
329 + func2.userArgs = parms;
330 + obj.file.find({ _id: id }, func2);
331 + }
332 + else
333 + {
334 + obj.file.find({ _id: id }, func);
335 + }
336 + };
337 + obj.GetAll = function (func) { obj.file.find({}, func); };
338 + obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }, { type: 0 }, func); };
339 + obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, domain, type, id, func) { var x = { type: type, domain: domain, meshid: { $in: meshes } }; if (id) { x._id = id; } obj.file.find(x, { type: 0 }, func); };
340 + //obj.GetAllType = function (type, func) { obj.file.find({ type: type }, func); };
341 +
342 + obj.GetAllType = function (type, func) { obj.file.find({ type: type }, function (err, cursor) { if (err) { func(err); } else { var r = []; cursor.each(function (err, item) { if (err) { func(err); } else { if (item) { r.push(item); } else { func(null, r); } } }); } }); };
343 +
344 +
345 +
346 + obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.find({ type: type, domain: domain, _id: { $in: ids } }, func); };
347 + obj.GetUserWithEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email }, { type: 0 }, func); };
348 + obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email, emailVerified: true }, { type: 0 }, func); };
349 + obj.Remove = function (id) { obj.file.remove({ _id: id }); };
350 + obj.RemoveAll = function (func) { obj.file.remove({}, { multi: true }, func); };
351 + obj.RemoveAllOfType = function (type, func) { obj.file.remove({ type: type }, { multi: true }, func); };
352 + obj.InsertMany = function (data, func) { obj.file.insert(data, func); };
353 + obj.RemoveMeshDocuments = function (id) { obj.file.remove({ meshid: id }, { multi: true }); obj.file.remove({ _id: 'nt' + id }); };
354 + obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if (docs.length == 1) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
355 + obj.DeleteDomain = function (domain, func) { obj.file.remove({ domain: domain }, { multi: true }, func); };
356 + obj.SetUser = function (user) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); };
357 + obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
358 + obj.getLocalAmtNodes = function (func) { obj.file.find({ type: 'node', host: { $exists: true, $ne: null }, intelamt: { $exists: true } }, func); };
359 + obj.getAmtUuidNode = function (meshid, uuid, func) { obj.file.find({ type: 'node', meshid: meshid, 'intelamt.uuid': uuid }, func); };
360 + 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)); }); } }
361 +
362 + // Database actions on the events collection
363 + obj.GetAllEvents = function (func) { obj.eventsfile.find({}, func); };
364 + obj.StoreEvent = function (event) { obj.eventsfile.insert(event); };
365 + obj.GetEvents = function (ids, domain, func) { if (obj.databaseType == 1) { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func); } else { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func); } };
366 + obj.GetEventsWithLimit = function (ids, domain, limit, func) { if (obj.databaseType == 1) { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func); } else { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func); } };
367 + obj.GetUserEvents = function (ids, domain, username, func) {
368 + if (obj.databaseType == 1) {
369 + obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { username: username }] }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func);
370 + } else {
371 + obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { username: username }] }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func);
372 + }
373 + };
374 + obj.GetUserEventsWithLimit = function (ids, domain, username, limit, func) {
375 + if (obj.databaseType == 1) {
376 + obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { username: username }] }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func);
377 + } else {
378 + obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { username: username }] }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func);
379 + }
380 + };
381 + obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, func) { if (obj.databaseType == 1) { obj.eventsfile.find({ domain: domain, nodeid: nodeid }, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).exec(func); } else { obj.eventsfile.find({ domain: domain, nodeid: nodeid }, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit, func); } };
382 + obj.RemoveAllEvents = function (domain) { obj.eventsfile.remove({ domain: domain }, { multi: true }); };
383 + obj.RemoveAllNodeEvents = function (domain, nodeid) { obj.eventsfile.remove({ domain: domain, nodeid: nodeid }, { multi: true }); };
384 +
385 + // Database actions on the power collection
386 + obj.getAllPower = function (func) { obj.powerfile.find({}, func); };
387 + obj.storePowerEvent = function (event, multiServer, func) { if (multiServer != null) { event.server = multiServer.serverid; } obj.powerfile.insert(event, func); };
388 + obj.getPowerTimeline = function (nodeid, func) { if (obj.databaseType == 1) { 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); } };
389 + obj.removeAllPowerEvents = function () { obj.powerfile.remove({}, { multi: true }); };
390 + obj.removeAllPowerEventsForNode = function (nodeid) { obj.powerfile.remove({ nodeid: nodeid }, { multi: true }); };
391 +
392 + // Database actions on the SMBIOS collection
393 + obj.SetSMBIOS = function (smbios, func) { obj.smbiosfile.update({ _id: smbios._id }, smbios, { upsert: true }, func); };
394 + obj.RemoveSMBIOS = function (id) { obj.smbiosfile.remove({ _id: id }); };
395 + obj.GetSMBIOS = function (id, func) { obj.smbiosfile.find({ _id: id }, func); };
396 +
397 + // Database actions on the Server Stats collection
398 + obj.SetServerStats = function (data, func) { obj.serverstatsfile.insert(data, func); };
399 + 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); };
400 +
401 + // Read a configuration file from the database
402 + obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
403 +
404 + // Write a configuration file to the database
405 + obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
406 +
407 + // List all configuration files
408 + obj.listConfigFiles = function (func) { obj.file.find({ type: 'cfile' }).sort({ _id: 1 }).exec(func); }
409 +
410 + // Get all configuration files
411 + obj.getAllConfigFiles = function (password, func) {
412 + obj.file.find({ type: 'cfile' }, function (err, docs) {
413 + if (err != null) { func(null); return; }
414 + var r = null;
415 + for (var i = 0; i < docs.length; i++) {
416 + var name = docs[i]._id.split('/')[1];
417 + var data = obj.decryptData(password, docs[i].data);
418 + if (data != null) { if (r == null) { r = {}; } r[name] = data; }
419 + }
420 + func(r);
421 + });
422 + }
423 +
424 + // Get encryption key
425 + obj.getEncryptDataKey = function (password) {
426 + if (typeof password != 'string') return null;
427 + return obj.parent.crypto.createHash('sha384').update(password).digest("raw").slice(0, 32);
428 + }
429 +
430 + // Encrypt data
431 + obj.encryptData = function (password, plaintext) {
432 + var key = obj.getEncryptDataKey(password);
433 + if (key == null) return null;
434 + const iv = obj.parent.crypto.randomBytes(16);
435 + const aes = obj.parent.crypto.createCipheriv('aes-256-cbc', key, iv);
436 + var ciphertext = aes.update(plaintext);
437 + ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
438 + return ciphertext.toString('base64');
439 + }
440 +
441 + // Decrypt data
442 + obj.decryptData = function (password, ciphertext) {
443 + try {
444 + var key = obj.getEncryptDataKey(password);
445 + if (key == null) return null;
446 + const ciphertextBytes = Buffer.from(ciphertext, 'base64');
447 + const iv = ciphertextBytes.slice(0, 16);
448 + const data = ciphertextBytes.slice(16);
449 + const aes = obj.parent.crypto.createDecipheriv('aes-256-cbc', key, iv);
450 + var plaintextBytes = Buffer.from(aes.update(data));
451 + plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
452 + return plaintextBytes;
453 + } catch (ex) { return null; }
454 + }
455 +
456 + // Get the number of records in the database for various types, this is the slow NeDB way.
457 + // 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.
458 + obj.getStats = function (func) {
459 + if (obj.databaseType == 2) {
460 + // MongoDB version
461 + obj.file.aggregate([{ "$group": { _id: "$type", count: { $sum: 1 } } }], function (err, docs) {
462 + var counters = {}, totalCount = 0;
463 + for (var i in docs) { if (docs[i]._id != null) { counters[docs[i]._id] = docs[i].count; totalCount += docs[i].count; } }
464 + func({ nodes: counters['node'], meshes: counters['mesh'], users: counters['user'], total: totalCount });
465 + })
466 + } else {
467 + // NeDB version
468 + obj.file.count({ type: 'node' }, function (err, nodeCount) {
469 + obj.file.count({ type: 'mesh' }, function (err, meshCount) {
470 + obj.file.count({ type: 'user' }, function (err, userCount) {
471 + obj.file.count({}, function (err, totalCount) {
472 + func({ nodes: nodeCount, meshes: meshCount, users: userCount, total: totalCount });
473 + });
474 + });
475 + });
476 + });
477 + }
478 + }
479 +
480 + // 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.
481 + obj.getValueOfTheDay = function (id, startValue, func) { obj.Get(id, function (err, docs) { var date = new Date(), t = date.toLocaleDateString(); if (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 }); }); };
482 + obj.escapeBase64 = function escapeBase64(val) { return (val.replace(/\+/g, '@').replace(/\//g, '$')); }
483 +
484 + function Clone(v) { return JSON.parse(JSON.stringify(v)); }
485 +
486 + return obj;
487 +};
\ No newline at end of file
db.js
+470 -246
@@ -25,7 +25,7 @@
25 // Just run with --mongodb [connectionstring], where the connection string is documented here: https://docs.mongodb.com/manual/reference/connection-string/
26 // The default collection is "meshcentral", but you can override it using --mongodbcol [collection]
27 //
28 -module.exports.CreateDB = function (parent) {
28 +module.exports.CreateDB = function (parent, func) {
29 var obj = {};
30 var Datastore = null;
31 var expireEventsSeconds = (60 * 60 * 24 * 20); // By default, expire events after 20 days. (Seconds * Minutes * Hours * Days)
@@ -36,6 +36,347 @@ module.exports.CreateDB = function (parent) {
36 obj.identifier = null;
37 obj.dbKey = null;
38
39 + obj.SetupDatabase = function (func) {
40 + // Check if the database unique identifier is present
41 + // This is used to check that in server peering mode, everyone is using the same database.
42 + obj.Get('DatabaseIdentifier', function (err, docs) {
43 + if ((docs.length == 1) && (docs[0].value != null)) {
44 + obj.identifier = docs[0].value;
45 + } else {
46 + obj.identifier = Buffer.from(require('crypto').randomBytes(48), 'binary').toString('hex');
47 + obj.Set({ _id: 'DatabaseIdentifier', value: obj.identifier });
48 + }
49 + });
50 +
51 + // Load database schema version and check if we need to update
52 + obj.Get('SchemaVersion', function (err, docs) {
53 + var ver = 0;
54 + if (docs && docs.length == 1) { ver = docs[0].value; }
55 + if (ver == 1) { console.log('This is an unsupported beta 1 database, delete it to create a new one.'); process.exit(0); }
56 +
57 + // TODO: Any schema upgrades here...
58 + obj.Set({ _id: 'SchemaVersion', value: 2 });
59 +
60 + func(ver);
61 + });
62 + };
63 +
64 + obj.cleanup = function (func) {
65 + // TODO: Remove all mesh links to invalid users
66 + // TODO: Remove all meshes that dont have any links
67 +
68 + // Remove all events, power events and SMBIOS data from the main collection. They are all in seperate collections now.
69 + obj.file.remove({ type: 'event' }, { multi: true });
70 + obj.file.remove({ type: 'power' }, { multi: true });
71 + obj.file.remove({ type: 'smbios' }, { multi: true });
72 +
73 + // Remove all objects that have a "meshid" that no longer points to a valid mesh.
74 + obj.GetAllType('mesh', function (err, docs) {
75 + var meshlist = [];
76 + if ((err == null) && (docs.length > 0)) { for (var i in docs) { meshlist.push(docs[i]._id); } }
77 + obj.file.remove({ meshid: { $exists: true, $nin: meshlist } }, { multi: true });
78 +
79 + // Fix all of the creating & login to ticks by seconds, not milliseconds.
80 + obj.GetAllType('user', function (err, docs) {
81 + if (err == null && docs.length > 0) {
82 + for (var i in docs) {
83 + var fixed = false;
84 +
85 + // Fix account creation
86 + if (docs[i].creation) {
87 + if (docs[i].creation > 1300000000000) { docs[i].creation = Math.floor(docs[i].creation / 1000); fixed = true; }
88 + if ((docs[i].creation % 1) != 0) { docs[i].creation = Math.floor(docs[i].creation); fixed = true; }
89 + }
90 +
91 + // Fix last account login
92 + if (docs[i].login) {
93 + if (docs[i].login > 1300000000000) { docs[i].login = Math.floor(docs[i].login / 1000); fixed = true; }
94 + if ((docs[i].login % 1) != 0) { docs[i].login = Math.floor(docs[i].login); fixed = true; }
95 + }
96 +
97 + // Fix last password change
98 + if (docs[i].passchange) {
99 + if (docs[i].passchange > 1300000000000) { docs[i].passchange = Math.floor(docs[i].passchange / 1000); fixed = true; }
100 + if ((docs[i].passchange % 1) != 0) { docs[i].passchange = Math.floor(docs[i].passchange); fixed = true; }
101 + }
102 +
103 + // Fix subscriptions
104 + if (docs[i].subscriptions != null) { delete docs[i].subscriptions; fixed = true; }
105 +
106 + // Save the user if needed
107 + if (fixed) { obj.Set(docs[i]); }
108 +
109 + // We are done
110 + if (func) { func(); }
111 + }
112 + }
113 + });
114 + });
115 + };
116 +
117 + if (obj.databaseType == 3) {
118 + // Database actions on the main collection (MongoDB)
119 + function xfind(collection, query, projection, func) {
120 + if (projection) {
121 + collection.find(query, projection, function (err, cursor) { if (err) { func(err); } else { var r = []; cursor.each(function (err, item) { if (err) { func(err); } else { if (item) { r.push(item); } else { func(null, r); } } }); } });
122 + } else {
123 + collection.find(query, function (err, cursor) { if (err) { func(err); } else { var r = []; cursor.each(function (err, item) { if (err) { func(err); } else { if (item) { r.push(item); } else { func(null, r); } } }); } });
124 + }
125 + };
126 +
127 + obj.Set = function (data, func) { obj.file.update({ _id: data._id }, data, { upsert: true }, func); };
128 + obj.Get = function (id, func) {
129 + if (arguments.length > 2) {
130 + var parms = [func];
131 + for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
132 + var func2 = function _func2(arg1, arg2) {
133 + var userCallback = _func2.userArgs.shift();
134 + _func2.userArgs.unshift(arg2);
135 + _func2.userArgs.unshift(arg1);
136 + userCallback.apply(obj, _func2.userArgs);
137 + };
138 + func2.userArgs = parms;
139 + xfind(obj.file, { _id: id }, null, func2);
140 + } else {
141 + xfind(obj.file, { _id: id }, null, func);
142 + }
143 + };
144 + obj.GetAll = function (func) { xfind(obj.file, {}, null, func); };
145 + obj.GetAllTypeNoTypeField = function (type, domain, func) { xfind(obj.file, { type: type, domain: domain }, { type: 0 }, func); };
146 + obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, domain, type, id, func) { var x = { type: type, domain: domain, meshid: { $in: meshes } }; if (id) { x._id = id; } xfind(obj.file, x, { type: 0 }, func); };
147 + obj.GetAllType = function (type, func) { xfind(obj.file, { type: type }, null, func); };
148 + obj.GetAllIdsOfType = function (ids, domain, type, func) { xfind(obj.file, { type: type, domain: domain, _id: { $in: ids } }, func); };
149 + obj.GetUserWithEmail = function (domain, email, func) { xfind(obj.file, { type: 'user', domain: domain, email: email }, { type: 0 }, func); };
150 + obj.GetUserWithVerifiedEmail = function (domain, email, func) { xfind(obj.file, { type: 'user', domain: domain, email: email, emailVerified: true }, { type: 0 }, func); };
151 + obj.Remove = function (id) { obj.file.remove({ _id: id }); };
152 + obj.RemoveAll = function (func) { obj.file.remove({}, { multi: true }, func); };
153 + obj.RemoveAllOfType = function (type, func) { obj.file.remove({ type: type }, { multi: true }, func); };
154 + obj.InsertMany = function (data, func) { obj.file.insert(data, func); };
155 + obj.RemoveMeshDocuments = function (id) { obj.file.remove({ meshid: id }, { multi: true }); obj.file.remove({ _id: 'nt' + id }); };
156 + obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if (docs.length == 1) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
157 + obj.DeleteDomain = function (domain, func) { obj.file.remove({ domain: domain }, { multi: true }, func); };
158 + obj.SetUser = function (user) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); };
159 + obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
160 + obj.getLocalAmtNodes = function (func) { xfind(obj.file, { type: 'node', host: { $exists: true, $ne: null }, intelamt: { $exists: true } }, func); };
161 + obj.getAmtUuidNode = function (meshid, uuid, func) { xfind(obj.file, { type: 'node', meshid: meshid, 'intelamt.uuid': uuid }, func); };
162 + 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)); }); } }
163 +
164 + // Database actions on the events collection
165 + obj.GetAllEvents = function (func) { xfind(obj.eventsfile, {}, func); };
166 + obj.StoreEvent = function (event) { obj.eventsfile.insert(event); };
167 + obj.GetEvents = function (ids, domain, func) { if (obj.databaseType == 1) { xfind(obj.eventsfile, { domain: domain, ids: { $in: ids } }, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func); } else { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func); } };
168 + obj.GetEventsWithLimit = function (ids, domain, limit, func) { if (obj.databaseType == 1) { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func); } else { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func); } };
169 + obj.GetUserEvents = function (ids, domain, username, func) { xfind(obj.eventsfile, { domain: domain, $or: [{ ids: { $in: ids } }, { username: username }] }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func); };
170 + obj.GetUserEventsWithLimit = function (ids, domain, username, limit, func) { xfind(obj.eventsfile, { domain: domain, $or: [{ ids: { $in: ids } }, { username: username }] }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func); };
171 + obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, func) { xfind(obj.eventsfile, { domain: domain, nodeid: nodeid }, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).exec(func); };
172 + obj.RemoveAllEvents = function (domain) { obj.eventsfile.remove({ domain: domain }, { multi: true }); };
173 + obj.RemoveAllNodeEvents = function (domain, nodeid) { obj.eventsfile.remove({ domain: domain, nodeid: nodeid }, { multi: true }); };
174 +
175 + // Database actions on the power collection
176 + obj.getAllPower = function (func) { xfind(obj.powerfile, {}, func); };
177 + obj.storePowerEvent = function (event, multiServer, func) { if (multiServer != null) { event.server = multiServer.serverid; } obj.powerfile.insert(event, func); };
178 + obj.getPowerTimeline = function (nodeid, func) { xfind(obj.powerfile, { nodeid: { $in: ['*', nodeid] } }, { _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }).exec(func); };
179 + obj.removeAllPowerEvents = function () { obj.powerfile.remove({}, { multi: true }); };
180 + obj.removeAllPowerEventsForNode = function (nodeid) { obj.powerfile.remove({ nodeid: nodeid }, { multi: true }); };
181 +
182 + // Database actions on the SMBIOS collection
183 + obj.SetSMBIOS = function (smbios, func) { obj.smbiosfile.update({ _id: smbios._id }, smbios, { upsert: true }, func); };
184 + obj.RemoveSMBIOS = function (id) { obj.smbiosfile.remove({ _id: id }); };
185 + obj.GetSMBIOS = function (id, func) { xfind(obj.smbiosfile, { _id: id }, func); };
186 +
187 + // Database actions on the Server Stats collection
188 + obj.SetServerStats = function (data, func) { obj.serverstatsfile.insert(data, func); };
189 + obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); xfind(obj.serverstatsfile, { time: { $gt: t } }, { _id: 0, cpu: 0 }, func); };
190 +
191 + // Read a configuration file from the database
192 + obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
193 +
194 + // Write a configuration file to the database
195 + obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
196 +
197 + // List all configuration files
198 + obj.listConfigFiles = function (func) { xfind(obj.file, { type: 'cfile' }).sort({ _id: 1 }).exec(func); }
199 +
200 + // Get all configuration files
201 + obj.getAllConfigFiles = function (password, func) {
202 + xfind(obj.file, { type: 'cfile' }, function (err, docs) {
203 + if (err != null) { func(null); return; }
204 + var r = null;
205 + for (var i = 0; i < docs.length; i++) {
206 + var name = docs[i]._id.split('/')[1];
207 + var data = obj.decryptData(password, docs[i].data);
208 + if (data != null) { if (r == null) { r = {}; } r[name] = data; }
209 + }
210 + func(r);
211 + });
212 + }
213 + } else {
214 + // Database actions on the main collection (NeDB and MongoJS)
215 + obj.Set = function (data, func) { obj.file.update({ _id: data._id }, data, { upsert: true }, func); };
216 + obj.Get = function (id, func) {
217 + if (arguments.length > 2) {
218 + var parms = [func];
219 + for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
220 + var func2 = function _func2(arg1, arg2) {
221 + var userCallback = _func2.userArgs.shift();
222 + _func2.userArgs.unshift(arg2);
223 + _func2.userArgs.unshift(arg1);
224 + userCallback.apply(obj, _func2.userArgs);
225 + };
226 + func2.userArgs = parms;
227 + obj.file.find({ _id: id }, func2);
228 + }
229 + else {
230 + obj.file.find({ _id: id }, func);
231 + }
232 + };
233 + obj.GetAll = function (func) { obj.file.find({}, func); };
234 + obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }, { type: 0 }, func); };
235 + obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, domain, type, id, func) { var x = { type: type, domain: domain, meshid: { $in: meshes } }; if (id) { x._id = id; } obj.file.find(x, { type: 0 }, func); };
236 + obj.GetAllType = function (type, func) { obj.file.find({ type: type }, func); };
237 + obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.find({ type: type, domain: domain, _id: { $in: ids } }, func); };
238 + obj.GetUserWithEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email }, { type: 0 }, func); };
239 + obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email, emailVerified: true }, { type: 0 }, func); };
240 + obj.Remove = function (id) { obj.file.remove({ _id: id }); };
241 + obj.RemoveAll = function (func) { obj.file.remove({}, { multi: true }, func); };
242 + obj.RemoveAllOfType = function (type, func) { obj.file.remove({ type: type }, { multi: true }, func); };
243 + obj.InsertMany = function (data, func) { obj.file.insert(data, func); };
244 + obj.RemoveMeshDocuments = function (id) { obj.file.remove({ meshid: id }, { multi: true }); obj.file.remove({ _id: 'nt' + id }); };
245 + obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if (docs.length == 1) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
246 + obj.DeleteDomain = function (domain, func) { obj.file.remove({ domain: domain }, { multi: true }, func); };
247 + obj.SetUser = function (user) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); };
248 + obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
249 + obj.getLocalAmtNodes = function (func) { obj.file.find({ type: 'node', host: { $exists: true, $ne: null }, intelamt: { $exists: true } }, func); };
250 + obj.getAmtUuidNode = function (meshid, uuid, func) { obj.file.find({ type: 'node', meshid: meshid, 'intelamt.uuid': uuid }, func); };
251 + 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)); }); } }
252 +
253 + // Database actions on the events collection
254 + obj.GetAllEvents = function (func) { obj.eventsfile.find({}, func); };
255 + obj.StoreEvent = function (event) { obj.eventsfile.insert(event); };
256 + obj.GetEvents = function (ids, domain, func) { if (obj.databaseType == 1) { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func); } else { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func); } };
257 + obj.GetEventsWithLimit = function (ids, domain, limit, func) { if (obj.databaseType == 1) { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func); } else { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func); } };
258 + obj.GetUserEvents = function (ids, domain, username, func) {
259 + if (obj.databaseType == 1) {
260 + obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { username: username }] }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func);
261 + } else {
262 + obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { username: username }] }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func);
263 + }
264 + };
265 + obj.GetUserEventsWithLimit = function (ids, domain, username, limit, func) {
266 + if (obj.databaseType == 1) {
267 + obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { username: username }] }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func);
268 + } else {
269 + obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { username: username }] }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func);
270 + }
271 + };
272 + obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, func) { if (obj.databaseType == 1) { obj.eventsfile.find({ domain: domain, nodeid: nodeid }, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).exec(func); } else { obj.eventsfile.find({ domain: domain, nodeid: nodeid }, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit, func); } };
273 + obj.RemoveAllEvents = function (domain) { obj.eventsfile.remove({ domain: domain }, { multi: true }); };
274 + obj.RemoveAllNodeEvents = function (domain, nodeid) { obj.eventsfile.remove({ domain: domain, nodeid: nodeid }, { multi: true }); };
275 +
276 + // Database actions on the power collection
277 + obj.getAllPower = function (func) { obj.powerfile.find({}, func); };
278 + obj.storePowerEvent = function (event, multiServer, func) { if (multiServer != null) { event.server = multiServer.serverid; } obj.powerfile.insert(event, func); };
279 + obj.getPowerTimeline = function (nodeid, func) { if (obj.databaseType == 1) { 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); } };
280 + obj.removeAllPowerEvents = function () { obj.powerfile.remove({}, { multi: true }); };
281 + obj.removeAllPowerEventsForNode = function (nodeid) { obj.powerfile.remove({ nodeid: nodeid }, { multi: true }); };
282 +
283 + // Database actions on the SMBIOS collection
284 + obj.SetSMBIOS = function (smbios, func) { obj.smbiosfile.update({ _id: smbios._id }, smbios, { upsert: true }, func); };
285 + obj.RemoveSMBIOS = function (id) { obj.smbiosfile.remove({ _id: id }); };
286 + obj.GetSMBIOS = function (id, func) { obj.smbiosfile.find({ _id: id }, func); };
287 +
288 + // Database actions on the Server Stats collection
289 + obj.SetServerStats = function (data, func) { obj.serverstatsfile.insert(data, func); };
290 + 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); };
291 +
292 + // Read a configuration file from the database
293 + obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
294 +
295 + // Write a configuration file to the database
296 + obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
297 +
298 + // List all configuration files
299 + obj.listConfigFiles = function (func) { obj.file.find({ type: 'cfile' }).sort({ _id: 1 }).exec(func); }
300 +
301 + // Get all configuration files
302 + obj.getAllConfigFiles = function (password, func) {
303 + obj.file.find({ type: 'cfile' }, function (err, docs) {
304 + if (err != null) { func(null); return; }
305 + var r = null;
306 + for (var i = 0; i < docs.length; i++) {
307 + var name = docs[i]._id.split('/')[1];
308 + var data = obj.decryptData(password, docs[i].data);
309 + if (data != null) { if (r == null) { r = {}; } r[name] = data; }
310 + }
311 + func(r);
312 + });
313 + }
314 + }
315 +
316 +
317 + // Get encryption key
318 + obj.getEncryptDataKey = function (password) {
319 + if (typeof password != 'string') return null;
320 + return obj.parent.crypto.createHash('sha384').update(password).digest("raw").slice(0, 32);
321 + }
322 +
323 + // Encrypt data
324 + obj.encryptData = function (password, plaintext) {
325 + var key = obj.getEncryptDataKey(password);
326 + if (key == null) return null;
327 + const iv = obj.parent.crypto.randomBytes(16);
328 + const aes = obj.parent.crypto.createCipheriv('aes-256-cbc', key, iv);
329 + var ciphertext = aes.update(plaintext);
330 + ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
331 + return ciphertext.toString('base64');
332 + }
333 +
334 + // Decrypt data
335 + obj.decryptData = function (password, ciphertext) {
336 + try {
337 + var key = obj.getEncryptDataKey(password);
338 + if (key == null) return null;
339 + const ciphertextBytes = Buffer.from(ciphertext, 'base64');
340 + const iv = ciphertextBytes.slice(0, 16);
341 + const data = ciphertextBytes.slice(16);
342 + const aes = obj.parent.crypto.createDecipheriv('aes-256-cbc', key, iv);
343 + var plaintextBytes = Buffer.from(aes.update(data));
344 + plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
345 + return plaintextBytes;
346 + } catch (ex) { return null; }
347 + }
348 +
349 + // Get the number of records in the database for various types, this is the slow NeDB way.
350 + // 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.
351 + obj.getStats = function (func) {
352 + if (obj.databaseType == 2) {
353 + // MongoDB version
354 + obj.file.aggregate([{ "$group": { _id: "$type", count: { $sum: 1 } } }], function (err, docs) {
355 + var counters = {}, totalCount = 0;
356 + for (var i in docs) { if (docs[i]._id != null) { counters[docs[i]._id] = docs[i].count; totalCount += docs[i].count; } }
357 + func({ nodes: counters['node'], meshes: counters['mesh'], users: counters['user'], total: totalCount });
358 + })
359 + } else {
360 + // NeDB version
361 + obj.file.count({ type: 'node' }, function (err, nodeCount) {
362 + obj.file.count({ type: 'mesh' }, function (err, meshCount) {
363 + obj.file.count({ type: 'user' }, function (err, userCount) {
364 + obj.file.count({}, function (err, totalCount) {
365 + func({ nodes: nodeCount, meshes: meshCount, users: userCount, total: totalCount });
366 + });
367 + });
368 + });
369 + });
370 + }
371 + }
372 +
373 + // 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.
374 + obj.getValueOfTheDay = function (id, startValue, func) { obj.Get(id, function (err, docs) { var date = new Date(), t = date.toLocaleDateString(); if (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 }); }); };
375 + obj.escapeBase64 = function escapeBase64(val) { return (val.replace(/\+/g, '@').replace(/\//g, '$')); }
376 +
377 + function Clone(v) { return JSON.parse(JSON.stringify(v)); }
378 +
379 +
380 // Read expiration time from configuration file
381 if (typeof obj.parent.args.dbexpire == 'object') {
382 if (typeof obj.parent.args.dbexpire.events == 'number') { expireEventsSeconds = obj.parent.args.dbexpire.events; }
@@ -43,8 +384,132 @@ module.exports.CreateDB = function (parent) {
384 if (typeof obj.parent.args.dbexpire.statsevents == 'number') { expireServerStatsSeconds = obj.parent.args.dbexpire.statsevents; }
385 }
386
46 - if (obj.parent.args.mongodb) {
387 + if (obj.parent.args.mongo) {
388 // Use MongoDB
389 + obj.databaseType = 3;
390 + Datastore = require('mongodb').MongoClient;
391 + Datastore.connect(obj.parent.args.mongo, function (err, client) {
392 + if (err != null) { console.log("Unable to connect to database: " + err); process.exit(); return; }
393 +
394 + var dbname = 'meshcentral';
395 + if (obj.parent.args.mongodbname) { dbname = obj.parent.args.mongodbname; }
396 + const db = client.db(dbname);
397 +
398 + var dbcollection = 'meshcentral';
399 + if (obj.parent.args.mongodbcol) { dbcollection = obj.parent.args.mongodbcol; }
400 +
401 + // Setup MongoDB main collection and indexes
402 + obj.file = db.collection(dbcollection);
403 + /*
404 + obj.file.getIndexes(function (err, indexes) {
405 + // Check if we need to reset indexes
406 + var indexesByName = {}, indexCount = 0;
407 + for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
408 + if ((indexCount != 4) || (indexesByName['TypeDomainMesh1'] == null) || (indexesByName['Email1'] == null) || (indexesByName['Mesh1'] == null)) {
409 + console.log('Resetting main indexes...');
410 + obj.file.dropIndexes(function (err) {
411 + obj.file.createIndex({ type: 1, domain: 1, meshid: 1 }, { sparse: 1, name: 'TypeDomainMesh1' }); // Speeds up GetAllTypeNoTypeField() and GetAllTypeNoTypeFieldMeshFiltered()
412 + obj.file.createIndex({ email: 1 }, { sparse: 1, name: 'Email1' }); // Speeds up GetUserWithEmail() and GetUserWithVerifiedEmail()
413 + obj.file.createIndex({ meshid: 1 }, { sparse: 1, name: 'Mesh1' }); // Speeds up RemoveMesh()
414 + });
415 + }
416 + });
417 + */
418 +
419 + /*
420 + // Setup the changeStream on the MongoDB main collection
421 + obj.fileChangeStream = obj.file.watch();
422 + obj.fileChangeStream.on('change', function (next) {
423 + // Process next document
424 + console.log('change', next);
425 + });
426 + */
427 +
428 + // Setup MongoDB events collection and indexes
429 + obj.eventsfile = db.collection('events'); // Collection containing all events
430 + /*
431 + obj.eventsfile.getIndexes(function (err, indexes) {
432 + // Check if we need to reset indexes
433 + var indexesByName = {}, indexCount = 0;
434 + for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
435 + if ((indexCount != 5) || (indexesByName['Username1'] == null) || (indexesByName['DomainNodeTime1'] == null) || (indexesByName['IdsAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
436 + // Reset all indexes
437 + console.log('Resetting events indexes...');
438 + obj.eventsfile.dropIndexes(function (err) {
439 + obj.eventsfile.createIndex({ username: 1 }, { sparse: 1, name: 'Username1' });
440 + obj.eventsfile.createIndex({ domain: 1, nodeid: 1, time: -1 }, { sparse: 1, name: 'DomainNodeTime1' });
441 + obj.eventsfile.createIndex({ ids: 1, time: -1 }, { sparse: 1, name: 'IdsAndTime1' });
442 + obj.eventsfile.createIndex({ "time": 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
443 + });
444 + } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireEventsSeconds) {
445 + // Reset the timeout index
446 + console.log('Resetting events expire index...');
447 + obj.eventsfile.dropIndex("ExpireTime1", function (err) {
448 + obj.eventsfile.createIndex({ "time": 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
449 + });
450 + }
451 + });
452 + */
453 +
454 + // Setup MongoDB power events collection and indexes
455 + obj.powerfile = db.collection('power'); // Collection containing all power events
456 + /*
457 + obj.powerfile.getIndexes(function (err, indexes) {
458 + // Check if we need to reset indexes
459 + var indexesByName = {}, indexCount = 0;
460 + for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
461 + if ((indexCount != 3) || (indexesByName['NodeIdAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
462 + // Reset all indexes
463 + console.log('Resetting power events indexes...');
464 + obj.powerfile.dropIndexes(function (err) {
465 + // Create all indexes
466 + obj.powerfile.createIndex({ nodeid: 1, time: 1 }, { sparse: 1, name: 'NodeIdAndTime1' });
467 + obj.powerfile.createIndex({ "time": 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
468 + });
469 + } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expirePowerEventsSeconds) {
470 + // Reset the timeout index
471 + console.log('Resetting power events expire index...');
472 + obj.powerfile.dropIndex("ExpireTime1", function (err) {
473 + // Reset the expire power events index
474 + obj.powerfile.createIndex({ "time": 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
475 + });
476 + }
477 + });
478 + */
479 +
480 + // Setup MongoDB smbios collection, no indexes needed
481 + obj.smbiosfile = db.collection('smbios'); // Collection containing all smbios information
482 +
483 + // Setup MongoDB server stats collection
484 + obj.serverstatsfile = db.collection('serverstats'); // Collection of server stats
485 + /*
486 + obj.serverstatsfile.getIndexes(function (err, indexes) {
487 + // Check if we need to reset indexes
488 + var indexesByName = {}, indexCount = 0;
489 + for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
490 + if ((indexCount != 3) || (indexesByName['ExpireTime1'] == null)) {
491 + // Reset all indexes
492 + console.log('Resetting server stats indexes...');
493 + obj.serverstatsfile.dropIndexes(function (err) {
494 + // Create all indexes
495 + obj.serverstatsfile.createIndex({ "time": 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
496 + obj.serverstatsfile.createIndex({ "expire": 1 }, { expireAfterSeconds: 0, name: 'ExpireTime2' }); // Auto-expire events
497 + });
498 + } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireServerStatsSeconds) {
499 + // Reset the timeout index
500 + console.log('Resetting server stats expire index...');
501 + obj.serverstatsfile.dropIndex("ExpireTime1", function (err) {
502 + // Reset the expire server stats index
503 + obj.serverstatsfile.createIndex({ "time": 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
504 + });
505 + }
506 + });
507 + */
508 +
509 + func(obj); // Completed setup of MongoDB
510 + });
511 + } else if (obj.parent.args.mongodb) {
512 + // Use MongoJS
513 obj.databaseType = 2;
514 Datastore = require('mongojs');
515 var db = Datastore(obj.parent.args.mongodb);
@@ -141,6 +606,8 @@ module.exports.CreateDB = function (parent) {
606 });
607 }
608 });
609 +
610 + func(obj); // Completed setup of MongoJS
611 } else {
612 // Use NeDB (The default)
613 obj.databaseType = 1;
@@ -199,252 +666,9 @@ module.exports.CreateDB = function (parent) {
666 obj.serverstatsfile.persistence.setAutocompactionInterval(36000);
667 obj.serverstatsfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: 60 * 60 * 24 * 30 }); // Limit the server stats log to 30 days (Seconds * Minutes * Hours * Days)
668 obj.serverstatsfile.ensureIndex({ fieldName: 'expire', expireAfterSeconds: 0 }); // Auto-expire events
202 - }
203 -
204 - obj.SetupDatabase = function (func) {
205 - // Check if the database unique identifier is present
206 - // This is used to check that in server peering mode, everyone is using the same database.
207 - obj.Get('DatabaseIdentifier', function (err, docs) {
208 - if ((docs.length == 1) && (docs[0].value != null)) {
209 - obj.identifier = docs[0].value;
210 - } else {
211 - obj.identifier = Buffer.from(require('crypto').randomBytes(48), 'binary').toString('hex');
212 - obj.Set({ _id: 'DatabaseIdentifier', value: obj.identifier });
213 - }
214 - });
215 -
216 - // Load database schema version and check if we need to update
217 - obj.Get('SchemaVersion', function (err, docs) {
218 - var ver = 0;
219 - if (docs && docs.length == 1) { ver = docs[0].value; }
220 - if (ver == 1) { console.log('This is an unsupported beta 1 database, delete it to create a new one.'); process.exit(0); }
221 -
222 - // TODO: Any schema upgrades here...
223 - obj.Set({ _id: 'SchemaVersion', value: 2 });
224 -
225 - func(ver);
226 - });
227 - };
228 -
229 - obj.cleanup = function (func) {
230 - // TODO: Remove all mesh links to invalid users
231 - // TODO: Remove all meshes that dont have any links
232 -
233 - // Remove all events, power events and SMBIOS data from the main collection. They are all in seperate collections now.
234 - obj.file.remove({ type: 'event' }, { multi: true });
235 - obj.file.remove({ type: 'power' }, { multi: true });
236 - obj.file.remove({ type: 'smbios' }, { multi: true });
237 -
238 - // Remove all objects that have a "meshid" that no longer points to a valid mesh.
239 - obj.GetAllType('mesh', function (err, docs) {
240 - var meshlist = [];
241 - if ((err == null) && (docs.length > 0)) { for (var i in docs) { meshlist.push(docs[i]._id); } }
242 - obj.file.remove({ meshid: { $exists: true, $nin: meshlist } }, { multi: true });
243 -
244 - // Fix all of the creating & login to ticks by seconds, not milliseconds.
245 - obj.GetAllType('user', function (err, docs) {
246 - if (err == null && docs.length > 0) {
247 - for (var i in docs) {
248 - var fixed = false;
249 -
250 - // Fix account creation
251 - if (docs[i].creation) {
252 - if (docs[i].creation > 1300000000000) { docs[i].creation = Math.floor(docs[i].creation / 1000); fixed = true; }
253 - if ((docs[i].creation % 1) != 0) { docs[i].creation = Math.floor(docs[i].creation); fixed = true; }
254 - }
255 -
256 - // Fix last account login
257 - if (docs[i].login) {
258 - if (docs[i].login > 1300000000000) { docs[i].login = Math.floor(docs[i].login / 1000); fixed = true; }
259 - if ((docs[i].login % 1) != 0) { docs[i].login = Math.floor(docs[i].login); fixed = true; }
260 - }
261 -
262 - // Fix last password change
263 - if (docs[i].passchange) {
264 - if (docs[i].passchange > 1300000000000) { docs[i].passchange = Math.floor(docs[i].passchange / 1000); fixed = true; }
265 - if ((docs[i].passchange % 1) != 0) { docs[i].passchange = Math.floor(docs[i].passchange); fixed = true; }
266 - }
267 -
268 - // Fix subscriptions
269 - if (docs[i].subscriptions != null) { delete docs[i].subscriptions; fixed = true; }
270 -
271 - // Save the user if needed
272 - if (fixed) { obj.Set(docs[i]); }
273 -
274 - // We are done
275 - if (func) { func(); }
276 - }
277 - }
278 - });
279 - });
280 - };
281 -
282 - // Database actions on the main collection
283 - obj.Set = function (data, func) { obj.file.update({ _id: data._id }, data, { upsert: true }, func); };
284 - obj.Get = function (id, func)
285 - {
286 - if (arguments.length > 2)
287 - {
288 - var parms = [func];
289 - for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
290 - var func2 = function _func2(arg1, arg2)
291 - {
292 - var userCallback = _func2.userArgs.shift();
293 - _func2.userArgs.unshift(arg2);
294 - _func2.userArgs.unshift(arg1);
295 - userCallback.apply(obj, _func2.userArgs);
296 - };
297 - func2.userArgs = parms;
298 - obj.file.find({ _id: id }, func2);
299 - }
300 - else
301 - {
302 - obj.file.find({ _id: id }, func);
303 - }
304 - };
305 - obj.GetAll = function (func) { obj.file.find({}, func); };
306 - obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }, { type: 0 }, func); };
307 - obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, domain, type, id, func) { var x = { type: type, domain: domain, meshid: { $in: meshes } }; if (id) { x._id = id; } obj.file.find(x, { type: 0 }, func); };
308 - obj.GetAllType = function (type, func) { obj.file.find({ type: type }, func); };
309 - obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.find({ type: type, domain: domain, _id: { $in: ids } }, func); };
310 - obj.GetUserWithEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email }, { type: 0 }, func); };
311 - obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email, emailVerified: true }, { type: 0 }, func); };
312 - obj.Remove = function (id) { obj.file.remove({ _id: id }); };
313 - obj.RemoveAll = function (func) { obj.file.remove({}, { multi: true }, func); };
314 - obj.RemoveAllOfType = function (type, func) { obj.file.remove({ type: type }, { multi: true }, func); };
315 - obj.InsertMany = function (data, func) { obj.file.insert(data, func); };
316 - obj.RemoveMeshDocuments = function (id) { obj.file.remove({ meshid: id }, { multi: true }); obj.file.remove({ _id: 'nt' + id }); };
317 - obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if (docs.length == 1) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
318 - obj.DeleteDomain = function (domain, func) { obj.file.remove({ domain: domain }, { multi: true }, func); };
319 - obj.SetUser = function (user) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); };
320 - obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
321 - obj.getLocalAmtNodes = function (func) { obj.file.find({ type: 'node', host: { $exists: true, $ne: null }, intelamt: { $exists: true } }, func); };
322 - obj.getAmtUuidNode = function (meshid, uuid, func) { obj.file.find({ type: 'node', meshid: meshid, 'intelamt.uuid': uuid }, func); };
323 - 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)); }); } }
324 -
325 - // Database actions on the events collection
326 - obj.GetAllEvents = function (func) { obj.eventsfile.find({}, func); };
327 - obj.StoreEvent = function (event) { obj.eventsfile.insert(event); };
328 - obj.GetEvents = function (ids, domain, func) { if (obj.databaseType == 1) { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func); } else { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func); } };
329 - obj.GetEventsWithLimit = function (ids, domain, limit, func) { if (obj.databaseType == 1) { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func); } else { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func); } };
330 - obj.GetUserEvents = function (ids, domain, username, func) {
331 - if (obj.databaseType == 1) {
332 - obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { username: username }] }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func);
333 - } else {
334 - obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { username: username }] }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func);
335 - }
336 - };
337 - obj.GetUserEventsWithLimit = function (ids, domain, username, limit, func) {
338 - if (obj.databaseType == 1) {
339 - obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { username: username }] }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func);
340 - } else {
341 - obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { username: username }] }, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func);
342 - }
343 - };
344 - obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, func) { if (obj.databaseType == 1) { obj.eventsfile.find({ domain: domain, nodeid: nodeid }, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).exec(func); } else { obj.eventsfile.find({ domain: domain, nodeid: nodeid }, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit, func); } };
345 - obj.RemoveAllEvents = function (domain) { obj.eventsfile.remove({ domain: domain }, { multi: true }); };
346 - obj.RemoveAllNodeEvents = function (domain, nodeid) { obj.eventsfile.remove({ domain: domain, nodeid: nodeid }, { multi: true }); };
347 -
348 - // Database actions on the power collection
349 - obj.getAllPower = function (func) { obj.powerfile.find({}, func); };
350 - obj.storePowerEvent = function (event, multiServer, func) { if (multiServer != null) { event.server = multiServer.serverid; } obj.powerfile.insert(event, func); };
351 - obj.getPowerTimeline = function (nodeid, func) { if (obj.databaseType == 1) { 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); } };
352 - obj.removeAllPowerEvents = function () { obj.powerfile.remove({}, { multi: true }); };
353 - obj.removeAllPowerEventsForNode = function (nodeid) { obj.powerfile.remove({ nodeid: nodeid }, { multi: true }); };
354 -
355 - // Database actions on the SMBIOS collection
356 - obj.SetSMBIOS = function (smbios, func) { obj.smbiosfile.update({ _id: smbios._id }, smbios, { upsert: true }, func); };
357 - obj.RemoveSMBIOS = function (id) { obj.smbiosfile.remove({ _id: id }); };
358 - obj.GetSMBIOS = function (id, func) { obj.smbiosfile.find({ _id: id }, func); };
359 -
360 - // Database actions on the Server Stats collection
361 - obj.SetServerStats = function (data, func) { obj.serverstatsfile.insert(data, func); };
362 - 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); };
363 -
364 - // Read a configuration file from the database
365 - obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
366 -
367 - // Write a configuration file to the database
368 - obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
369 -
370 - // List all configuration files
371 - obj.listConfigFiles = function (func) { obj.file.find({ type: 'cfile' }).sort({ _id: 1 }).exec(func); }
372 -
373 - // Get all configuration files
374 - obj.getAllConfigFiles = function (password, func) {
375 - obj.file.find({ type: 'cfile' }, function (err, docs) {
376 - if (err != null) { func(null); return; }
377 - var r = null;
378 - for (var i = 0; i < docs.length; i++) {
379 - var name = docs[i]._id.split('/')[1];
380 - var data = obj.decryptData(password, docs[i].data);
381 - if (data != null) { if (r == null) { r = {}; } r[name] = data; }
382 - }
383 - func(r);
384 - });
385 - }
669
387 - // Get encryption key
388 - obj.getEncryptDataKey = function (password) {
389 - if (typeof password != 'string') return null;
390 - return obj.parent.crypto.createHash('sha384').update(password).digest("raw").slice(0, 32);
670 + func(obj); // Completed setup of NeDB
671 }
672
393 - // Encrypt data
394 - obj.encryptData = function (password, plaintext) {
395 - var key = obj.getEncryptDataKey(password);
396 - if (key == null) return null;
397 - const iv = obj.parent.crypto.randomBytes(16);
398 - const aes = obj.parent.crypto.createCipheriv('aes-256-cbc', key, iv);
399 - var ciphertext = aes.update(plaintext);
400 - ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
401 - return ciphertext.toString('base64');
402 - }
403 -
404 - // Decrypt data
405 - obj.decryptData = function (password, ciphertext) {
406 - try {
407 - var key = obj.getEncryptDataKey(password);
408 - if (key == null) return null;
409 - const ciphertextBytes = Buffer.from(ciphertext, 'base64');
410 - const iv = ciphertextBytes.slice(0, 16);
411 - const data = ciphertextBytes.slice(16);
412 - const aes = obj.parent.crypto.createDecipheriv('aes-256-cbc', key, iv);
413 - var plaintextBytes = Buffer.from(aes.update(data));
414 - plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
415 - return plaintextBytes;
416 - } catch (ex) { return null; }
417 - }
418 -
419 - // Get the number of records in the database for various types, this is the slow NeDB way.
420 - // 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.
421 - obj.getStats = function (func) {
422 - if (obj.databaseType == 2) {
423 - // MongoDB version
424 - obj.file.aggregate([{ "$group": { _id: "$type", count: { $sum: 1 } } }], function (err, docs) {
425 - var counters = {}, totalCount = 0;
426 - for (var i in docs) { if (docs[i]._id != null) { counters[docs[i]._id] = docs[i].count; totalCount += docs[i].count; } }
427 - func({ nodes: counters['node'], meshes: counters['mesh'], users: counters['user'], total: totalCount });
428 - })
429 - } else {
430 - // NeDB version
431 - obj.file.count({ type: 'node' }, function (err, nodeCount) {
432 - obj.file.count({ type: 'mesh' }, function (err, meshCount) {
433 - obj.file.count({ type: 'user' }, function (err, userCount) {
434 - obj.file.count({}, function (err, totalCount) {
435 - func({ nodes: nodeCount, meshes: meshCount, users: userCount, total: totalCount });
436 - });
437 - });
438 - });
439 - });
440 - }
441 - }
442 -
443 - // 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.
444 - obj.getValueOfTheDay = function (id, startValue, func) { obj.Get(id, function (err, docs) { var date = new Date(), t = date.toLocaleDateString(); if (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 }); }); };
445 - obj.escapeBase64 = function escapeBase64(val) { return (val.replace(/\+/g, '@').replace(/\//g, '$')); }
446 -
447 - function Clone(v) { return JSON.parse(JSON.stringify(v)); }
448 -
673 return obj;
674 };
\ No newline at end of file
meshcentral.js
+247 -242
@@ -238,268 +238,272 @@ function CreateMeshCentralServer(config, args) {
238 if (typeof obj.args.swarmallowedip == 'string') { if (obj.args.swarmallowedip == '') { obj.args.swarmallowedip = null; } else { obj.args.swarmallowedip = obj.args.swarmallowedip.split(','); } }
239 if (typeof obj.args.debug == 'number') obj.debugLevel = obj.args.debug;
240 if (obj.args.debug == true) obj.debugLevel = 1;
241 - obj.db = require('./db.js').CreateDB(obj);
242 - obj.db.SetupDatabase(function (dbversion) {
243 - // See if any database operations needs to be completed
244 - if (obj.args.deletedomain) { obj.db.DeleteDomain(obj.args.deletedomain, function () { console.log('Deleted domain ' + obj.args.deletedomain + '.'); process.exit(); }); return; }
245 - if (obj.args.deletedefaultdomain) { obj.db.DeleteDomain('', function () { console.log('Deleted default domain.'); process.exit(); }); return; }
246 - if (obj.args.showall) { obj.db.GetAll(function (err, docs) { console.log(docs); process.exit(); }); return; }
247 - if (obj.args.showusers) { obj.db.GetAllType('user', function (err, docs) { console.log(docs); process.exit(); }); return; }
248 - if (obj.args.shownodes) { obj.db.GetAllType('node', function (err, docs) { console.log(docs); process.exit(); }); return; }
249 - if (obj.args.showmeshes) { obj.db.GetAllType('mesh', function (err, docs) { console.log(docs); process.exit(); }); return; }
250 - if (obj.args.showevents) { obj.db.GetAllEvents(function (err, docs) { console.log(docs); process.exit(); }); return; }
251 - if (obj.args.showpower) { obj.db.getAllPower(function (err, docs) { console.log(docs); process.exit(); }); return; }
252 - if (obj.args.clearpower) { obj.db.removeAllPowerEvents(function () { process.exit(); }); return; }
253 - if (obj.args.showiplocations) { obj.db.GetAllType('iploc', function (err, docs) { console.log(docs); process.exit(); }); return; }
254 - if (obj.args.logintoken) { obj.getLoginToken(obj.args.logintoken, function (r) { console.log(r); process.exit(); }); return; }
255 - if (obj.args.logintokenkey) { obj.showLoginTokenKey(function (r) { console.log(r); process.exit(); }); return; }
256 -
257 - // Show a list of all configuration files in the database
258 - if (obj.args.dblistconfigfiles) {
259 - obj.db.GetAllType('cfile', function (err, docs) { if (err == null) { if (docs.length == 0) { console.log('No files found.'); } else { for (var i in docs) { console.log(docs[i]._id.split('/')[1] + ', ' + Buffer.from(docs[i].data, 'base64').length + ' bytes.'); } } } else { console.log('Unable to read from database.'); } process.exit(); }); return;
260 - }
241 + require('./db.js').CreateDB(obj,
242 + function (db) {
243 + obj.db = db;
244 + obj.db.SetupDatabase(function (dbversion) {
245 + // See if any database operations needs to be completed
246 + if (obj.args.deletedomain) { obj.db.DeleteDomain(obj.args.deletedomain, function () { console.log('Deleted domain ' + obj.args.deletedomain + '.'); process.exit(); }); return; }
247 + if (obj.args.deletedefaultdomain) { obj.db.DeleteDomain('', function () { console.log('Deleted default domain.'); process.exit(); }); return; }
248 + if (obj.args.showall) { obj.db.GetAll(function (err, docs) { console.log(docs); process.exit(); }); return; }
249 + if (obj.args.showusers) { obj.db.GetAllType('user', function (err, docs) { console.log(docs); process.exit(); }); return; }
250 + if (obj.args.shownodes) { obj.db.GetAllType('node', function (err, docs) { console.log(docs); process.exit(); }); return; }
251 + if (obj.args.showmeshes) { obj.db.GetAllType('mesh', function (err, docs) { console.log(docs); process.exit(); }); return; }
252 + if (obj.args.showevents) { obj.db.GetAllEvents(function (err, docs) { console.log(docs); process.exit(); }); return; }
253 + if (obj.args.showpower) { obj.db.getAllPower(function (err, docs) { console.log(docs); process.exit(); }); return; }
254 + if (obj.args.clearpower) { obj.db.removeAllPowerEvents(function () { process.exit(); }); return; }
255 + if (obj.args.showiplocations) { obj.db.GetAllType('iploc', function (err, docs) { console.log(docs); process.exit(); }); return; }
256 + if (obj.args.logintoken) { obj.getLoginToken(obj.args.logintoken, function (r) { console.log(r); process.exit(); }); return; }
257 + if (obj.args.logintokenkey) { obj.showLoginTokenKey(function (r) { console.log(r); process.exit(); }); return; }
258 +
259 + // Show a list of all configuration files in the database
260 + if (obj.args.dblistconfigfiles) {
261 + obj.db.GetAllType('cfile', function (err, docs) { if (err == null) { if (docs.length == 0) { console.log('No files found.'); } else { for (var i in docs) { console.log(docs[i]._id.split('/')[1] + ', ' + Buffer.from(docs[i].data, 'base64').length + ' bytes.'); } } } else { console.log('Unable to read from database.'); } process.exit(); }); return;
262 + }
263
262 - // Display the content of a configuration file in the database
263 - if (obj.args.dbshowconfigfile) {
264 - if (typeof obj.args.configkey != 'string') { console.log('Error, --configkey is required.'); process.exit(); return; }
265 - obj.db.getConfigFile(obj.args.dbshowconfigfile, function (err, docs) {
266 - if (err == null) {
267 - if (docs.length == 0) { console.log('File not found.'); } else {
268 - var data = obj.db.decryptData(obj.args.configkey, docs[0].data);
269 - if (data == null) { console.log('Invalid config key.'); } else { console.log(data); }
270 - }
271 - } else { console.log('Unable to read from database.'); }
272 - process.exit();
273 - }); return;
274 - }
264 + // Display the content of a configuration file in the database
265 + if (obj.args.dbshowconfigfile) {
266 + if (typeof obj.args.configkey != 'string') { console.log('Error, --configkey is required.'); process.exit(); return; }
267 + obj.db.getConfigFile(obj.args.dbshowconfigfile, function (err, docs) {
268 + if (err == null) {
269 + if (docs.length == 0) { console.log('File not found.'); } else {
270 + var data = obj.db.decryptData(obj.args.configkey, docs[0].data);
271 + if (data == null) { console.log('Invalid config key.'); } else { console.log(data); }
272 + }
273 + } else { console.log('Unable to read from database.'); }
274 + process.exit();
275 + }); return;
276 + }
277
276 - // Delete all configuration files from database
277 - if (obj.args.dbdeleteconfigfiles) {
278 - console.log('Deleting all configuration files from the database...'); obj.db.RemoveAllOfType('cfile', function () { console.log('Done.'); process.exit(); });
279 - }
278 + // Delete all configuration files from database
279 + if (obj.args.dbdeleteconfigfiles) {
280 + console.log('Deleting all configuration files from the database...'); obj.db.RemoveAllOfType('cfile', function () { console.log('Done.'); process.exit(); });
281 + }
282
281 - // Push all relevent files from meshcentral-data into the database
282 - if (obj.args.dbpushconfigfiles) {
283 - if (typeof obj.args.configkey != 'string') { console.log('Error, --configkey is required.'); process.exit(); return; }
284 - if ((obj.args.dbpushconfigfiles !== true) && (typeof obj.args.dbpushconfigfiles != 'string')) {
285 - console.log('Usage: --dbpulldatafiles (path) This will import files from folder into the database');
286 - console.log(' --dbpulldatafiles This will import files from meshcentral-data into the db.');
287 - process.exit();
288 - } else {
289 - if ((obj.args.dbpushconfigfiles == '*') || (obj.args.dbpushconfigfiles === true)) { obj.args.dbpushconfigfiles = obj.datapath; }
290 - obj.fs.readdir(obj.args.dbpushconfigfiles, function (err, files) {
291 - if (err != null) { console.log('ERROR: Unable to read from folder ' + obj.args.dbpushconfigfiles); process.exit(); return; }
292 - var configFound = false;
293 - for (var i in files) { if (files[i] == 'config.json') { configFound = true; } }
294 - if (configFound == false) { console.log('ERROR: No config.json in folder ' + obj.args.dbpushconfigfiles); process.exit(); return; }
295 - obj.db.RemoveAllOfType('cfile', function () {
283 + // Push all relevent files from meshcentral-data into the database
284 + if (obj.args.dbpushconfigfiles) {
285 + if (typeof obj.args.configkey != 'string') { console.log('Error, --configkey is required.'); process.exit(); return; }
286 + if ((obj.args.dbpushconfigfiles !== true) && (typeof obj.args.dbpushconfigfiles != 'string')) {
287 + console.log('Usage: --dbpulldatafiles (path) This will import files from folder into the database');
288 + console.log(' --dbpulldatafiles This will import files from meshcentral-data into the db.');
289 + process.exit();
290 + } else {
291 + if ((obj.args.dbpushconfigfiles == '*') || (obj.args.dbpushconfigfiles === true)) { obj.args.dbpushconfigfiles = obj.datapath; }
292 obj.fs.readdir(obj.args.dbpushconfigfiles, function (err, files) {
297 - var lockCount = 1
298 - for (var i in files) {
299 - const file = files[i];
300 - if ((file == 'config.json') || file.endsWith('.key') || file.endsWith('.crt') || (file == 'terms.txt') || file.endsWith('.jpg') || file.endsWith('.png')) {
301 - const path = obj.path.join(obj.args.dbpushconfigfiles, files[i]), binary = Buffer.from(obj.fs.readFileSync(path, { encoding: 'binary' }), 'binary');
302 - console.log('Pushing ' + file + ', ' + binary.length + ' bytes.');
303 - lockCount++;
304 - obj.db.setConfigFile(file, obj.db.encryptData(obj.args.configkey, binary), function () { if ((--lockCount) == 0) { console.log('Done.'); process.exit(); } });
305 - }
306 - }
307 - if (--lockCount == 0) { process.exit(); }
293 + if (err != null) { console.log('ERROR: Unable to read from folder ' + obj.args.dbpushconfigfiles); process.exit(); return; }
294 + var configFound = false;
295 + for (var i in files) { if (files[i] == 'config.json') { configFound = true; } }
296 + if (configFound == false) { console.log('ERROR: No config.json in folder ' + obj.args.dbpushconfigfiles); process.exit(); return; }
297 + obj.db.RemoveAllOfType('cfile', function () {
298 + obj.fs.readdir(obj.args.dbpushconfigfiles, function (err, files) {
299 + var lockCount = 1
300 + for (var i in files) {
301 + const file = files[i];
302 + if ((file == 'config.json') || file.endsWith('.key') || file.endsWith('.crt') || (file == 'terms.txt') || file.endsWith('.jpg') || file.endsWith('.png')) {
303 + const path = obj.path.join(obj.args.dbpushconfigfiles, files[i]), binary = Buffer.from(obj.fs.readFileSync(path, { encoding: 'binary' }), 'binary');
304 + console.log('Pushing ' + file + ', ' + binary.length + ' bytes.');
305 + lockCount++;
306 + obj.db.setConfigFile(file, obj.db.encryptData(obj.args.configkey, binary), function () { if ((--lockCount) == 0) { console.log('Done.'); process.exit(); } });
307 + }
308 + }
309 + if (--lockCount == 0) { process.exit(); }
310 + });
311 + });
312 });
309 - });
310 - });
311 - }
312 - return;
313 - }
313 + }
314 + return;
315 + }
316
315 - // Pull all database files into meshcentral-data
316 - if (obj.args.dbpullconfigfiles) {
317 - if (typeof obj.args.configkey != 'string') { console.log('Error, --configkey is required.'); process.exit(); return; }
318 - if (typeof obj.args.dbpullconfigfiles != 'string') {
319 - console.log('Usage: --dbpulldatafiles (path)');
320 - process.exit();
321 - } else {
322 - obj.db.GetAllType('cfile', function (err, docs) {
323 - if (err == null) {
324 - if (docs.length == 0) {
325 - console.log('File not found.');
326 - } else {
327 - for (var i in docs) {
328 - const file = docs[i]._id.split('/')[1], binary = obj.db.decryptData(obj.args.configkey, docs[i].data);
329 - if (binary == null) {
330 - console.log('Invalid config key.');
317 + // Pull all database files into meshcentral-data
318 + if (obj.args.dbpullconfigfiles) {
319 + if (typeof obj.args.configkey != 'string') { console.log('Error, --configkey is required.'); process.exit(); return; }
320 + if (typeof obj.args.dbpullconfigfiles != 'string') {
321 + console.log('Usage: --dbpulldatafiles (path)');
322 + process.exit();
323 + } else {
324 + obj.db.GetAllType('cfile', function (err, docs) {
325 + if (err == null) {
326 + if (docs.length == 0) {
327 + console.log('File not found.');
328 } else {
332 - var fullFileName = obj.path.join(obj.args.dbpullconfigfiles, file);
333 - try { obj.fs.writeFileSync(fullFileName, binary); } catch (ex) { console.log('Unable to write to ' + fullFileName); process.exit(); return; }
334 - console.log('Pulling ' + file + ', ' + binary.length + ' bytes.');
329 + for (var i in docs) {
330 + const file = docs[i]._id.split('/')[1], binary = obj.db.decryptData(obj.args.configkey, docs[i].data);
331 + if (binary == null) {
332 + console.log('Invalid config key.');
333 + } else {
334 + var fullFileName = obj.path.join(obj.args.dbpullconfigfiles, file);
335 + try { obj.fs.writeFileSync(fullFileName, binary); } catch (ex) { console.log('Unable to write to ' + fullFileName); process.exit(); return; }
336 + console.log('Pulling ' + file + ', ' + binary.length + ' bytes.');
337 + }
338 + }
339 }
340 + } else {
341 + console.log('Unable to read from database.');
342 }
337 - }
338 - } else {
339 - console.log('Unable to read from database.');
343 + process.exit();
344 + });
345 }
341 - process.exit();
342 - });
343 - }
344 - return;
345 - }
346 + return;
347 + }
348
347 - if (obj.args.dbexport) {
348 - // Export the entire database to a JSON file
349 - if (obj.args.dbexport == true) { obj.args.dbexport = obj.getConfigFilePath('meshcentral.db.json'); }
350 - obj.db.GetAll(function (err, docs) {
351 - obj.fs.writeFileSync(obj.args.dbexport, JSON.stringify(docs));
352 - console.log('Exported ' + docs.length + ' objects(s) to ' + obj.args.dbexport + '.'); process.exit();
353 - });
354 - return;
355 - }
356 - if (obj.args.dbexportmin) {
357 - // Export a minimal database to a JSON file. Export only users, meshes and nodes.
358 - // This is a useful command to look at the database.
359 - if (obj.args.dbexportmin == true) { obj.args.dbexportmin = obj.getConfigFilePath('meshcentral.db.json'); }
360 - obj.db.GetAllType({ $in: ['user', 'node', 'mesh'] }, function (err, docs) {
361 - obj.fs.writeFileSync(obj.args.dbexportmin, JSON.stringify(docs));
362 - console.log('Exported ' + docs.length + ' objects(s) to ' + obj.args.dbexportmin + '.'); process.exit();
363 - });
364 - return;
365 - }
366 - if (obj.args.dbimport) {
367 - // Import the entire database from a JSON file
368 - if (obj.args.dbimport == true) { obj.args.dbimport = obj.getConfigFilePath('meshcentral.db.json'); }
369 - var json = null, json2 = "", badCharCount = 0;
370 - try { json = obj.fs.readFileSync(obj.args.dbimport, { encoding: 'utf8' }); } catch (e) { console.log('Invalid JSON file: ' + obj.args.dbimport + ': ' + e); process.exit(); }
371 - for (i = 0; i < json.length; i++) { if (json.charCodeAt(i) >= 32) { json2 += json[i]; } else { var tt = json.charCodeAt(i); if (tt != 10 && tt != 13) { badCharCount++; } } } // Remove all bad chars
372 - if (badCharCount > 0) { console.log(badCharCount + ' invalid character(s) where removed.'); }
373 - try { json = JSON.parse(json2); } catch (e) { console.log('Invalid JSON format: ' + obj.args.dbimport + ': ' + e); process.exit(); }
374 - if ((json == null) || (typeof json.length != 'number') || (json.length < 1)) { console.log('Invalid JSON format: ' + obj.args.dbimport + '.'); }
375 - for (i in json) { if ((json[i].type == "mesh") && (json[i].links != null)) { for (var j in json[i].links) { var esc = obj.common.escapeFieldName(j); if (esc !== j) { json[i].links[esc] = json[i].links[j]; delete json[i].links[j]; } } } } // Escape MongoDB invalid field chars
376 - //for (i in json) { if ((json[i].type == "node") && (json[i].host != null)) { json[i].rname = json[i].host; delete json[i].host; } } // DEBUG: Change host to rname
377 - setTimeout(function () { // If the Mongo database is being created for the first time, there is a race condition here. This will get around it.
378 - obj.db.RemoveAll(function () {
379 - obj.db.InsertMany(json, function (err) {
380 - if (err != null) { console.log(err); } else { console.log('Imported ' + json.length + ' objects(s) from ' + obj.args.dbimport + '.'); } process.exit();
349 + if (obj.args.dbexport) {
350 + // Export the entire database to a JSON file
351 + if (obj.args.dbexport == true) { obj.args.dbexport = obj.getConfigFilePath('meshcentral.db.json'); }
352 + obj.db.GetAll(function (err, docs) {
353 + obj.fs.writeFileSync(obj.args.dbexport, JSON.stringify(docs));
354 + console.log('Exported ' + docs.length + ' objects(s) to ' + obj.args.dbexport + '.'); process.exit();
355 });
382 - });
383 - }, 100);
384 - return;
385 - }
386 - /*
387 - if (obj.args.dbimport) {
388 - // Import the entire database from a very large JSON file
389 - obj.db.RemoveAll(function () {
390 - if (obj.args.dbimport == true) { obj.args.dbimport = obj.getConfigFilePath('meshcentral.db.json'); }
391 - var json = null, json2 = "", badCharCount = 0;
392 - const StreamArray = require('stream-json/streamers/StreamArray');
393 - const jsonStream = StreamArray.withParser();
394 - jsonStream.on('data', function (data) { obj.db.Set(data.value); });
395 - jsonStream.on('end', () => { console.log('Done.'); process.exit(); });
396 - obj.fs.createReadStream(obj.args.dbimport).pipe(jsonStream.input);
397 - });
398 - return;
399 - }
400 - */
401 - if (obj.args.dbmerge) {
402 - // Import the entire database from a JSON file
403 - if (obj.args.dbmerge == true) { obj.args.dbmerge = obj.getConfigFilePath('meshcentral.db.json'); }
404 - var json = null, json2 = "", badCharCount = 0;
405 - try { json = obj.fs.readFileSync(obj.args.dbmerge, { encoding: 'utf8' }); } catch (e) { console.log('Invalid JSON file: ' + obj.args.dbmerge + ': ' + e); process.exit(); }
406 - for (i = 0; i < json.length; i++) { if (json.charCodeAt(i) >= 32) { json2 += json[i]; } else { var tt = json.charCodeAt(i); if (tt != 10 && tt != 13) { badCharCount++; } } } // Remove all bad chars
407 - if (badCharCount > 0) { console.log(badCharCount + ' invalid character(s) where removed.'); }
408 - try { json = JSON.parse(json2); } catch (e) { console.log('Invalid JSON format: ' + obj.args.dbmerge + ': ' + e); process.exit(); }
409 - if ((json == null) || (typeof json.length != 'number') || (json.length < 1)) { console.log('Invalid JSON format: ' + obj.args.dbimport + '.'); }
410 -
411 - // Get all users from current database
412 - obj.db.GetAllType('user', function (err, docs) {
413 - var users = {}, usersCount = 0;
414 - for (var i in docs) { users[docs[i]._id] = docs[i]; usersCount++; }
415 -
416 - // Fetch all meshes from the database
417 - obj.db.GetAllType('mesh', function (err, docs) {
418 - obj.common.unEscapeAllLinksFieldName(docs);
419 - var meshes = {}, meshesCount = 0;
420 - for (var i in docs) { meshes[docs[i]._id] = docs[i]; meshesCount++; }
421 - console.log('Loaded ' + usersCount + ' users and ' + meshesCount + ' meshes.');
422 - // Look at each object in the import file
423 - var objectToAdd = [];
424 - for (var i in json) {
425 - var newobj = json[i];
426 - if (newobj.type == 'user') {
427 - // Check if the user already exists
428 - var existingUser = users[newobj._id];
429 - if (existingUser) {
430 - // Merge the links
431 - if (typeof newobj.links == 'object') {
432 - for (var j in newobj.links) {
433 - if ((existingUser.links == null) || (existingUser.links[j] == null)) {
434 - if (existingUser.links == null) { existingUser.links = {}; }
435 - existingUser.links[j] = newobj.links[j];
356 + return;
357 + }
358 + if (obj.args.dbexportmin) {
359 + // Export a minimal database to a JSON file. Export only users, meshes and nodes.
360 + // This is a useful command to look at the database.
361 + if (obj.args.dbexportmin == true) { obj.args.dbexportmin = obj.getConfigFilePath('meshcentral.db.json'); }
362 + obj.db.GetAllType({ $in: ['user', 'node', 'mesh'] }, function (err, docs) {
363 + obj.fs.writeFileSync(obj.args.dbexportmin, JSON.stringify(docs));
364 + console.log('Exported ' + docs.length + ' objects(s) to ' + obj.args.dbexportmin + '.'); process.exit();
365 + });
366 + return;
367 + }
368 + if (obj.args.dbimport) {
369 + // Import the entire database from a JSON file
370 + if (obj.args.dbimport == true) { obj.args.dbimport = obj.getConfigFilePath('meshcentral.db.json'); }
371 + var json = null, json2 = "", badCharCount = 0;
372 + try { json = obj.fs.readFileSync(obj.args.dbimport, { encoding: 'utf8' }); } catch (e) { console.log('Invalid JSON file: ' + obj.args.dbimport + ': ' + e); process.exit(); }
373 + for (i = 0; i < json.length; i++) { if (json.charCodeAt(i) >= 32) { json2 += json[i]; } else { var tt = json.charCodeAt(i); if (tt != 10 && tt != 13) { badCharCount++; } } } // Remove all bad chars
374 + if (badCharCount > 0) { console.log(badCharCount + ' invalid character(s) where removed.'); }
375 + try { json = JSON.parse(json2); } catch (e) { console.log('Invalid JSON format: ' + obj.args.dbimport + ': ' + e); process.exit(); }
376 + if ((json == null) || (typeof json.length != 'number') || (json.length < 1)) { console.log('Invalid JSON format: ' + obj.args.dbimport + '.'); }
377 + for (i in json) { if ((json[i].type == "mesh") && (json[i].links != null)) { for (var j in json[i].links) { var esc = obj.common.escapeFieldName(j); if (esc !== j) { json[i].links[esc] = json[i].links[j]; delete json[i].links[j]; } } } } // Escape MongoDB invalid field chars
378 + //for (i in json) { if ((json[i].type == "node") && (json[i].host != null)) { json[i].rname = json[i].host; delete json[i].host; } } // DEBUG: Change host to rname
379 + setTimeout(function () { // If the Mongo database is being created for the first time, there is a race condition here. This will get around it.
380 + obj.db.RemoveAll(function () {
381 + obj.db.InsertMany(json, function (err) {
382 + if (err != null) { console.log(err); } else { console.log('Imported ' + json.length + ' objects(s) from ' + obj.args.dbimport + '.'); } process.exit();
383 + });
384 + });
385 + }, 100);
386 + return;
387 + }
388 + /*
389 + if (obj.args.dbimport) {
390 + // Import the entire database from a very large JSON file
391 + obj.db.RemoveAll(function () {
392 + if (obj.args.dbimport == true) { obj.args.dbimport = obj.getConfigFilePath('meshcentral.db.json'); }
393 + var json = null, json2 = "", badCharCount = 0;
394 + const StreamArray = require('stream-json/streamers/StreamArray');
395 + const jsonStream = StreamArray.withParser();
396 + jsonStream.on('data', function (data) { obj.db.Set(data.value); });
397 + jsonStream.on('end', () => { console.log('Done.'); process.exit(); });
398 + obj.fs.createReadStream(obj.args.dbimport).pipe(jsonStream.input);
399 + });
400 + return;
401 + }
402 + */
403 + if (obj.args.dbmerge) {
404 + // Import the entire database from a JSON file
405 + if (obj.args.dbmerge == true) { obj.args.dbmerge = obj.getConfigFilePath('meshcentral.db.json'); }
406 + var json = null, json2 = "", badCharCount = 0;
407 + try { json = obj.fs.readFileSync(obj.args.dbmerge, { encoding: 'utf8' }); } catch (e) { console.log('Invalid JSON file: ' + obj.args.dbmerge + ': ' + e); process.exit(); }
408 + for (i = 0; i < json.length; i++) { if (json.charCodeAt(i) >= 32) { json2 += json[i]; } else { var tt = json.charCodeAt(i); if (tt != 10 && tt != 13) { badCharCount++; } } } // Remove all bad chars
409 + if (badCharCount > 0) { console.log(badCharCount + ' invalid character(s) where removed.'); }
410 + try { json = JSON.parse(json2); } catch (e) { console.log('Invalid JSON format: ' + obj.args.dbmerge + ': ' + e); process.exit(); }
411 + if ((json == null) || (typeof json.length != 'number') || (json.length < 1)) { console.log('Invalid JSON format: ' + obj.args.dbimport + '.'); }
412 +
413 + // Get all users from current database
414 + obj.db.GetAllType('user', function (err, docs) {
415 + var users = {}, usersCount = 0;
416 + for (var i in docs) { users[docs[i]._id] = docs[i]; usersCount++; }
417 +
418 + // Fetch all meshes from the database
419 + obj.db.GetAllType('mesh', function (err, docs) {
420 + obj.common.unEscapeAllLinksFieldName(docs);
421 + var meshes = {}, meshesCount = 0;
422 + for (var i in docs) { meshes[docs[i]._id] = docs[i]; meshesCount++; }
423 + console.log('Loaded ' + usersCount + ' users and ' + meshesCount + ' meshes.');
424 + // Look at each object in the import file
425 + var objectToAdd = [];
426 + for (var i in json) {
427 + var newobj = json[i];
428 + if (newobj.type == 'user') {
429 + // Check if the user already exists
430 + var existingUser = users[newobj._id];
431 + if (existingUser) {
432 + // Merge the links
433 + if (typeof newobj.links == 'object') {
434 + for (var j in newobj.links) {
435 + if ((existingUser.links == null) || (existingUser.links[j] == null)) {
436 + if (existingUser.links == null) { existingUser.links = {}; }
437 + existingUser.links[j] = newobj.links[j];
438 + }
439 + }
440 }
441 + if (existingUser.name == 'admin') { existingUser.links = {}; }
442 + objectToAdd.push(existingUser); // Add this user
443 + } else {
444 + objectToAdd.push(newobj); // Add this user
445 }
438 - }
439 - if (existingUser.name == 'admin') { existingUser.links = {}; }
440 - objectToAdd.push(existingUser); // Add this user
441 - } else {
442 - objectToAdd.push(newobj); // Add this user
446 + } else if (newobj.type == 'mesh') {
447 + // Add this object after escaping
448 + objectToAdd.push(obj.common.escapeLinksFieldName(newobj));
449 + } // Don't add nodes.
450 }
444 - } else if (newobj.type == 'mesh') {
445 - // Add this object after escaping
446 - objectToAdd.push(obj.common.escapeLinksFieldName(newobj));
447 - } // Don't add nodes.
448 - }
449 - console.log('Importing ' + objectToAdd.length + ' object(s)...');
450 - var pendingCalls = 1;
451 - for (var i in objectToAdd) {
452 - pendingCalls++;
453 - obj.db.Set(objectToAdd[i], function (err) { if (err != null) { console.log(err); } else { if (--pendingCalls == 0) { process.exit(); } } });
454 - }
455 - if (--pendingCalls == 0) { process.exit(); }
456 - });
457 - });
458 - return;
459 - }
460 -
461 - // Load configuration for database if needed
462 - if (obj.args.loadconfigfromdb) {
463 - var key = null;
464 - if (typeof obj.args.configkey == 'string') { key = obj.args.configkey; }
465 - else if (typeof obj.args.loadconfigfromdb == 'string') { key = obj.args.loadconfigfromdb; }
466 - if (key == null) { console.log('Error, --configkey is required.'); process.exit(); return; }
467 - obj.db.getAllConfigFiles(key, function (configFiles) {
468 - if (configFiles == null) { console.log('Error, no configuration files found or invalid configkey.'); process.exit(); return; }
469 - if (!configFiles['config.json']) { console.log('Error, could not file config.json from database.'); process.exit(); return; }
470 - obj.configurationFiles = configFiles;
471 -
472 - // Parse the new configuration file
473 - var config2 = null;
474 - try { config2 = JSON.parse(configFiles['config.json']); } catch (ex) { console.log('Error, unable to parse config.json from database.'); process.exit(); return; }
475 -
476 - // Set the command line arguments to the config file if they are not present
477 - if (!config2.settings) { config2.settings = {}; }
478 - for (i in args) { config2.settings[i] = args[i]; }
479 -
480 - // Lower case all keys in the config file
481 - try {
482 - require('./common.js').objKeysToLower(config2, ["ldapoptions"]);
483 - } catch (ex) {
484 - console.log('CRITICAL ERROR: Unable to access the file \"./common.js\".\r\nCheck folder & file permissions.');
485 - process.exit();
451 + console.log('Importing ' + objectToAdd.length + ' object(s)...');
452 + var pendingCalls = 1;
453 + for (var i in objectToAdd) {
454 + pendingCalls++;
455 + obj.db.Set(objectToAdd[i], function (err) { if (err != null) { console.log(err); } else { if (--pendingCalls == 0) { process.exit(); } } });
456 + }
457 + if (--pendingCalls == 0) { process.exit(); }
458 + });
459 + });
460 return;
461 }
462
489 - // Grad some of the values from the original config.json file if present.
490 - config2['mongodb'] = config['mongodb'];
491 - config2['mongodbcol'] = config['mongodbcol'];
492 - config2['dbencryptkey'] = config['dbencryptkey'];
463 + // Load configuration for database if needed
464 + if (obj.args.loadconfigfromdb) {
465 + var key = null;
466 + if (typeof obj.args.configkey == 'string') { key = obj.args.configkey; }
467 + else if (typeof obj.args.loadconfigfromdb == 'string') { key = obj.args.loadconfigfromdb; }
468 + if (key == null) { console.log('Error, --configkey is required.'); process.exit(); return; }
469 + obj.db.getAllConfigFiles(key, function (configFiles) {
470 + if (configFiles == null) { console.log('Error, no configuration files found or invalid configkey.'); process.exit(); return; }
471 + if (!configFiles['config.json']) { console.log('Error, could not file config.json from database.'); process.exit(); return; }
472 + obj.configurationFiles = configFiles;
473 +
474 + // Parse the new configuration file
475 + var config2 = null;
476 + try { config2 = JSON.parse(configFiles['config.json']); } catch (ex) { console.log('Error, unable to parse config.json from database.'); process.exit(); return; }
477 +
478 + // Set the command line arguments to the config file if they are not present
479 + if (!config2.settings) { config2.settings = {}; }
480 + for (i in args) { config2.settings[i] = args[i]; }
481 +
482 + // Lower case all keys in the config file
483 + try {
484 + require('./common.js').objKeysToLower(config2, ["ldapoptions"]);
485 + } catch (ex) {
486 + console.log('CRITICAL ERROR: Unable to access the file \"./common.js\".\r\nCheck folder & file permissions.');
487 + process.exit();
488 + return;
489 + }
490 +
491 + // Grad some of the values from the original config.json file if present.
492 + config2['mongodb'] = config['mongodb'];
493 + config2['mongodbcol'] = config['mongodbcol'];
494 + config2['dbencryptkey'] = config['dbencryptkey'];
495
494 - // We got a new config.json from the database, let's use it.
495 - config = obj.config = config2;
496 - obj.StartEx1b();
496 + // We got a new config.json from the database, let's use it.
497 + config = obj.config = config2;
498 + obj.StartEx1b();
499 + });
500 + } else {
501 + config = obj.config = getConfig(true);
502 + obj.StartEx1b();
503 + }
504 });
498 - } else {
499 - config = obj.config = getConfig(true);
500 - obj.StartEx1b();
505 }
502 - });
506 + );
507 };
508
509 // Time to start the serverf or real.
@@ -1711,7 +1715,8 @@ function mainStart(args) {
1715 if (require('os').platform() == 'win32') { modules.push('node-windows'); if (sspi == true) { modules.push('node-sspi'); } } // Add Windows modules
1716 if (ldap == true) { modules.push('ldapauth-fork'); }
1717 if (config.letsencrypt != null) { modules.push('greenlock'); modules.push('le-store-certbot'); modules.push('le-challenge-fs'); modules.push('le-acme-core'); } // Add Greenlock Modules
1714 - if (config.settings.mongodb != null) { modules.push('mongojs'); } // Add MongoDB
1718 + if (config.settings.mongodb != null) { modules.push('mongojs'); } // Add MongoJS
1719 + else if (config.settings.mongo != null) { modules.push('mongodb'); } // Add MongoDB
1720 if (config.smtp != null) { modules.push('nodemailer'); } // Add SMTP support
1721
1722 // Get the current node version