Added encrypted auto-backup support.
Ylian Saint-Hilaire committed
May 17, 2019 at 12:40 UTC
c25658f5f053de4d38b86a4b2b8cf7d6661d95fe
6 files changed
+105
-495
MeshCentralServer.njsproj
+1
@@ -91,6 +91,7 @@
91
<Compile Include="agents\recoverycore.js" />
92
<Compile Include="agents\testsuite.js" />
93
<Compile Include="agents\tinycore.js" />
94
+ <Compile Include="amt-ider.js" />
95
<Compile Include="amtevents.js" />
96
<Compile Include="amtscanner.js" />
97
<Compile Include="amtscript.js" />
db-test.js
deleted
-487
@@ -1,487 +0,0 @@
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
+83
@@ -668,5 +668,88 @@ module.exports.CreateDB = function (parent, func) {
668
func(obj); // Completed function setup
669
}
670
671
+ obj.performBackup = function () {
672
+ console.log('Performing backup...');
673
+ try { obj.parent.fs.mkdirSync(obj.parent.backuppath); } catch (e) { }
674
+ const dbname = (obj.parent.args.mongodbname) ? (obj.parent.args.mongodbname) : 'meshcentral';
675
+ const currentDate = new Date();
676
+ const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2);
677
+ const newAutoBackupFile = 'meshcentral-autobackup-' + fileSuffix;
678
+ const newAutoBackupPath = obj.parent.path.join(obj.parent.backuppath, newAutoBackupFile);
679
+
680
+ if ((obj.databaseType == 2) || (obj.databaseType == 3)) {
681
+ // Perform a MongoDump backup
682
+ const newBackupFile = 'mongodump-' + fileSuffix;
683
+ const newBackupPath = obj.parent.path.join(obj.parent.backuppath, newBackupFile);
684
+ var mongoDumpPath = 'mongodump';
685
+ if (obj.parent.config.settings.autobackup && obj.parent.config.settings.autobackup.mongodumppath) { mongoDumpPath = obj.parent.config.settings.autobackup.mongodumppath; }
686
+ const child_process = require('child_process');
687
+ const cmd = mongoDumpPath + ' --db \"' + dbname + '\" --archive=\"' + newBackupPath + '.archive\"';
688
+ var backupProcess = child_process.exec(cmd, { cwd: obj.parent.backuppath }, function (error, stdout, stderr) {
689
+ backupProcess = null;
690
+ if ((error != null) && (error != '')) { console.log('ERROR: Unable to perform database backup.\r\n'); return; }
691
+
692
+ // Perform archive compression
693
+ var archiver = require('archiver');
694
+ var output = obj.parent.fs.createWriteStream(newAutoBackupPath + '.zip');
695
+ var archive = null;
696
+ if (obj.parent.config.settings.autobackup && (typeof obj.parent.config.settings.autobackup.zippassword == 'string')) {
697
+ try { archiver.registerFormat('zip-encrypted', require("archiver-zip-encrypted")); } catch (ex) { }
698
+ archive = archiver.create('zip-encrypted', { zlib: { level: 9 }, encryptionMethod: 'aes256', password: obj.parent.config.settings.autobackup.zippassword });
699
+ } else {
700
+ archive = archiver('zip', { zlib: { level: 9 } });
701
+ }
702
+ output.on('close', function () { setTimeout(function () { try { obj.parent.fs.unlink(newBackupPath + '.archive'); } catch (ex) { } }, 5000); });
703
+ output.on('end', function () { });
704
+ archive.on('warning', function (err) { console.log('Backup warning: ' + err); });
705
+ archive.on('error', function (err) { console.log('Backup error: ' + err); });
706
+ archive.pipe(output);
707
+ archive.file(newBackupPath + '.archive', { name: newBackupFile + '.archive' });
708
+ archive.directory(obj.parent.datapath, 'meshcentral-data');
709
+ archive.finalize();
710
+ });
711
+ } else {
712
+ // Perform a NeDB backup
713
+ var archiver = require('archiver');
714
+ var output = obj.parent.fs.createWriteStream(newAutoBackupPath + '.zip');
715
+ var archive = null;
716
+ if (obj.parent.config.settings.autobackup && (typeof obj.parent.config.settings.autobackup.zippassword == 'string')) {
717
+ try { archiver.registerFormat('zip-encrypted', require("archiver-zip-encrypted")); } catch (ex) { }
718
+ archive = archiver.create('zip-encrypted', { zlib: { level: 9 }, encryptionMethod: 'aes256', password: obj.parent.config.settings.autobackup.zippassword });
719
+ } else {
720
+ archive = archiver('zip', { zlib: { level: 9 } });
721
+ }
722
+ output.on('close', function () { });
723
+ output.on('end', function () { });
724
+ archive.on('warning', function (err) { console.log('Backup warning: ' + err); });
725
+ archive.on('error', function (err) { console.log('Backup error: ' + err); });
726
+ archive.pipe(output);
727
+ archive.directory(obj.parent.datapath, 'meshcentral-data');
728
+ archive.finalize();
729
+ }
730
+
731
+ // Remove old backups
732
+ if (obj.parent.config.settings.autobackup && (typeof obj.parent.config.settings.autobackup.keeplastdaysbackup == 'number')) {
733
+ var cutoffDate = new Date();
734
+ cutoffDate.setDate(cutoffDate.getDate() - obj.parent.config.settings.autobackup.keeplastdaysbackup);
735
+ obj.parent.fs.readdir(obj.parent.backuppath, function (err, dir) {
736
+ if ((err == null) && (dir.length > 0)) {
737
+ for (var i in dir) {
738
+ var name = dir[i];
739
+ if (name.startsWith('meshcentral-autobackup-') && name.endsWith('.zip')) {
740
+ var timex = name.substring(23, name.length - 4).split('-');
741
+ if (timex.length == 5) {
742
+ var fileDate = new Date(parseInt(timex[0]), parseInt(timex[1]) - 1, parseInt(timex[2]), parseInt(timex[3]), parseInt(timex[4]));
743
+ if (fileDate && (cutoffDate > fileDate)) { try { obj.parent.fs.unlink(obj.parent.path.join(obj.parent.backuppath, name)); } catch (ex) { } }
744
+ }
745
+ }
746
+ }
747
+ }
748
+ });
749
+ }
750
+ }
751
+
752
+ function padNumber(number, digits) { return Array(Math.max(digits - String(number).length + 1, 0)).join(0) + number; }
753
+
754
return obj;
755
};
\ No newline at end of file
meshcentral.js
+14
-7
@@ -69,12 +69,14 @@ function CreateMeshCentralServer(config, args) {
69
obj.parentpath = obj.path.join(__dirname, '../..');
70
obj.datapath = obj.path.join(__dirname, '../../meshcentral-data');
71
obj.filespath = obj.path.join(__dirname, '../../meshcentral-files');
72
+ obj.backuppath = obj.path.join(__dirname, '../../meshcentral-backup');
73
if (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web/views'))) { obj.webViewsPath = obj.path.join(__dirname, '../../meshcentral-web/views'); } else { obj.webViewsPath = obj.path.join(__dirname, 'views'); }
74
if (obj.fs.existsSync(obj.path.join(__dirname, '../../meshcentral-web/public'))) { obj.webPublicPath = obj.path.join(__dirname, '../../meshcentral-web/public'); } else { obj.webPublicPath = obj.path.join(__dirname, 'public'); }
75
} else {
76
obj.parentpath = __dirname;
77
obj.datapath = obj.path.join(__dirname, '../meshcentral-data');
78
obj.filespath = obj.path.join(__dirname, '../meshcentral-files');
79
+ obj.backuppath = obj.path.join(__dirname, '../meshcentral-backups');
80
if (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web/views'))) { obj.webViewsPath = obj.path.join(__dirname, '../meshcentral-web/views'); } else { obj.webViewsPath = obj.path.join(__dirname, 'views'); }
81
if (obj.fs.existsSync(obj.path.join(__dirname, '../meshcentral-web/public'))) { obj.webPublicPath = obj.path.join(__dirname, '../meshcentral-web/public'); } else { obj.webPublicPath = obj.path.join(__dirname, 'public'); }
82
}
@@ -506,7 +508,7 @@ function CreateMeshCentralServer(config, args) {
508
);
509
};
510
509
- // Time to start the serverf or real.
511
+ // Time to start the server or real.
512
obj.StartEx1b = function () {
513
var i;
514
@@ -865,6 +867,11 @@ function CreateMeshCentralServer(config, args) {
867
//obj.debug(1, 'Server started');
868
if (obj.args.nousers == true) { obj.updateServerState('nousers', '1'); }
869
obj.updateServerState('state', 'running');
870
+
871
+ // Setup database backup
872
+ if (obj.config.settings.autobackup && (typeof obj.config.settings.autobackup.backupinvervalhours == 'number')) {
873
+ setInterval(obj.db.performBackup, obj.config.settings.autobackup.backupinvervalhours * 60 * 60 * 1000);
874
+ }
875
});
876
});
877
};
@@ -1615,10 +1622,7 @@ function CreateMeshCentralServer(config, args) {
1622
// Return the server configuration
1623
function getConfig(createSampleConfig) {
1624
// Figure out the datapath location
1618
- var i;
1619
- var fs = require('fs');
1620
- var path = require('path');
1621
- var datapath = null;
1625
+ var i, fs = require('fs'), path = require('path'), datapath = null;
1626
var args = require('minimist')(process.argv.slice(2));
1627
if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) {
1628
datapath = path.join(__dirname, '../../meshcentral-data');
@@ -1686,7 +1690,7 @@ function InstallModule(modulename, func, tag1, tag2) {
1690
// Looks like we need to keep a global reference to the child process object for this to work correctly.
1691
InstallModuleChildProcess = child_process.exec('npm install --no-optional --save ' + modulename, { maxBuffer: 512000, timeout: 10000, cwd: parentpath }, function (error, stdout, stderr) {
1692
InstallModuleChildProcess = null;
1689
- if (error != null) {
1693
+ if ((error != null) && (error != '')) {
1694
console.log('ERROR: Unable to install required module "' + modulename + '". MeshCentral may not have access to npm, or npm may not have suffisent rights to load the new module. Try "npm install ' + modulename + '" to manualy install this module.\r\n');
1695
process.exit();
1696
return;
@@ -1735,7 +1739,7 @@ function mainStart(args) {
1739
if (ldap == true) { modules.push('ldapauth-fork'); }
1740
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
1741
if (config.settings.mongodb != null) { modules.push('mongojs'); } // Add MongoJS
1738
- else if (config.settings.mongo != null) { modules.push('mongodb'); } // Add MongoDB
1742
+ else if (config.settings.xmongodb != null) { modules.push('mongodb'); } // Add MongoDB
1743
if (config.smtp != null) { modules.push('nodemailer'); } // Add SMTP support
1744
1745
// Get the current node version
@@ -1744,6 +1748,9 @@ function mainStart(args) {
1748
// If running NodeJS < 8, install "util.promisify"
1749
if (nodeVersion < 8) { modules.push('util.promisify'); }
1750
1751
+ // Setup encrypted zip support if needed
1752
+ if (config.settings.autobackup && config.settings.autobackup.zippassword) { modules.push('archiver-zip-encrypted'); }
1753
+
1754
// Setup 2nd factor authentication
1755
if (config.settings.no2factorauth !== true) {
1756
// Setup YubiKey OTP if configured
package.json
+2
-1
@@ -1,6 +1,6 @@
1
{
2
"name": "meshcentral",
3
- "version": "0.3.4-p",
3
+ "version": "0.3.4-q",
4
"keywords": [
5
"Remote Management",
6
"Intel AMT",
@@ -28,6 +28,7 @@
28
],
29
"dependencies": {
30
"archiver": "^3.0.0",
31
+ "archiver-zip-encrypted": "^1.0.3",
32
"body-parser": "^1.19.0",
33
"cbor": "^4.1.5",
34
"compression": "^1.7.4",
sample-config.json
+5
@@ -36,6 +36,11 @@
36
{ "urls": "stun:stun.services.mozilla.com" },
37
{ "urls": "stun:stun.l.google.com:19302" }
38
]
39
+ },
40
+ "_AutoBackup": {
41
+ "backupInvervalHours": 24,
42
+ "keepLastDaysBackup": 10,
43
+ "zippassword": "MyReallySecretPassword3"
44
}
45
},
46
"_domains": {