Rolled back MongoDB fast bulk operations until more testing is done.

Ylian Saint-Hilaire committed Jan 27, 2021 at 02:23 UTC 027810259d08c7c1f37232c0930200f7e9bd3cf4
31 files changed +8657 -213
agents/agents-old/MeshCmd-signed.exe
Binary files /dev/null and b/agents/agents-old/MeshCmd-signed.exe differ
agents/agents-old/MeshCmd64-signed.exe
Binary files /dev/null and b/agents/agents-old/MeshCmd64-signed.exe differ
agents/agents-old/MeshService-signed.exe
Binary files /dev/null and b/agents/agents-old/MeshService-signed.exe differ
agents/agents-old/MeshService.exe
Binary files /dev/null and b/agents/agents-old/MeshService.exe differ
agents/agents-old/MeshService64-signed.exe
Binary files /dev/null and b/agents/agents-old/MeshService64-signed.exe differ
agents/agents-old/MeshService64.exe
Binary files /dev/null and b/agents/agents-old/MeshService64.exe differ
agents/agents-old/meshagent_aarch64
Binary files /dev/null and b/agents/agents-old/meshagent_aarch64 differ
agents/agents-old/meshagent_aarch64-cortex-a53
Binary files /dev/null and b/agents/agents-old/meshagent_aarch64-cortex-a53 differ
agents/agents-old/meshagent_arm
Binary files /dev/null and b/agents/agents-old/meshagent_arm differ
agents/agents-old/meshagent_arm-linaro
Binary files /dev/null and b/agents/agents-old/meshagent_arm-linaro differ
agents/agents-old/meshagent_arm64
Binary files /dev/null and b/agents/agents-old/meshagent_arm64 differ
agents/agents-old/meshagent_armhf
Binary files /dev/null and b/agents/agents-old/meshagent_armhf differ
agents/agents-old/meshagent_armhf2
Binary files /dev/null and b/agents/agents-old/meshagent_armhf2 differ
agents/agents-old/meshagent_freebsd_x86-64
Binary files /dev/null and b/agents/agents-old/meshagent_freebsd_x86-64 differ
agents/agents-old/meshagent_mips
Binary files /dev/null and b/agents/agents-old/meshagent_mips differ
agents/agents-old/meshagent_mips24kc
Binary files /dev/null and b/agents/agents-old/meshagent_mips24kc differ
agents/agents-old/meshagent_mipsel24kc
Binary files /dev/null and b/agents/agents-old/meshagent_mipsel24kc differ
agents/agents-old/meshagent_osx-arm-64
Binary files /dev/null and b/agents/agents-old/meshagent_osx-arm-64 differ
agents/agents-old/meshagent_osx-universal-64
Binary files /dev/null and b/agents/agents-old/meshagent_osx-universal-64 differ
agents/agents-old/meshagent_osx-x86-64
Binary files /dev/null and b/agents/agents-old/meshagent_osx-x86-64 differ
agents/agents-old/meshagent_pogo
Binary files /dev/null and b/agents/agents-old/meshagent_pogo differ
agents/agents-old/meshagent_poky
Binary files /dev/null and b/agents/agents-old/meshagent_poky differ
agents/agents-old/meshagent_poky64
Binary files /dev/null and b/agents/agents-old/meshagent_poky64 differ
agents/agents-old/meshagent_x86
Binary files /dev/null and b/agents/agents-old/meshagent_x86 differ
agents/agents-old/meshagent_x86-64
Binary files /dev/null and b/agents/agents-old/meshagent_x86-64 differ
agents/agents-old/meshagent_x86-64_nokvm
Binary files /dev/null and b/agents/agents-old/meshagent_x86-64_nokvm differ
agents/agents-old/meshagent_x86_nokvm
Binary files /dev/null and b/agents/agents-old/meshagent_x86_nokvm differ
db-bulk.js new
+2031
@@ -0,0 +1,2031 @@
1 +/**
2 +* @description MeshCentral database module
3 +* @author Ylian Saint-Hilaire
4 +* @copyright Intel Corporation 2018-2021
5 +* @license Apache-2.0
6 +* @version v0.0.2
7 +*/
8 +
9 +/*xjslint node: true */
10 +/*xjslint plusplus: true */
11 +/*xjslint maxlen: 256 */
12 +/*jshint node: true */
13 +/*jshint strict: false */
14 +/*jshint esversion: 6 */
15 +"use strict";
16 +
17 +//
18 +// Construct Meshcentral database object
19 +//
20 +// The default database is NeDB
21 +// https://github.com/louischatriot/nedb
22 +//
23 +// Alternativety, MongoDB can be used
24 +// https://www.mongodb.com/
25 +// Just run with --mongodb [connectionstring], where the connection string is documented here: https://docs.mongodb.com/manual/reference/connection-string/
26 +// The default collection is "meshcentral", but you can override it using --mongodbcol [collection]
27 +//
28 +module.exports.CreateDB = function (parent, func) {
29 + var obj = {};
30 + var Datastore = null;
31 + var expireEventsSeconds = (60 * 60 * 24 * 20); // By default, expire events after 20 days (1728000). (Seconds * Minutes * Hours * Days)
32 + var expirePowerEventsSeconds = (60 * 60 * 24 * 10); // By default, expire power events after 10 days (864000). (Seconds * Minutes * Hours * Days)
33 + var expireServerStatsSeconds = (60 * 60 * 24 * 30); // By default, expire power events after 30 days (2592000). (Seconds * Minutes * Hours * Days)
34 + const common = require('./common.js');
35 + obj.identifier = null;
36 + obj.dbKey = null;
37 + obj.dbRecordsEncryptKey = null;
38 + obj.dbRecordsDecryptKey = null;
39 + obj.changeStream = false;
40 + obj.pluginsActive = ((parent.config) && (parent.config.settings) && (parent.config.settings.plugins != null) && (parent.config.settings.plugins != false) && ((typeof parent.config.settings.plugins != 'object') || (parent.config.settings.plugins.enabled != false)));
41 +
42 + // MongoDB bulk operations state
43 + obj.filePendingGet = null;
44 + obj.filePendingGets = null;
45 + obj.filePendingRemove = null;
46 + obj.filePendingRemoves = null;
47 + obj.filePendingSet = false;
48 + obj.filePendingSets = null;
49 + obj.filePendingCb = null;
50 + obj.filePendingCbs = null;
51 + obj.powerFilePendingSet = false;
52 + obj.powerFilePendingSets = null;
53 + obj.powerFilePendingCb = null;
54 + obj.powerFilePendingCbs = null;
55 + obj.eventsFilePendingSet = false;
56 + obj.eventsFilePendingSets = null;
57 + obj.eventsFilePendingCb = null;
58 + obj.eventsFilePendingCbs = null;
59 +
60 + obj.SetupDatabase = function (func) {
61 + // Check if the database unique identifier is present
62 + // This is used to check that in server peering mode, everyone is using the same database.
63 + obj.Get('DatabaseIdentifier', function (err, docs) {
64 + if (err != null) { parent.debug('db', 'ERROR (Get DatabaseIdentifier): ' + err); }
65 + if ((err == null) && (docs.length == 1) && (docs[0].value != null)) {
66 + obj.identifier = docs[0].value;
67 + } else {
68 + obj.identifier = Buffer.from(require('crypto').randomBytes(48), 'binary').toString('hex');
69 + obj.Set({ _id: 'DatabaseIdentifier', value: obj.identifier });
70 + }
71 + });
72 +
73 + // Load database schema version and check if we need to update
74 + obj.Get('SchemaVersion', function (err, docs) {
75 + if (err != null) { parent.debug('db', 'ERROR (Get SchemaVersion): ' + err); }
76 + var ver = 0;
77 + if ((err == null) && (docs.length == 1)) { ver = docs[0].value; }
78 + if (ver == 1) { console.log('This is an unsupported beta 1 database, delete it to create a new one.'); process.exit(0); }
79 +
80 + // TODO: Any schema upgrades here...
81 + obj.Set({ _id: 'SchemaVersion', value: 2 });
82 +
83 + func(ver);
84 + });
85 + };
86 +
87 + // Perform database maintenance
88 + obj.maintenance = function () {
89 + if (obj.databaseType == 1) { // NeDB will not remove expired records unless we try to access them. This will force the removal.
90 + obj.eventsfile.remove({ time: { '$lt': new Date(Date.now() - (expireEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
91 + obj.powerfile.remove({ time: { '$lt': new Date(Date.now() - (expirePowerEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
92 + obj.serverstatsfile.remove({ time: { '$lt': new Date(Date.now() - (expireServerStatsSeconds * 1000)) } }, { multi: true }); // Force delete older events
93 + }
94 + }
95 +
96 + obj.cleanup = function (func) {
97 + // TODO: Remove all mesh links to invalid users
98 + // TODO: Remove all meshes that dont have any links
99 +
100 + // Remove all events, power events and SMBIOS data from the main collection. They are all in seperate collections now.
101 + if ((obj.databaseType == 4) || (obj.databaseType == 5)) {
102 + // MariaDB or MySQL
103 + obj.RemoveAllOfType('event', function () { });
104 + obj.RemoveAllOfType('power', function () { });
105 + obj.RemoveAllOfType('smbios', function () { });
106 + } else if (obj.databaseType == 3) {
107 + // MongoDB
108 + obj.file.deleteMany({ type: 'event' }, { multi: true });
109 + obj.file.deleteMany({ type: 'power' }, { multi: true });
110 + obj.file.deleteMany({ type: 'smbios' }, { multi: true });
111 + } else {
112 + // NeDB or MongoJS
113 + obj.file.remove({ type: 'event' }, { multi: true });
114 + obj.file.remove({ type: 'power' }, { multi: true });
115 + obj.file.remove({ type: 'smbios' }, { multi: true });
116 + }
117 +
118 + // List of valid identifiers
119 + var validIdentifiers = {}
120 +
121 + // Load all user groups
122 + obj.GetAllType('ugrp', function (err, docs) {
123 + if (err != null) { parent.debug('db', 'ERROR (GetAll user): ' + err); }
124 + if ((err == null) && (docs.length > 0)) {
125 + for (var i in docs) {
126 + // Add this as a valid user identifier
127 + validIdentifiers[docs[i]._id] = 1;
128 + }
129 + }
130 +
131 + // Fix all of the creating & login to ticks by seconds, not milliseconds.
132 + obj.GetAllType('user', function (err, docs) {
133 + if (err != null) { parent.debug('db', 'ERROR (GetAll user): ' + err); }
134 + if ((err == null) && (docs.length > 0)) {
135 + for (var i in docs) {
136 + var fixed = false;
137 +
138 + // Add this as a valid user identifier
139 + validIdentifiers[docs[i]._id] = 1;
140 +
141 + // Fix email address capitalization
142 + if (docs[i].email && (docs[i].email != docs[i].email.toLowerCase())) {
143 + docs[i].email = docs[i].email.toLowerCase(); fixed = true;
144 + }
145 +
146 + // Fix account creation
147 + if (docs[i].creation) {
148 + if (docs[i].creation > 1300000000000) { docs[i].creation = Math.floor(docs[i].creation / 1000); fixed = true; }
149 + if ((docs[i].creation % 1) != 0) { docs[i].creation = Math.floor(docs[i].creation); fixed = true; }
150 + }
151 +
152 + // Fix last account login
153 + if (docs[i].login) {
154 + if (docs[i].login > 1300000000000) { docs[i].login = Math.floor(docs[i].login / 1000); fixed = true; }
155 + if ((docs[i].login % 1) != 0) { docs[i].login = Math.floor(docs[i].login); fixed = true; }
156 + }
157 +
158 + // Fix last password change
159 + if (docs[i].passchange) {
160 + if (docs[i].passchange > 1300000000000) { docs[i].passchange = Math.floor(docs[i].passchange / 1000); fixed = true; }
161 + if ((docs[i].passchange % 1) != 0) { docs[i].passchange = Math.floor(docs[i].passchange); fixed = true; }
162 + }
163 +
164 + // Fix subscriptions
165 + if (docs[i].subscriptions != null) { delete docs[i].subscriptions; fixed = true; }
166 +
167 + // Save the user if needed
168 + if (fixed) { obj.Set(docs[i]); }
169 + }
170 +
171 + // Remove all objects that have a "meshid" that no longer points to a valid mesh.
172 + // Fix any incorrectly escaped user identifiers
173 + obj.GetAllType('mesh', function (err, docs) {
174 + if (err != null) { parent.debug('db', 'ERROR (GetAll mesh): ' + err); }
175 + var meshlist = [];
176 + if ((err == null) && (docs.length > 0)) {
177 + for (var i in docs) {
178 + var meshChange = false;
179 + docs[i] = common.unEscapeLinksFieldName(docs[i]);
180 + meshlist.push(docs[i]._id);
181 +
182 + // Make sure all mesh types are number type, if not, fix it.
183 + if (typeof docs[i].mtype == 'string') { docs[i].mtype = parseInt(docs[i].mtype); meshChange = true; }
184 +
185 + // Take a look at the links
186 + if (docs[i].links != null) {
187 + for (var j in docs[i].links) {
188 + if (validIdentifiers[j] == null) {
189 + // This identifier is not known, let see if we can fix it.
190 + var xid = j, xid2 = common.unEscapeFieldName(xid);
191 + while ((xid != xid2) && (validIdentifiers[xid2] == null)) { xid = xid2; xid2 = common.unEscapeFieldName(xid2); }
192 + if (validIdentifiers[xid2] == 1) {
193 + //console.log('Fixing id: ' + j + ' to ' + xid2);
194 + docs[i].links[xid2] = docs[i].links[j];
195 + delete docs[i].links[j];
196 + meshChange = true;
197 + } else {
198 + // TODO: here, we may want to clean up links to users and user groups that do not exist anymore.
199 + //console.log('Unknown id: ' + j);
200 + }
201 + }
202 + }
203 + }
204 +
205 + // Save the updated device group if needed
206 + if (meshChange) { obj.Set(docs[i]); }
207 + }
208 + }
209 + if ((obj.databaseType == 4) || (obj.databaseType == 5)) {
210 + // MariaDB
211 + sqlDbQuery('DELETE FROM MeshCentral.Main WHERE (extra LIKE ("mesh/%") AND (extra NOT IN ?)', [meshlist], func);
212 + } else if (obj.databaseType == 3) {
213 + // MongoDB
214 + obj.file.deleteMany({ meshid: { $exists: true, $nin: meshlist } }, { multi: true });
215 + } else {
216 + // NeDB or MongoJS
217 + obj.file.remove({ meshid: { $exists: true, $nin: meshlist } }, { multi: true });
218 + }
219 +
220 + // We are done
221 + validIdentifiers = null;
222 + if (func) { func(); }
223 + });
224 + }
225 + });
226 + });
227 + };
228 +
229 + // Get encryption key
230 + obj.getEncryptDataKey = function (password) {
231 + if (typeof password != 'string') return null;
232 + return parent.crypto.createHash('sha384').update(password).digest("raw").slice(0, 32);
233 + }
234 +
235 + // Encrypt data
236 + obj.encryptData = function (password, plaintext) {
237 + var key = obj.getEncryptDataKey(password);
238 + if (key == null) return null;
239 + const iv = parent.crypto.randomBytes(16);
240 + const aes = parent.crypto.createCipheriv('aes-256-cbc', key, iv);
241 + var ciphertext = aes.update(plaintext);
242 + ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
243 + return ciphertext.toString('base64');
244 + }
245 +
246 + // Decrypt data
247 + obj.decryptData = function (password, ciphertext) {
248 + try {
249 + var key = obj.getEncryptDataKey(password);
250 + if (key == null) return null;
251 + const ciphertextBytes = Buffer.from(ciphertext, 'base64');
252 + const iv = ciphertextBytes.slice(0, 16);
253 + const data = ciphertextBytes.slice(16);
254 + const aes = parent.crypto.createDecipheriv('aes-256-cbc', key, iv);
255 + var plaintextBytes = Buffer.from(aes.update(data));
256 + plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
257 + return plaintextBytes;
258 + } catch (ex) { return null; }
259 + }
260 +
261 + // Get the number of records in the database for various types, this is the slow NeDB way.
262 + // 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.
263 + obj.getStats = function (func) {
264 + if (obj.databaseType == 3) {
265 + // MongoDB
266 + obj.file.aggregate([{ "$group": { _id: "$type", count: { $sum: 1 } } }]).toArray(function (err, docs) {
267 + var counters = {}, totalCount = 0;
268 + if (err == null) { for (var i in docs) { if (docs[i]._id != null) { counters[docs[i]._id] = docs[i].count; totalCount += docs[i].count; } } }
269 + func(counters);
270 + });
271 + } else if (obj.databaseType == 2) {
272 + // MongoJS
273 + obj.file.aggregate([{ "$group": { _id: "$type", count: { $sum: 1 } } }], function (err, docs) {
274 + var counters = {}, totalCount = 0;
275 + if (err == null) { for (var i in docs) { if (docs[i]._id != null) { counters[docs[i]._id] = docs[i].count; totalCount += docs[i].count; } } }
276 + func(counters);
277 + });
278 + } else if (obj.databaseType == 1) {
279 + // NeDB version
280 + obj.file.count({ type: 'node' }, function (err, nodeCount) {
281 + obj.file.count({ type: 'mesh' }, function (err, meshCount) {
282 + obj.file.count({ type: 'user' }, function (err, userCount) {
283 + obj.file.count({ type: 'sysinfo' }, function (err, sysinfoCount) {
284 + obj.file.count({ type: 'note' }, function (err, noteCount) {
285 + obj.file.count({ type: 'iploc' }, function (err, iplocCount) {
286 + obj.file.count({ type: 'ifinfo' }, function (err, ifinfoCount) {
287 + obj.file.count({ type: 'cfile' }, function (err, cfileCount) {
288 + obj.file.count({ type: 'lastconnect' }, function (err, lastconnectCount) {
289 + obj.file.count({}, function (err, totalCount) {
290 + func({ node: nodeCount, mesh: meshCount, user: userCount, sysinfo: sysinfoCount, iploc: iplocCount, note: noteCount, ifinfo: ifinfoCount, cfile: cfileCount, lastconnect: lastconnectCount, total: totalCount });
291 + });
292 + });
293 + });
294 + });
295 + });
296 + });
297 + });
298 + });
299 + });
300 + });
301 + }
302 + }
303 +
304 + // 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.
305 + obj.getValueOfTheDay = function (id, startValue, func) { obj.Get(id, function (err, docs) { var date = new Date(), t = date.toLocaleDateString(); if ((err == null) && (docs.length == 1)) { var r = docs[0]; if (r.day == t) { func({ _id: id, value: r.value, day: t }); return; } } func({ _id: id, value: startValue, day: t }); }); };
306 + obj.escapeBase64 = function escapeBase64(val) { return (val.replace(/\+/g, '@').replace(/\//g, '$')); }
307 +
308 + // Encrypt an database object
309 + obj.performRecordEncryptionRecode = function (func) {
310 + var count = 0;
311 + obj.GetAllType('user', function (err, docs) {
312 + if (err != null) { parent.debug('db', 'ERROR (performRecordEncryptionRecode): ' + err); }
313 + if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } }
314 + obj.GetAllType('node', function (err, docs) {
315 + if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } }
316 + obj.GetAllType('mesh', function (err, docs) {
317 + if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } }
318 + if (obj.databaseType == 1) { // If we are using NeDB, compact the database.
319 + obj.file.persistence.compactDatafile();
320 + obj.file.on('compaction.done', function () { func(count); }); // It's important to wait for compaction to finish before exit, otherwise NeDB may corrupt.
321 + } else {
322 + func(count); // For all other databases, normal exit.
323 + }
324 + });
325 + });
326 + });
327 + }
328 +
329 + // Encrypt an database object
330 + function performTypedRecordDecrypt(data) {
331 + if ((data == null) || (obj.dbRecordsDecryptKey == null) || (typeof data != 'object')) return data;
332 + for (var i in data) {
333 + if (data[i] == null) continue;
334 + if (data[i].type == 'user') {
335 + data[i] = performPartialRecordDecrypt(data[i]);
336 + } else if ((data[i].type == 'node') && (data[i].intelamt != null)) {
337 + data[i].intelamt = performPartialRecordDecrypt(data[i].intelamt);
338 + } else if ((data[i].type == 'mesh') && (data[i].amt != null)) {
339 + data[i].amt = performPartialRecordDecrypt(data[i].amt);
340 + }
341 + }
342 + return data;
343 + }
344 +
345 + // Encrypt an database object
346 + function performTypedRecordEncrypt(data) {
347 + if (obj.dbRecordsEncryptKey == null) return data;
348 + if (data.type == 'user') { return performPartialRecordEncrypt(Clone(data), ['otpkeys', 'otphkeys', 'otpsecret', 'salt', 'hash', 'oldpasswords']); }
349 + else if ((data.type == 'node') && (data.intelamt != null)) { var xdata = Clone(data); xdata.intelamt = performPartialRecordEncrypt(xdata.intelamt, ['user', 'pass', 'mpspass']); return xdata; }
350 + else if ((data.type == 'mesh') && (data.amt != null)) { var xdata = Clone(data); xdata.amt = performPartialRecordEncrypt(xdata.amt, ['password']); return xdata; }
351 + return data;
352 + }
353 +
354 + // Encrypt an object and return a buffer.
355 + function performPartialRecordEncrypt(plainobj, encryptNames) {
356 + if (typeof plainobj != 'object') return plainobj;
357 + var enc = {}, enclen = 0;
358 + for (var i in encryptNames) { if (plainobj[encryptNames[i]] != null) { enclen++; enc[encryptNames[i]] = plainobj[encryptNames[i]]; delete plainobj[encryptNames[i]]; } }
359 + if (enclen > 0) { plainobj._CRYPT = performRecordEncrypt(enc); } else { delete plainobj._CRYPT; }
360 + return plainobj;
361 + }
362 +
363 + // Encrypt an object and return a buffer.
364 + function performPartialRecordDecrypt(plainobj) {
365 + if ((typeof plainobj != 'object') || (plainobj._CRYPT == null)) return plainobj;
366 + var enc = performRecordDecrypt(plainobj._CRYPT);
367 + if (enc != null) { for (var i in enc) { plainobj[i] = enc[i]; } }
368 + delete plainobj._CRYPT;
369 + return plainobj;
370 + }
371 +
372 + // Encrypt an object and return a base64.
373 + function performRecordEncrypt(plainobj) {
374 + if (obj.dbRecordsEncryptKey == null) return null;
375 + const iv = parent.crypto.randomBytes(12);
376 + const aes = parent.crypto.createCipheriv('aes-256-gcm', obj.dbRecordsEncryptKey, iv);
377 + var ciphertext = aes.update(JSON.stringify(plainobj));
378 + var cipherfinal = aes.final();
379 + ciphertext = Buffer.concat([iv, aes.getAuthTag(), ciphertext, cipherfinal]);
380 + return ciphertext.toString('base64');
381 + }
382 +
383 + // Takes a base64 and return an object.
384 + function performRecordDecrypt(ciphertext) {
385 + if (obj.dbRecordsDecryptKey == null) return null;
386 + const ciphertextBytes = Buffer.from(ciphertext, 'base64');
387 + const iv = ciphertextBytes.slice(0, 12);
388 + const data = ciphertextBytes.slice(28);
389 + const aes = parent.crypto.createDecipheriv('aes-256-gcm', obj.dbRecordsDecryptKey, iv);
390 + aes.setAuthTag(ciphertextBytes.slice(12, 28));
391 + var plaintextBytes, r;
392 + try {
393 + plaintextBytes = Buffer.from(aes.update(data));
394 + plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
395 + r = JSON.parse(plaintextBytes.toString());
396 + } catch (e) { throw "Incorrect DbRecordsDecryptKey/DbRecordsEncryptKey or invalid database _CRYPT data: " + e; }
397 + return r;
398 + }
399 +
400 + // Clone an object (TODO: Make this more efficient)
401 + function Clone(v) { return JSON.parse(JSON.stringify(v)); }
402 +
403 + // Read expiration time from configuration file
404 + if (typeof parent.args.dbexpire == 'object') {
405 + if (typeof parent.args.dbexpire.events == 'number') { expireEventsSeconds = parent.args.dbexpire.events; }
406 + if (typeof parent.args.dbexpire.powerevents == 'number') { expirePowerEventsSeconds = parent.args.dbexpire.powerevents; }
407 + if (typeof parent.args.dbexpire.statsevents == 'number') { expireServerStatsSeconds = parent.args.dbexpire.statsevents; }
408 + }
409 +
410 + // If a DB record encryption key is provided, perform database record encryption
411 + if ((typeof parent.args.dbrecordsencryptkey == 'string') && (parent.args.dbrecordsencryptkey.length != 0)) {
412 + // Hash the database password into a AES256 key and setup encryption and decryption.
413 + obj.dbRecordsEncryptKey = obj.dbRecordsDecryptKey = parent.crypto.createHash('sha384').update(parent.args.dbrecordsencryptkey).digest('raw').slice(0, 32);
414 + }
415 +
416 + // If a DB record decryption key is provided, perform database record decryption
417 + if ((typeof parent.args.dbrecordsdecryptkey == 'string') && (parent.args.dbrecordsdecryptkey.length != 0)) {
418 + // Hash the database password into a AES256 key and setup encryption and decryption.
419 + obj.dbRecordsDecryptKey = parent.crypto.createHash('sha384').update(parent.args.dbrecordsdecryptkey).digest('raw').slice(0, 32);
420 + }
421 +
422 + if (parent.args.mariadb || parent.args.mysql) {
423 + if (parent.args.mariadb) {
424 + // Use MariaDB
425 + obj.databaseType = 4;
426 + Datastore = require('mariadb').createPool(parent.args.mariadb);
427 + } else if (parent.args.mysql) {
428 + // Use MySQL
429 + Datastore = require('mysql').createConnection(parent.args.mysql);
430 + obj.databaseType = 5;
431 + }
432 + //sqlDbQuery('DROP DATABASE MeshCentral', null, function (err, docs) { console.log('DROP'); }); return;
433 + sqlDbQuery('USE meshcentral', null, function (err, docs) {
434 + if (err != null) { parent.debug('db', 'ERROR: USE meshcentral: ' + err); }
435 + if (err == null) { setupFunctions(func); } else {
436 + parent.debug('db', 'Creating database...');
437 + sqlDbBatchExec([
438 + 'CREATE DATABASE meshcentral',
439 + // Main table
440 + 'CREATE TABLE meshcentral.main (id VARCHAR(256) NOT NULL, type CHAR(32), domain CHAR(64), extra CHAR(255), extraex CHAR(255), doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))',
441 + 'CREATE INDEX ndxtypedomainextra ON meshcentral.main (type, domain, extra)',
442 + 'CREATE INDEX ndxextra ON meshcentral.main (extra)',
443 + 'CREATE INDEX ndxextraex ON meshcentral.main (extraex)',
444 + // Events table
445 + 'CREATE TABLE meshcentral.events(id INT NOT NULL AUTO_INCREMENT, time DATETIME, domain CHAR(64), action CHAR(255), nodeid CHAR(255), userid CHAR(255), doc JSON, PRIMARY KEY(id), CHECK(json_valid(doc)))',
446 + 'CREATE INDEX ndxeventstime ON meshcentral.events(time)',
447 + 'CREATE INDEX ndxeventsusername ON meshcentral.events(domain, userid, time)',
448 + 'CREATE INDEX ndxeventsdomainnodeidtime ON meshcentral.events(domain, nodeid, time)',
449 + // Events ID table
450 + 'CREATE TABLE meshcentral.eventids(fkid INT NOT NULL, target CHAR(255), CONSTRAINT fk_eventid FOREIGN KEY (fkid) REFERENCES events (id) ON DELETE CASCADE ON UPDATE RESTRICT)',
451 + 'CREATE INDEX ndxeventids ON meshcentral.eventids(target)',
452 + // Server stats table
453 + 'CREATE TABLE meshcentral.serverstats (time DATETIME, expire DATETIME, doc JSON, PRIMARY KEY(time), CHECK (json_valid(doc)))',
454 + 'CREATE INDEX ndxserverstattime ON meshcentral.serverstats (time)',
455 + 'CREATE INDEX ndxserverstatexpire ON meshcentral.serverstats (expire)',
456 + // Power events table
457 + 'CREATE TABLE meshcentral.power (id INT NOT NULL AUTO_INCREMENT, time DATETIME, nodeid CHAR(255), doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))',
458 + 'CREATE INDEX ndxpowernodeidtime ON meshcentral.power (nodeid, time)',
459 + // SMBIOS table
460 + 'CREATE TABLE meshcentral.smbios (id CHAR(255), time DATETIME, expire DATETIME, doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))',
461 + 'CREATE INDEX ndxsmbiostime ON meshcentral.smbios (time)',
462 + 'CREATE INDEX ndxsmbiosexpire ON meshcentral.smbios (expire)',
463 + // Plugins table
464 + 'CREATE TABLE meshcentral.plugin (id INT NOT NULL AUTO_INCREMENT, doc JSON, PRIMARY KEY(id), CHECK (json_valid(doc)))'
465 + ], function (err) {
466 + if (err != null) { parent.debug('db', 'BatchSetupDb: ' + err); }
467 + setupFunctions(func);
468 + });
469 + }
470 + });
471 + } else if (parent.args.mongodb) {
472 + // Use MongoDB
473 + obj.databaseType = 3;
474 + require('mongodb').MongoClient.connect(parent.args.mongodb, { useNewUrlParser: true, useUnifiedTopology: true }, function (err, client) {
475 + if (err != null) { console.log("Unable to connect to database: " + err); process.exit(); return; }
476 + Datastore = client;
477 + parent.debug('db', 'Connected to MongoDB database...');
478 +
479 + // Get the database name and setup the database client
480 + var dbname = 'meshcentral';
481 + if (parent.args.mongodbname) { dbname = parent.args.mongodbname; }
482 + const dbcollectionname = (parent.args.mongodbcol) ? (parent.args.mongodbcol) : 'meshcentral';
483 + const db = client.db(dbname);
484 +
485 + // Check the database version
486 + db.admin().serverInfo(function (err, info) {
487 + if ((err != null) || (info == null) || (info.versionArray == null) || (Array.isArray(info.versionArray) == false) || (info.versionArray.length < 2) || (typeof info.versionArray[0] != 'number') || (typeof info.versionArray[1] != 'number')) {
488 + console.log('WARNING: Unable to check MongoDB version.');
489 + } else {
490 + if ((info.versionArray[0] < 3) || ((info.versionArray[0] == 3) && (info.versionArray[1] < 6))) {
491 + // We are running with mongoDB older than 3.6, this is not good.
492 + parent.addServerWarning("Current version of MongoDB (" + info.version + ") is too old, please upgrade to MongoDB 3.6 or better.");
493 + }
494 + }
495 + });
496 +
497 + // Setup MongoDB main collection and indexes
498 + obj.file = db.collection(dbcollectionname);
499 + obj.file.indexes(function (err, indexes) {
500 + // Check if we need to reset indexes
501 + var indexesByName = {}, indexCount = 0;
502 + for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
503 + if ((indexCount != 4) || (indexesByName['TypeDomainMesh1'] == null) || (indexesByName['Email1'] == null) || (indexesByName['Mesh1'] == null)) {
504 + console.log('Resetting main indexes...');
505 + obj.file.dropIndexes(function (err) {
506 + obj.file.createIndex({ type: 1, domain: 1, meshid: 1 }, { sparse: 1, name: 'TypeDomainMesh1' }); // Speeds up GetAllTypeNoTypeField() and GetAllTypeNoTypeFieldMeshFiltered()
507 + obj.file.createIndex({ email: 1 }, { sparse: 1, name: 'Email1' }); // Speeds up GetUserWithEmail() and GetUserWithVerifiedEmail()
508 + obj.file.createIndex({ meshid: 1 }, { sparse: 1, name: 'Mesh1' }); // Speeds up RemoveMesh()
509 + });
510 + }
511 + });
512 +
513 + // Setup the changeStream on the MongoDB main collection if possible
514 + if (parent.args.mongodbchangestream == true) {
515 + if (typeof obj.file.watch != 'function') {
516 + console.log('WARNING: watch() is not a function, MongoDB ChangeStream not supported.');
517 + } else {
518 + obj.fileChangeStream = obj.file.watch([{ $match: { $or: [{ 'fullDocument.type': { $in: ['node', 'mesh', 'user', 'ugrp'] } }, { 'operationType': 'delete' }] } }], { fullDocument: 'updateLookup' });
519 + obj.fileChangeStream.on('change', function (change) {
520 + if ((change.operationType == 'update') || (change.operationType == 'replace')) {
521 + switch (change.fullDocument.type) {
522 + case 'node': { dbNodeChange(change, false); break; } // A node has changed
523 + case 'mesh': { dbMeshChange(change, false); break; } // A device group has changed
524 + case 'user': { dbUserChange(change, false); break; } // A user account has changed
525 + case 'ugrp': { dbUGrpChange(change, false); break; } // A user account has changed
526 + }
527 + } else if (change.operationType == 'insert') {
528 + switch (change.fullDocument.type) {
529 + case 'node': { dbNodeChange(change, true); break; } // A node has added
530 + case 'mesh': { dbMeshChange(change, true); break; } // A device group has created
531 + case 'user': { dbUserChange(change, true); break; } // A user account has created
532 + case 'ugrp': { dbUGrpChange(change, true); break; } // A user account has created
533 + }
534 + } else if (change.operationType == 'delete') {
535 + if ((change.documentKey == null) || (change.documentKey._id == null)) return;
536 + var splitId = change.documentKey._id.split('/');
537 + switch (splitId[0]) {
538 + case 'node': {
539 + //Not Good: Problem here is that we don't know what meshid the node belonged to before the delete.
540 + //parent.DispatchEvent(['*', node.meshid], obj, { etype: 'node', action: 'removenode', nodeid: change.documentKey._id, domain: splitId[1] });
541 + break;
542 + }
543 + case 'mesh': {
544 + parent.DispatchEvent(['*', change.documentKey._id], obj, { etype: 'mesh', action: 'deletemesh', meshid: change.documentKey._id, domain: splitId[1] });
545 + break;
546 + }
547 + case 'user': {
548 + //Not Good: This is not a perfect user removal because we don't know what groups the user was in.
549 + //parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', action: 'accountremove', userid: change.documentKey._id, domain: splitId[1], username: splitId[2] });
550 + break;
551 + }
552 + case 'ugrp': {
553 + parent.DispatchEvent(['*', change.documentKey._id], obj, { etype: 'ugrp', action: 'deleteusergroup', ugrpid: change.documentKey._id, domain: splitId[1] });
554 + break;
555 + }
556 + }
557 + }
558 + });
559 + obj.changeStream = true;
560 + }
561 + }
562 +
563 + // Setup MongoDB events collection and indexes
564 + obj.eventsfile = db.collection('events'); // Collection containing all events
565 + obj.eventsfile.indexes(function (err, indexes) {
566 + // Check if we need to reset indexes
567 + var indexesByName = {}, indexCount = 0;
568 + for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
569 + if ((indexCount != 5) || (indexesByName['Username1'] == null) || (indexesByName['DomainNodeTime1'] == null) || (indexesByName['IdsAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
570 + // Reset all indexes
571 + console.log("Resetting events indexes...");
572 + obj.eventsfile.dropIndexes(function (err) {
573 + obj.eventsfile.createIndex({ username: 1 }, { sparse: 1, name: 'Username1' });
574 + obj.eventsfile.createIndex({ domain: 1, nodeid: 1, time: -1 }, { sparse: 1, name: 'DomainNodeTime1' });
575 + obj.eventsfile.createIndex({ ids: 1, time: -1 }, { sparse: 1, name: 'IdsAndTime1' });
576 + obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
577 + });
578 + } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireEventsSeconds) {
579 + // Reset the timeout index
580 + console.log("Resetting events expire index...");
581 + obj.eventsfile.dropIndex('ExpireTime1', function (err) {
582 + obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
583 + });
584 + }
585 + });
586 +
587 + // Setup MongoDB power events collection and indexes
588 + obj.powerfile = db.collection('power'); // Collection containing all power events
589 + obj.powerfile.indexes(function (err, indexes) {
590 + // Check if we need to reset indexes
591 + var indexesByName = {}, indexCount = 0;
592 + for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
593 + if ((indexCount != 3) || (indexesByName['NodeIdAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
594 + // Reset all indexes
595 + console.log("Resetting power events indexes...");
596 + obj.powerfile.dropIndexes(function (err) {
597 + // Create all indexes
598 + obj.powerfile.createIndex({ nodeid: 1, time: 1 }, { sparse: 1, name: 'NodeIdAndTime1' });
599 + obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
600 + });
601 + } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expirePowerEventsSeconds) {
602 + // Reset the timeout index
603 + console.log("Resetting power events expire index...");
604 + obj.powerfile.dropIndex('ExpireTime1', function (err) {
605 + // Reset the expire power events index
606 + obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
607 + });
608 + }
609 + });
610 +
611 + // Setup MongoDB smbios collection, no indexes needed
612 + obj.smbiosfile = db.collection('smbios'); // Collection containing all smbios information
613 +
614 + // Setup MongoDB server stats collection
615 + obj.serverstatsfile = db.collection('serverstats'); // Collection of server stats
616 + obj.serverstatsfile.indexes(function (err, indexes) {
617 + // Check if we need to reset indexes
618 + var indexesByName = {}, indexCount = 0;
619 + for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
620 + if ((indexCount != 3) || (indexesByName['ExpireTime1'] == null)) {
621 + // Reset all indexes
622 + console.log("Resetting server stats indexes...");
623 + obj.serverstatsfile.dropIndexes(function (err) {
624 + // Create all indexes
625 + obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
626 + obj.serverstatsfile.createIndex({ 'expire': 1 }, { expireAfterSeconds: 0, name: 'ExpireTime2' }); // Auto-expire events
627 + });
628 + } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireServerStatsSeconds) {
629 + // Reset the timeout index
630 + console.log("Resetting server stats expire index...");
631 + obj.serverstatsfile.dropIndex('ExpireTime1', function (err) {
632 + // Reset the expire server stats index
633 + obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
634 + });
635 + }
636 + });
637 +
638 + // Setup plugin info collection
639 + if (obj.pluginsActive) { obj.pluginsfile = db.collection('plugins'); }
640 +
641 + setupFunctions(func); // Completed setup of MongoDB
642 + });
643 + } else if (parent.args.xmongodb) {
644 + // Use MongoJS, this is the old system.
645 + obj.databaseType = 2;
646 + Datastore = require('mongojs');
647 + var db = Datastore(parent.args.xmongodb);
648 + var dbcollection = 'meshcentral';
649 + if (parent.args.mongodbcol) { dbcollection = parent.args.mongodbcol; }
650 +
651 + // Setup MongoDB main collection and indexes
652 + obj.file = db.collection(dbcollection);
653 + obj.file.getIndexes(function (err, indexes) {
654 + // Check if we need to reset indexes
655 + var indexesByName = {}, indexCount = 0;
656 + for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
657 + if ((indexCount != 4) || (indexesByName['TypeDomainMesh1'] == null) || (indexesByName['Email1'] == null) || (indexesByName['Mesh1'] == null)) {
658 + console.log("Resetting main indexes...");
659 + obj.file.dropIndexes(function (err) {
660 + obj.file.createIndex({ type: 1, domain: 1, meshid: 1 }, { sparse: 1, name: 'TypeDomainMesh1' }); // Speeds up GetAllTypeNoTypeField() and GetAllTypeNoTypeFieldMeshFiltered()
661 + obj.file.createIndex({ email: 1 }, { sparse: 1, name: 'Email1' }); // Speeds up GetUserWithEmail() and GetUserWithVerifiedEmail()
662 + obj.file.createIndex({ meshid: 1 }, { sparse: 1, name: 'Mesh1' }); // Speeds up RemoveMesh()
663 + });
664 + }
665 + });
666 +
667 + // Setup MongoDB events collection and indexes
668 + obj.eventsfile = db.collection('events'); // Collection containing all events
669 + obj.eventsfile.getIndexes(function (err, indexes) {
670 + // Check if we need to reset indexes
671 + var indexesByName = {}, indexCount = 0;
672 + for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
673 + if ((indexCount != 5) || (indexesByName['Username1'] == null) || (indexesByName['DomainNodeTime1'] == null) || (indexesByName['IdsAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
674 + // Reset all indexes
675 + console.log("Resetting events indexes...");
676 + obj.eventsfile.dropIndexes(function (err) {
677 + obj.eventsfile.createIndex({ username: 1 }, { sparse: 1, name: 'Username1' });
678 + obj.eventsfile.createIndex({ domain: 1, nodeid: 1, time: -1 }, { sparse: 1, name: 'DomainNodeTime1' });
679 + obj.eventsfile.createIndex({ ids: 1, time: -1 }, { sparse: 1, name: 'IdsAndTime1' });
680 + obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
681 + });
682 + } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireEventsSeconds) {
683 + // Reset the timeout index
684 + console.log("Resetting events expire index...");
685 + obj.eventsfile.dropIndex('ExpireTime1', function (err) {
686 + obj.eventsfile.createIndex({ time: 1 }, { expireAfterSeconds: expireEventsSeconds, name: 'ExpireTime1' });
687 + });
688 + }
689 + });
690 +
691 + // Setup MongoDB power events collection and indexes
692 + obj.powerfile = db.collection('power'); // Collection containing all power events
693 + obj.powerfile.getIndexes(function (err, indexes) {
694 + // Check if we need to reset indexes
695 + var indexesByName = {}, indexCount = 0;
696 + for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
697 + if ((indexCount != 3) || (indexesByName['NodeIdAndTime1'] == null) || (indexesByName['ExpireTime1'] == null)) {
698 + // Reset all indexes
699 + console.log("Resetting power events indexes...");
700 + obj.powerfile.dropIndexes(function (err) {
701 + // Create all indexes
702 + obj.powerfile.createIndex({ nodeid: 1, time: 1 }, { sparse: 1, name: 'NodeIdAndTime1' });
703 + obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
704 + });
705 + } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expirePowerEventsSeconds) {
706 + // Reset the timeout index
707 + console.log("Resetting power events expire index...");
708 + obj.powerfile.dropIndex('ExpireTime1', function (err) {
709 + // Reset the expire power events index
710 + obj.powerfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expirePowerEventsSeconds, name: 'ExpireTime1' });
711 + });
712 + }
713 + });
714 +
715 + // Setup MongoDB smbios collection, no indexes needed
716 + obj.smbiosfile = db.collection('smbios'); // Collection containing all smbios information
717 +
718 + // Setup MongoDB server stats collection
719 + obj.serverstatsfile = db.collection('serverstats'); // Collection of server stats
720 + obj.serverstatsfile.getIndexes(function (err, indexes) {
721 + // Check if we need to reset indexes
722 + var indexesByName = {}, indexCount = 0;
723 + for (var i in indexes) { indexesByName[indexes[i].name] = indexes[i]; indexCount++; }
724 + if ((indexCount != 3) || (indexesByName['ExpireTime1'] == null)) {
725 + // Reset all indexes
726 + console.log("Resetting server stats indexes...");
727 + obj.serverstatsfile.dropIndexes(function (err) {
728 + // Create all indexes
729 + obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
730 + obj.serverstatsfile.createIndex({ 'expire': 1 }, { expireAfterSeconds: 0, name: 'ExpireTime2' }); // Auto-expire events
731 + });
732 + } else if (indexesByName['ExpireTime1'].expireAfterSeconds != expireServerStatsSeconds) {
733 + // Reset the timeout index
734 + console.log("Resetting server stats expire index...");
735 + obj.serverstatsfile.dropIndex('ExpireTime1', function (err) {
736 + // Reset the expire server stats index
737 + obj.serverstatsfile.createIndex({ 'time': 1 }, { expireAfterSeconds: expireServerStatsSeconds, name: 'ExpireTime1' });
738 + });
739 + }
740 + });
741 +
742 + // Setup plugin info collection
743 + if (obj.pluginsActive) { obj.pluginsfile = db.collection('plugins'); }
744 +
745 + setupFunctions(func); // Completed setup of MongoJS
746 + } else {
747 + // Use NeDB (The default)
748 + obj.databaseType = 1;
749 + Datastore = require('nedb');
750 + var datastoreOptions = { filename: parent.getConfigFilePath('meshcentral.db'), autoload: true };
751 +
752 + // If a DB encryption key is provided, perform database encryption
753 + if ((typeof parent.args.dbencryptkey == 'string') && (parent.args.dbencryptkey.length != 0)) {
754 + // Hash the database password into a AES256 key and setup encryption and decryption.
755 + obj.dbKey = parent.crypto.createHash('sha384').update(parent.args.dbencryptkey).digest('raw').slice(0, 32);
756 + datastoreOptions.afterSerialization = function (plaintext) {
757 + const iv = parent.crypto.randomBytes(16);
758 + const aes = parent.crypto.createCipheriv('aes-256-cbc', obj.dbKey, iv);
759 + var ciphertext = aes.update(plaintext);
760 + ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
761 + return ciphertext.toString('base64');
762 + }
763 + datastoreOptions.beforeDeserialization = function (ciphertext) {
764 + const ciphertextBytes = Buffer.from(ciphertext, 'base64');
765 + const iv = ciphertextBytes.slice(0, 16);
766 + const data = ciphertextBytes.slice(16);
767 + const aes = parent.crypto.createDecipheriv('aes-256-cbc', obj.dbKey, iv);
768 + var plaintextBytes = Buffer.from(aes.update(data));
769 + plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
770 + return plaintextBytes.toString();
771 + }
772 + }
773 +
774 + // Start NeDB main collection and setup indexes
775 + obj.file = new Datastore(datastoreOptions);
776 + obj.file.persistence.setAutocompactionInterval(86400000); // Compact once a day
777 + obj.file.ensureIndex({ fieldName: 'type' });
778 + obj.file.ensureIndex({ fieldName: 'domain' });
779 + obj.file.ensureIndex({ fieldName: 'meshid', sparse: true });
780 + obj.file.ensureIndex({ fieldName: 'nodeid', sparse: true });
781 + obj.file.ensureIndex({ fieldName: 'email', sparse: true });
782 +
783 + // Setup the events collection and setup indexes
784 + obj.eventsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-events.db'), autoload: true, corruptAlertThreshold: 1 });
785 + obj.eventsfile.persistence.setAutocompactionInterval(86400000); // Compact once a day
786 + obj.eventsfile.ensureIndex({ fieldName: 'ids' }); // TODO: Not sure if this is a good index, this is a array field.
787 + obj.eventsfile.ensureIndex({ fieldName: 'nodeid', sparse: true });
788 + obj.eventsfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: expireEventsSeconds });
789 + obj.eventsfile.remove({ time: { '$lt': new Date(Date.now() - (expireEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
790 +
791 + // Setup the power collection and setup indexes
792 + obj.powerfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-power.db'), autoload: true, corruptAlertThreshold: 1 });
793 + obj.powerfile.persistence.setAutocompactionInterval(86400000); // Compact once a day
794 + obj.powerfile.ensureIndex({ fieldName: 'nodeid' });
795 + obj.powerfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: expirePowerEventsSeconds });
796 + obj.powerfile.remove({ time: { '$lt': new Date(Date.now() - (expirePowerEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
797 +
798 + // Setup the SMBIOS collection, for NeDB we don't setup SMBIOS since NeDB will corrupt the database. Remove any existing ones.
799 + //obj.smbiosfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-smbios.db'), autoload: true, corruptAlertThreshold: 1 });
800 + parent.fs.unlink(parent.getConfigFilePath('meshcentral-smbios.db'), function () { });
801 +
802 + // Setup the server stats collection and setup indexes
803 + obj.serverstatsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-stats.db'), autoload: true, corruptAlertThreshold: 1 });
804 + obj.serverstatsfile.persistence.setAutocompactionInterval(86400000); // Compact once a day
805 + obj.serverstatsfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: expireServerStatsSeconds });
806 + obj.serverstatsfile.ensureIndex({ fieldName: 'expire', expireAfterSeconds: 0 }); // Auto-expire events
807 + obj.serverstatsfile.remove({ time: { '$lt': new Date(Date.now() - (expireServerStatsSeconds * 1000)) } }, { multi: true }); // Force delete older events
808 +
809 + // Setup plugin info collection
810 + if (obj.pluginsActive) {
811 + obj.pluginsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-plugins.db'), autoload: true });
812 + obj.pluginsfile.persistence.setAutocompactionInterval(86400000); // Compact once a day
813 + }
814 +
815 + setupFunctions(func); // Completed setup of NeDB
816 + }
817 +
818 + // Check the object names for a "."
819 + function checkObjectNames(r, tag) {
820 + if (typeof r != 'object') return;
821 + for (var i in r) {
822 + if (i.indexOf('.') >= 0) { throw ('BadDbName (' + tag + '): ' + JSON.stringify(r)); }
823 + checkObjectNames(r[i], tag);
824 + }
825 + }
826 +
827 + // Query the database
828 + function sqlDbQuery(query, args, func) {
829 + if (obj.databaseType == 4) { // MariaDB
830 + Datastore.getConnection()
831 + .then(function (conn) {
832 + conn.query(query, args)
833 + .then(function (rows) {
834 + conn.release();
835 + const docs = [];
836 + for (var i in rows) { if (rows[i].doc) { docs.push(performTypedRecordDecrypt((typeof rows[i].doc == 'object')? rows[i].doc : JSON.parse(rows[i].doc))); } }
837 + if (func) try { func(null, docs); } catch (ex) { console.log('SQLERR1', ex); }
838 + })
839 + .catch(function (err) { conn.release(); if (func) try { func(err); } catch (ex) { console.log('SQLERR2', ex); } });
840 + }).catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log('SQLERR3', ex); } } });
841 + } else if (obj.databaseType == 5) { // MySQL
842 + Datastore.query(query, args, function (error, results, fields) {
843 + if (error != null) {
844 + if (func) try { func(error); } catch (ex) { console.log('SQLERR4', ex); }
845 + } else {
846 + var docs = [];
847 + for (var i in results) { if (results[i].doc) { docs.push(JSON.parse(results[i].doc)); } }
848 + //console.log(docs);
849 + if (func) { try { func(null, docs); } catch (ex) { console.log('SQLERR5', ex); } }
850 + }
851 + });
852 + }
853 + }
854 +
855 + // Exec on the database
856 + function sqlDbExec(query, args, func) {
857 + if (obj.databaseType == 4) { // MariaDB
858 + Datastore.getConnection()
859 + .then(function (conn) {
860 + conn.query(query, args)
861 + .then(function (rows) {
862 + conn.release();
863 + if (func) try { func(null, rows[0]); } catch (ex) { console.log(ex); }
864 + })
865 + .catch(function (err) { conn.release(); if (func) try { func(err); } catch (ex) { console.log(ex); } });
866 + }).catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } });
867 + } else if (obj.databaseType == 5) { // MySQL
868 + Datastore.query(query, args, function (error, results, fields) {
869 + if (func) try { func(error, results[0]); } catch (ex) { console.log(ex); }
870 + });
871 + }
872 + }
873 +
874 + // Execute a batch of commands on the database
875 + function sqlDbBatchExec(queries, func) {
876 + if (obj.databaseType == 4) { // MariaDB
877 + Datastore.getConnection()
878 + .then(function (conn) {
879 + var Promises = [];
880 + for (var i in queries) { if (typeof queries[i] == 'string') { Promises.push(conn.query(queries[i])); } else { Promises.push(conn.query(queries[i][0], queries[i][1])); } }
881 + Promise.all(Promises)
882 + .then(function (rows) { conn.release(); if (func) { try { func(null); } catch (ex) { console.log(ex); } } })
883 + .catch(function (err) { conn.release(); if (func) { try { func(err); } catch (ex) { console.log(ex); } } });
884 + })
885 + .catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } });
886 + } else if (obj.databaseType == 5) { // MySQL
887 + var Promises = [];
888 + for (var i in queries) { if (typeof queries[i] == 'string') { Promises.push(Datastore.query(queries[i])); } else { Promises.push(Datastore.query(queries[i][0], queries[i][1])); } }
889 + Promise.all(Promises)
890 + .then(function (error, results, fields) { if (func) { try { func(error, results); } catch (ex) { console.log(ex); } } })
891 + .catch(function (error, results, fields) { if (func) { try { func(error); } catch (ex) { console.log(ex); } } });
892 + }
893 + }
894 +
895 + function setupFunctions(func) {
896 + if ((obj.databaseType == 4) || (obj.databaseType == 5)) {
897 + // Database actions on the main collection (MariaDB or MySQL)
898 + obj.Set = function (value, func) {
899 + var extra = null, extraex = null;
900 + value = common.escapeLinksFieldNameEx(value);
901 + if (value.meshid) { extra = value.meshid; } else if (value.email) { extra = 'email/' + value.email; } else if (value.nodeid) { extra = value.nodeid; }
902 + if ((value.type == 'node') && (value.intelamt != null) && (value.intelamt.uuid != null)) { extraex = 'uuid/' + value.intelamt.uuid; }
903 + if (value._id == null) { value._id = require('crypto').randomBytes(16).toString('hex'); }
904 + sqlDbQuery('REPLACE INTO meshcentral.main VALUE (?, ?, ?, ?, ?, ?)', [value._id, (value.type ? value.type : null), ((value.domain != null) ? value.domain : null), extra, extraex, JSON.stringify(performTypedRecordEncrypt(value))], func);
905 + }
906 + obj.Get = function (_id, func) { sqlDbQuery('SELECT doc FROM meshcentral.main WHERE id = ?', [_id], function (err, docs) { if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); } func(err, docs); }); }
907 + obj.GetAll = function (func) { sqlDbQuery('SELECT domain, doc FROM meshcentral.main', null, func); }
908 + obj.GetHash = function (id, func) { sqlDbQuery('SELECT doc FROM meshcentral.main WHERE id = ?', [id], func); }
909 + obj.GetAllTypeNoTypeField = function (type, domain, func) { sqlDbQuery('SELECT doc FROM meshcentral.main WHERE type = ? AND domain = ?', [type, domain], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, docs); }); };
910 + obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
911 + if (id && (id != '')) {
912 + sqlDbQuery('SELECT doc FROM meshcentral.main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?)', [id, type, domain, meshes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, docs); });
913 + } else {
914 + if (extrasids == null) {
915 + sqlDbQuery('SELECT doc FROM meshcentral.main WHERE type = ? AND domain = ? AND extra IN (?)', [type, domain, meshes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, docs); });
916 + } else {
917 + sqlDbQuery('SELECT doc FROM meshcentral.main WHERE type = ? AND domain = ? AND (extra IN (?) OR id IN (?))', [type, domain, meshes, extrasids], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, docs); });
918 + }
919 + }
920 + };
921 + obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
922 + if (id && (id != '')) {
923 + sqlDbQuery('SELECT doc FROM meshcentral.main WHERE id = ? AND type = ? AND domain = ? AND extra IN (?)', [id, type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, docs); });
924 + } else {
925 + sqlDbQuery('SELECT doc FROM meshcentral.main WHERE type = ? AND domain = ? AND extra IN (?)', [type, domain, nodes], function (err, docs) { if (err == null) { for (var i in docs) { delete docs[i].type } } func(err, docs); });
926 + }
927 + };
928 + obj.GetAllType = function (type, func) { sqlDbQuery('SELECT doc FROM meshcentral.main WHERE type = ?', [type], func); }
929 + obj.GetAllIdsOfType = function (ids, domain, type, func) { sqlDbQuery('SELECT doc FROM meshcentral.main WHERE id IN (?) AND domain = ? AND type = ?', [ids, domain, type], func); }
930 + obj.GetUserWithEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM meshcentral.main WHERE domain = ? AND extra = ?', [domain, 'email/' + email], func); }
931 + obj.GetUserWithVerifiedEmail = function (domain, email, func) { sqlDbQuery('SELECT doc FROM meshcentral.main WHERE domain = ? AND extra = ?', [domain, 'email/' + email], func); }
932 + obj.Remove = function (id, func) { sqlDbQuery('DELETE FROM meshcentral.main WHERE id = ?', [id], func); };
933 + obj.RemoveAll = function (func) { sqlDbQuery('DELETE FROM meshcentral.main', null, func); };
934 + obj.RemoveAllOfType = function (type, func) { sqlDbQuery('DELETE FROM meshcentral.main WHERE type = ?', [type], func); };
935 + obj.InsertMany = function (data, func) { var pendingOps = 0; for (var i in data) { pendingOps++; obj.Set(data[i], function () { if (--pendingOps == 0) { func(); } }); } };
936 + obj.RemoveMeshDocuments = function (id) { sqlDbQuery('DELETE FROM meshcentral.main WHERE extra = ?', [id], function () { sqlDbQuery('DELETE FROM meshcentral.main WHERE id = ?', ['nt' + id], func); } ); };
937 + obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
938 + obj.DeleteDomain = function (domain, func) { sqlDbQuery('DELETE FROM meshcentral.main WHERE domain = ?', [domain], func); };
939 + obj.SetUser = function (user) { if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
940 + obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
941 + obj.getLocalAmtNodes = function (func) { sqlDbQuery('SELECT doc FROM meshcentral.main WHERE (type = "node") AND (extraex IS NOT NULL)', null, function (err, docs) { var r = []; if (err == null) { for (var i in docs) { if (docs[i].host != null) { r.push(docs[i]); } } } func(err, r); }); };
942 + obj.getAmtUuidMeshNode = function (meshid, uuid, func) { sqlDbQuery('SELECT doc FROM meshcentral.main WHERE meshid = ? AND extraex = ?', [meshid, 'uuid/' + uuid], func); };
943 + obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { sqlDbExec('SELECT COUNT(id) FROM meshcentral.main WHERE domain = ? AND type = ?', [domainid, type], function (err, response) { func((response['COUNT(id)'] == null) || (response['COUNT(id)'] > max), response['COUNT(id)']) }); } }
944 +
945 + // Database actions on the events collection
946 + obj.GetAllEvents = function (func) { sqlDbQuery('SELECT doc FROM meshcentral.events', null, func); };
947 + obj.StoreEvent = function (event, func) {
948 + var batchQuery = [['INSERT INTO meshcentral.events VALUE (?, ?, ?, ?, ?, ?, ?)', [null, event.time, ((typeof event.domain == 'string') ? event.domain : null), event.action, event.nodeid ? event.nodeid : null, event.userid ? event.userid : null, JSON.stringify(event)]]];
949 + for (var i in event.ids) { if (event.ids[i] != '*') { batchQuery.push(['INSERT INTO meshcentral.eventids VALUE (LAST_INSERT_ID(), ?)', [event.ids[i]]]); } }
950 + sqlDbBatchExec(batchQuery, function (err, docs) { if (func != null) { func(err, docs); } });
951 + };
952 + obj.GetEvents = function (ids, domain, func) {
953 + if (ids.indexOf('*') >= 0) {
954 + sqlDbQuery('SELECT doc FROM meshcentral.events WHERE (domain = ?) ORDER BY time DESC', [domain], func);
955 + } else {
956 + sqlDbQuery('SELECT doc FROM meshcentral.events JOIN meshcentral.eventids ON id = fkid WHERE (domain = ? AND target IN (?)) GROUP BY id ORDER BY time DESC', [domain, ids], func);
957 + }
958 + };
959 + obj.GetEventsWithLimit = function (ids, domain, limit, func) {
960 + if (ids.indexOf('*') >= 0) {
961 + sqlDbQuery('SELECT doc FROM meshcentral.events WHERE (domain = ?) ORDER BY time DESC LIMIT ?', [domain, limit], func);
962 + } else {
963 + sqlDbQuery('SELECT doc FROM meshcentral.events JOIN meshcentral.eventids ON id = fkid WHERE (domain = ? AND target IN (?)) GROUP BY id ORDER BY time DESC LIMIT ?', [domain, ids, limit], func);
964 + }
965 + };
966 + obj.GetUserEvents = function (ids, domain, username, func) {
967 + const userid = 'user/' + domain + '/' + username.toLowerCase();
968 + if (ids.indexOf('*') >= 0) {
969 + sqlDbQuery('SELECT doc FROM meshcentral.events WHERE (domain = ? AND userid = ?) ORDER BY time DESC', [domain, userid], func);
970 + } else {
971 + sqlDbQuery('SELECT doc FROM meshcentral.events JOIN meshcentral.eventids ON id = fkid WHERE (domain = ? AND userid = ? AND target IN (?)) GROUP BY id ORDER BY time DESC', [domain, userid, ids, limit], func);
972 + }
973 + };
974 + obj.GetUserEventsWithLimit = function (ids, domain, username, limit, func) {
975 + const userid = 'user/' + domain + '/' + username.toLowerCase();
976 + if (ids.indexOf('*') >= 0) {
977 + sqlDbQuery('SELECT doc FROM meshcentral.events WHERE (domain = ? AND userid = ?) ORDER BY time DESC LIMIT ?', [domain, userid, limit], func);
978 + } else {
979 + sqlDbQuery('SELECT doc FROM meshcentral.events JOIN meshcentral.eventids ON id = fkid WHERE (domain = ? AND userid = ? AND target IN (?)) GROUP BY id ORDER BY time DESC LIMIT ?', [domain, userid, ids, limit], func);
980 + }
981 + };
982 + obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, func) { sqlDbQuery('SELECT doc FROM meshcentral.events WHERE (nodeid = ?) AND (domain = ?) ORDER BY time DESC LIMIT ?', [nodeid, domain, limit], func); };
983 + obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, func) { sqlDbQuery('SELECT doc FROM meshcentral.events WHERE (nodeid = ?) AND (domain = ?) AND ((userid = ?) OR (userid IS NULL)) ORDER BY time DESC LIMIT ?', [nodeid, domain, userid, limit], func); };
984 + obj.RemoveAllEvents = function (domain) { sqlDbQuery('DELETE FROM meshcentral.events', null, function (err, docs) { }); };
985 + obj.RemoveAllNodeEvents = function (domain, nodeid) { sqlDbQuery('DELETE FROM meshcentral.events WHERE domain = ? AND nodeid = ?', [domain, nodeid], function (err, docs) { }); };
986 + obj.RemoveAllUserEvents = function (domain, userid) { sqlDbQuery('DELETE FROM meshcentral.events WHERE domain = ? AND userid = ?', [domain, userid], function (err, docs) { }); };
987 + obj.GetFailedLoginCount = function (username, domainid, lastlogin, func) { sqlDbExec('SELECT COUNT(id) FROM meshcentral.events WHERE action = "authfail" AND domain = ? AND userid = ? AND time > ?', [domainid, 'user/' + domainid + '/' + username.toLowerCase(), lastlogin], function (err, response) { func(err == null ? response['COUNT(id)'] : 0); }); }
988 +
989 + // Database actions on the power collection
990 + obj.getAllPower = function (func) { sqlDbQuery('SELECT doc FROM meshcentral.power', null, func); };
991 + obj.storePowerEvent = function (event, multiServer, func) { if (multiServer != null) { event.server = multiServer.serverid; } sqlDbQuery('INSERT INTO meshcentral.power VALUE (?, ?, ?, ?)', [null, event.time, event.nodeid ? event.nodeid : null, JSON.stringify(event)], func); };
992 + obj.getPowerTimeline = function (nodeid, func) { sqlDbQuery('SELECT doc FROM meshcentral.power WHERE ((nodeid = ?) OR (nodeid = "*")) ORDER BY time DESC', [nodeid], func); };
993 + obj.removeAllPowerEvents = function () { sqlDbQuery('DELETE FROM meshcentral.power', null, function (err, docs) { }); };
994 + obj.removeAllPowerEventsForNode = function (nodeid) { sqlDbQuery('DELETE FROM meshcentral.power WHERE nodeid = ?', [nodeid], function (err, docs) { }); };
995 +
996 + // Database actions on the SMBIOS collection
997 + obj.GetAllSMBIOS = function (func) { sqlDbQuery('SELECT doc FROM meshcentral.smbios', null, func); };
998 + obj.SetSMBIOS = function (smbios, func) { var expire = new Date(smbios.time); expire.setMonth(expire.getMonth() + 6); sqlDbQuery('REPLACE INTO meshcentral.smbios VALUE (?, ?, ?, ?)', [smbios._id, smbios.time, expire, JSON.stringify(smbios)], func); };
999 + obj.RemoveSMBIOS = function (id) { sqlDbQuery('DELETE FROM meshcentral.smbios WHERE id = ?', [id], function (err, docs) { }); };
1000 + obj.GetSMBIOS = function (id, func) { sqlDbQuery('SELECT doc FROM meshcentral.smbios WHERE id = ?', [id], func); };
1001 +
1002 + // Database actions on the Server Stats collection
1003 + obj.SetServerStats = function (data, func) { sqlDbQuery('REPLACE INTO meshcentral.serverstats VALUE (?, ?, ?)', [data.time, data.expire, JSON.stringify(data)], func); };
1004 + obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); sqlDbQuery('SELECT doc FROM meshcentral.main WHERE time < ?', [t], func); }; // TODO: Expire old entries
1005 +
1006 + // Read a configuration file from the database
1007 + obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
1008 +
1009 + // Write a configuration file to the database
1010 + obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
1011 +
1012 + // List all configuration files
1013 + obj.listConfigFiles = function (func) { sqlDbQuery('SELECT doc FROM meshcentral.main WHERE type = "cfile" ORDER BY id', func); }
1014 +
1015 + // Get all configuration files
1016 + obj.getAllConfigFiles = function (password, func) {
1017 + obj.file.find({ type: 'cfile' }).toArray(function (err, docs) {
1018 + if (err != null) { func(null); return; }
1019 + var r = null;
1020 + for (var i = 0; i < docs.length; i++) {
1021 + var name = docs[i]._id.split('/')[1];
1022 + var data = obj.decryptData(password, docs[i].data);
1023 + if (data != null) { if (r == null) { r = {}; } r[name] = data; }
1024 + }
1025 + func(r);
1026 + });
1027 + }
1028 +
1029 + // Get database information (TODO: Complete this)
1030 + obj.getDbStats = function (func) {
1031 + obj.stats = { c: 4 };
1032 + sqlDbExec('SELECT COUNT(id) FROM meshcentral.main', null, function (err, response) { obj.stats.meshcentral = response['COUNT(id)']; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
1033 + sqlDbExec('SELECT COUNT(time) FROM meshcentral.serverstats', null, function (err, response) { obj.stats.serverstats = response['COUNT(time)']; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
1034 + sqlDbExec('SELECT COUNT(id) FROM meshcentral.power', null, function (err, response) { obj.stats.power = response['COUNT(id)']; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
1035 + sqlDbExec('SELECT COUNT(id) FROM meshcentral.smbios', null, function (err, response) { obj.stats.smbios = response['COUNT(id)']; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
1036 + }
1037 +
1038 + // Plugin operations
1039 + if (obj.pluginsActive) {
1040 + obj.addPlugin = function (plugin, func) { sqlDbQuery('INSERT INTO meshcentral.plugin VALUE (?, ?)', [null, JSON.stringify(value)], func); }; // Add a plugin
1041 + obj.getPlugins = function (func) { sqlDbQuery('SELECT doc FROM meshcentral.plugin', null, func); }; // Get all plugins
1042 + obj.getPlugin = function (id, func) { sqlDbQuery('SELECT doc FROM meshcentral.plugin WHERE id = ?', [id], func); }; // Get plugin
1043 + obj.deletePlugin = function (id, func) { sqlDbQuery('DELETE FROM meshcentral.plugin WHERE id = ?', [id], func); }; // Delete plugin
1044 + obj.setPluginStatus = function (id, status, func) { obj.getPlugin(id, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].status = status; obj.updatePlugin(id, docs[0], func); } }); };
1045 + obj.updatePlugin = function (id, args, func) { delete args._id; sqlDbQuery('REPLACE INTO meshcentral.plugin VALUE (?, ?)', [id, JSON.stringify(args)], func); };
1046 + }
1047 + } else if (obj.databaseType == 3) {
1048 + // Database actions on the main collection (MongoDB)
1049 + obj.Set = function (data, func) { // Fast Set operation using bulkWrite(), this is much faster then using replaceOne()
1050 + if (obj.filePendingSet == false) {
1051 + // Perform the operation now
1052 + obj.filePendingSet = true; obj.filePendingSets = null;
1053 + if (func != null) { obj.filePendingCbs = [func]; }
1054 + obj.file.bulkWrite([{ replaceOne: { filter: { _id: data._id }, replacement: performTypedRecordEncrypt(common.escapeLinksFieldNameEx(data)), upsert: true } }], fileBulkWriteCompleted);
1055 + } else {
1056 + // Add this operation to the pending list
1057 + if (obj.filePendingSets == null) { obj.filePendingSets = {} }
1058 + obj.filePendingSets[data._id] = data;
1059 + if (func != null) { if (obj.filePendingCb == null) { obj.filePendingCb = [ func ]; } else { obj.filePendingCb.push(func); } }
1060 + }
1061 + };
1062 +
1063 + /*
1064 + obj.Get = function (id, func) {
1065 + if (arguments.length > 2) {
1066 + var parms = [func];
1067 + for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
1068 + var func2 = function _func2(arg1, arg2) {
1069 + var userCallback = _func2.userArgs.shift();
1070 + _func2.userArgs.unshift(arg2);
1071 + _func2.userArgs.unshift(arg1);
1072 + userCallback.apply(obj, _func2.userArgs);
1073 + };
1074 + func2.userArgs = parms;
1075 + obj.file.find({ _id: id }).toArray(function (err, docs) {
1076 + if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
1077 + func2(err, performTypedRecordDecrypt(docs));
1078 + });
1079 + } else {
1080 + obj.file.find({ _id: id }).toArray(function (err, docs) {
1081 + if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
1082 + func(err, performTypedRecordDecrypt(docs));
1083 + });
1084 + }
1085 + };
1086 + */
1087 +
1088 + obj.Get = function (id, func) { // Fast Get operation using a bulk find() to reduce round trips to the database.
1089 + // Encode arguments into return function if any are present.
1090 + var func2 = func;
1091 + if (arguments.length > 2) {
1092 + var parms = [func];
1093 + for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
1094 + var func2 = function _func2(arg1, arg2) {
1095 + var userCallback = _func2.userArgs.shift();
1096 + _func2.userArgs.unshift(arg2);
1097 + _func2.userArgs.unshift(arg1);
1098 + userCallback.apply(obj, _func2.userArgs);
1099 + };
1100 + func2.userArgs = parms;
1101 + }
1102 +
1103 + if (obj.filePendingGets == null) {
1104 + // No pending gets, perform the operation now.
1105 + obj.filePendingGets = {};
1106 + obj.filePendingGets[id] = [func2];
1107 + obj.file.find({ _id: id }).toArray(fileBulkReadCompleted);
1108 + } else {
1109 + // Add get to pending list.
1110 + if (obj.filePendingGet == null) { obj.filePendingGet = {}; }
1111 + if (obj.filePendingGet[id] == null) { obj.filePendingGet[id] = [func2]; } else { obj.filePendingGet[id].push(func2); }
1112 + }
1113 + };
1114 +
1115 + obj.GetAll = function (func) { obj.file.find({}).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
1116 + obj.GetHash = function (id, func) { obj.file.find({ _id: id }).project({ _id: 0, hash: 1 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
1117 + obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }).project({ type: 0 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
1118 + obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
1119 + if (extrasids == null) {
1120 + var x = { type: type, domain: domain, meshid: { $in: meshes } };
1121 + if (id) { x._id = id; }
1122 + obj.file.find(x, { type: 0 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
1123 + } else {
1124 + var x = { type: type, domain: domain, $or: [ { meshid: { $in: meshes } }, { _id: { $in: extrasids } } ] };
1125 + if (id) { x._id = id; }
1126 + obj.file.find(x, { type: 0 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
1127 + }
1128 + };
1129 + obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
1130 + var x = { type: type, domain: domain, nodeid: { $in: nodes } };
1131 + if (id) { x._id = id; }
1132 + obj.file.find(x, { type: 0 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
1133 + };
1134 + obj.GetAllType = function (type, func) { obj.file.find({ type: type }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
1135 + obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.find({ type: type, domain: domain, _id: { $in: ids } }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
1136 + obj.GetUserWithEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
1137 + obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email, emailVerified: true }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
1138 +
1139 + obj.Remove = function (id, func) { // Fast remove operation using a bulk find() to reduce round trips to the database.
1140 + if (obj.filePendingRemoves == null) {
1141 + // No pending gets, perform the operation now.
1142 + obj.filePendingRemoves = {};
1143 + obj.filePendingRemoves[id] = [func];
1144 + obj.file.deleteOne({ _id: id }, fileBulkRemoveCompleted);
1145 + } else {
1146 + // Add remove to pending list.
1147 + if (obj.filePendingRemove == null) { obj.filePendingRemove = {}; }
1148 + if (obj.filePendingRemove[id] == null) { obj.filePendingRemove[id] = [func]; } else { obj.filePendingRemove[id].push(func); }
1149 + }
1150 + };
1151 +
1152 + obj.RemoveAll = function (func) { obj.file.deleteMany({}, { multi: true }, func); };
1153 + obj.RemoveAllOfType = function (type, func) { obj.file.deleteMany({ type: type }, { multi: true }, func); };
1154 + obj.InsertMany = function (data, func) { obj.file.insertMany(data, func); };
1155 + obj.RemoveMeshDocuments = function (id) { obj.file.deleteMany({ meshid: id }, { multi: true }); obj.file.deleteOne({ _id: 'nt' + id }); };
1156 + obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
1157 + obj.DeleteDomain = function (domain, func) { obj.file.deleteMany({ domain: domain }, { multi: true }, func); };
1158 + obj.SetUser = function (user) { if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
1159 + obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
1160 + obj.getLocalAmtNodes = function (func) { obj.file.find({ type: 'node', host: { $exists: true, $ne: null }, intelamt: { $exists: true } }).toArray(func); }; // TODO: This query is not optimized, but local mode only.
1161 + obj.getAmtUuidMeshNode = function (meshid, uuid, func) { obj.file.find({ type: 'node', meshid: meshid, 'intelamt.uuid': uuid }).toArray(func); };
1162 +
1163 + // TODO: Starting in MongoDB 4.0.3, you should use countDocuments() instead of count() that is deprecated. We should detect MongoDB version and switch.
1164 + // https://docs.mongodb.com/manual/reference/method/db.collection.countDocuments/
1165 + //obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { obj.file.countDocuments({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max)); }); } }
1166 + obj.isMaxType = function (max, type, domainid, func) {
1167 + if (obj.file.countDocuments) {
1168 + if (max == null) { func(false); } else { obj.file.countDocuments({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); }
1169 + } else {
1170 + if (max == null) { func(false); } else { obj.file.count({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); }
1171 + }
1172 + }
1173 +
1174 + // Database actions on the events collection
1175 + obj.GetAllEvents = function (func) { obj.eventsfile.find({}).toArray(func); };
1176 + obj.StoreEvent = function (event, func) { // Fast MongoDB event store using bulkWrite()
1177 + if (obj.eventsFilePendingSet == false) {
1178 + // Perform the operation now
1179 + obj.eventsFilePendingSet = true; obj.eventsFilePendingSets = null;
1180 + if (func != null) { obj.eventsFilePendingCbs = [func]; }
1181 + obj.eventsfile.bulkWrite([{ insertOne: { document: event } }], eventsFileBulkWriteCompleted);
1182 + } else {
1183 + // Add this operation to the pending list
1184 + if (obj.eventsFilePendingSets == null) { obj.eventsFilePendingSets = [] }
1185 + obj.eventsFilePendingSets.push(event);
1186 + if (func != null) { if (obj.eventsFilePendingCb == null) { obj.eventsFilePendingCb = [func]; } else { obj.eventsFilePendingCb.push(func); } }
1187 + }
1188 + };
1189 + obj.GetEvents = function (ids, domain, func) { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).toArray(func); };
1190 + obj.GetEventsWithLimit = function (ids, domain, limit, func) { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).toArray(func); };
1191 + obj.GetUserEvents = function (ids, domain, username, func) { obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { username: username }] }).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).toArray(func); };
1192 + obj.GetUserEventsWithLimit = function (ids, domain, username, limit, func) { obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { username: username }] }).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).toArray(func); };
1193 + obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, func) { obj.eventsfile.find({ domain: domain, nodeid: nodeid }).project({ type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).toArray(func); };
1194 + obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, func) { obj.eventsfile.find({ domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } }).project({ type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).toArray(func); };
1195 + obj.RemoveAllEvents = function (domain) { obj.eventsfile.deleteMany({ domain: domain }, { multi: true }); };
1196 + obj.RemoveAllNodeEvents = function (domain, nodeid) { obj.eventsfile.deleteMany({ domain: domain, nodeid: nodeid }, { multi: true }); };
1197 + obj.RemoveAllUserEvents = function (domain, userid) { obj.eventsfile.deleteMany({ domain: domain, userid: userid }, { multi: true }); };
1198 + obj.GetFailedLoginCount = function (username, domainid, lastlogin, func) {
1199 + if (obj.eventsfile.countDocuments) {
1200 + obj.eventsfile.countDocuments({ action: 'authfail', username: username, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null) ? count : 0); });
1201 + } else {
1202 + obj.eventsfile.count({ action: 'authfail', username: username, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null) ? count : 0); });
1203 + }
1204 + }
1205 +
1206 + // Database actions on the power collection
1207 + obj.getAllPower = function (func) { obj.powerfile.find({}).toArray(func); };
1208 + obj.storePowerEvent = function (event, multiServer, func) { // Fast MongoDB event store using bulkWrite()
1209 + if (multiServer != null) { event.server = multiServer.serverid; }
1210 + if (obj.powerFilePendingSet == false) {
1211 + // Perform the operation now
1212 + obj.powerFilePendingSet = true; obj.powerFilePendingSets = null;
1213 + if (func != null) { obj.powerFilePendingCbs = [func]; }
1214 + obj.powerfile.bulkWrite([{ insertOne: { document: event } }], powerFileBulkWriteCompleted);
1215 + } else {
1216 + // Add this operation to the pending list
1217 + if (obj.powerFilePendingSets == null) { obj.powerFilePendingSets = [] }
1218 + obj.powerFilePendingSets.push(event);
1219 + if (func != null) { if (obj.powerFilePendingCb == null) { obj.powerFilePendingCb = [func]; } else { obj.powerFilePendingCb.push(func); } }
1220 + }
1221 + };
1222 + obj.getPowerTimeline = function (nodeid, func) { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }).project({ _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }).toArray(func); };
1223 + obj.removeAllPowerEvents = function () { obj.powerfile.deleteMany({}, { multi: true }); };
1224 + obj.removeAllPowerEventsForNode = function (nodeid) { obj.powerfile.deleteMany({ nodeid: nodeid }, { multi: true }); };
1225 +
1226 + // Database actions on the SMBIOS collection
1227 + obj.GetAllSMBIOS = function (func) { obj.smbiosfile.find({}).toArray(func); };
1228 + obj.SetSMBIOS = function (smbios, func) { obj.smbiosfile.updateOne({ _id: smbios._id }, { $set: smbios }, { upsert: true }, func); };
1229 + obj.RemoveSMBIOS = function (id) { obj.smbiosfile.deleteOne({ _id: id }); };
1230 + obj.GetSMBIOS = function (id, func) { obj.smbiosfile.find({ _id: id }).toArray(func); };
1231 +
1232 + // Database actions on the Server Stats collection
1233 + obj.SetServerStats = function (data, func) { obj.serverstatsfile.insertOne(data, func); };
1234 + obj.GetServerStats = function (hours, func) { var t = new Date(); t.setTime(t.getTime() - (60 * 60 * 1000 * hours)); obj.serverstatsfile.find({ time: { $gt: t } }, { _id: 0, cpu: 0 }).toArray(func); };
1235 +
1236 + // Read a configuration file from the database
1237 + obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
1238 +
1239 + // Write a configuration file to the database
1240 + obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
1241 +
1242 + // List all configuration files
1243 + obj.listConfigFiles = function (func) { obj.file.find({ type: 'cfile' }).sort({ _id: 1 }).toArray(func); }
1244 +
1245 + // Get all configuration files
1246 + obj.getAllConfigFiles = function (password, func) {
1247 + obj.file.find({ type: 'cfile' }).toArray(function (err, docs) {
1248 + if (err != null) { func(null); return; }
1249 + var r = null;
1250 + for (var i = 0; i < docs.length; i++) {
1251 + var name = docs[i]._id.split('/')[1];
1252 + var data = obj.decryptData(password, docs[i].data);
1253 + if (data != null) { if (r == null) { r = {}; } r[name] = data; }
1254 + }
1255 + func(r);
1256 + });
1257 + }
1258 +
1259 + // Get database information
1260 + obj.getDbStats = function (func) {
1261 + obj.stats = { c: 6 };
1262 + obj.getStats(function (r) { obj.stats.recordTypes = r; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } })
1263 + obj.file.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
1264 + obj.eventsfile.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
1265 + obj.powerfile.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
1266 + obj.smbiosfile.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
1267 + obj.serverstatsfile.stats().then(function (stats) { obj.stats[stats.ns] = { size: stats.size, count: stats.count, avgObjSize: stats.avgObjSize, capped: stats.capped }; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } }, function () { if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
1268 + }
1269 +
1270 + // Plugin operations
1271 + if (obj.pluginsActive) {
1272 + obj.addPlugin = function (plugin, func) { plugin.type = 'plugin'; obj.pluginsfile.insertOne(plugin, func); }; // Add a plugin
1273 + obj.getPlugins = function (func) { obj.pluginsfile.find({ type: 'plugin' }).project({ type: 0 }).sort({ name: 1 }).toArray(func); }; // Get all plugins
1274 + obj.getPlugin = function (id, func) { id = require('mongodb').ObjectID(id); obj.pluginsfile.find({ _id: id }).sort({ name: 1 }).toArray(func); }; // Get plugin
1275 + obj.deletePlugin = function (id, func) { id = require('mongodb').ObjectID(id); obj.pluginsfile.deleteOne({ _id: id }, func); }; // Delete plugin
1276 + obj.setPluginStatus = function (id, status, func) { id = require('mongodb').ObjectID(id); obj.pluginsfile.updateOne({ _id: id }, { $set: { status: status } }, func); };
1277 + obj.updatePlugin = function (id, args, func) { delete args._id; id = require('mongodb').ObjectID(id); obj.pluginsfile.updateOne({ _id: id }, { $set: args }, func); };
1278 + }
1279 +
1280 + } else {
1281 + // Database actions on the main collection (NeDB and MongoJS)
1282 + obj.Set = function (data, func) { data = common.escapeLinksFieldNameEx(data); var xdata = performTypedRecordEncrypt(data); obj.file.update({ _id: xdata._id }, xdata, { upsert: true }, func); };
1283 + obj.Get = function (id, func) {
1284 + if (arguments.length > 2) {
1285 + var parms = [func];
1286 + for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
1287 + var func2 = function _func2(arg1, arg2) {
1288 + var userCallback = _func2.userArgs.shift();
1289 + _func2.userArgs.unshift(arg2);
1290 + _func2.userArgs.unshift(arg1);
1291 + userCallback.apply(obj, _func2.userArgs);
1292 + };
1293 + func2.userArgs = parms;
1294 + obj.file.find({ _id: id }, function (err, docs) {
1295 + if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
1296 + func2(err, performTypedRecordDecrypt(docs));
1297 + });
1298 + } else {
1299 + obj.file.find({ _id: id }, function (err, docs) {
1300 + if ((docs != null) && (docs.length > 0) && (docs[0].links != null)) { docs[0] = common.unEscapeLinksFieldName(docs[0]); }
1301 + func(err, performTypedRecordDecrypt(docs));
1302 + });
1303 + }
1304 + };
1305 + obj.GetAll = function (func) { obj.file.find({}, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
1306 + obj.GetHash = function (id, func) { obj.file.find({ _id: id }, { _id: 0, hash: 1 }, func); };
1307 + obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
1308 + //obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, domain, type, id, func) {
1309 + //var x = { type: type, domain: domain, meshid: { $in: meshes } };
1310 + //if (id) { x._id = id; }
1311 + //obj.file.find(x, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
1312 + //};
1313 + obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, extrasids, domain, type, id, func) {
1314 + if (extrasids == null) {
1315 + var x = { type: type, domain: domain, meshid: { $in: meshes } };
1316 + if (id) { x._id = id; }
1317 + obj.file.find(x, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
1318 + } else {
1319 + var x = { type: type, domain: domain, $or: [{ meshid: { $in: meshes } }, { _id: { $in: extrasids } }] };
1320 + if (id) { x._id = id; }
1321 + obj.file.find(x, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
1322 + }
1323 + };
1324 + obj.GetAllTypeNodeFiltered = function (nodes, domain, type, id, func) {
1325 + var x = { type: type, domain: domain, nodeid: { $in: nodes } };
1326 + if (id) { x._id = id; }
1327 + obj.file.find(x, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
1328 + };
1329 + obj.GetAllType = function (type, func) { obj.file.find({ type: type }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
1330 + obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.find({ type: type, domain: domain, _id: { $in: ids } }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
1331 + obj.GetUserWithEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email }, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
1332 + obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email, emailVerified: true }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
1333 + obj.Remove = function (id, func) { obj.file.remove({ _id: id }, func); };
1334 + obj.RemoveAll = function (func) { obj.file.remove({}, { multi: true }, func); };
1335 + obj.RemoveAllOfType = function (type, func) { obj.file.remove({ type: type }, { multi: true }, func); };
1336 + obj.InsertMany = function (data, func) { obj.file.insert(data, func); };
1337 + obj.RemoveMeshDocuments = function (id) { obj.file.remove({ meshid: id }, { multi: true }); obj.file.remove({ _id: 'nt' + id }); };
1338 + obj.MakeSiteAdmin = function (username, domain) { obj.Get('user/' + domain + '/' + username, function (err, docs) { if ((err == null) && (docs.length == 1)) { docs[0].siteadmin = 0xFFFFFFFF; obj.Set(docs[0]); } }); };
1339 + obj.DeleteDomain = function (domain, func) { obj.file.remove({ domain: domain }, { multi: true }, func); };
1340 + obj.SetUser = function (user) { if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
1341 + obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
1342 + obj.getLocalAmtNodes = function (func) { obj.file.find({ type: 'node', host: { $exists: true, $ne: null }, intelamt: { $exists: true } }, func); };
1343 + obj.getAmtUuidMeshNode = function (meshid, uuid, func) { obj.file.find({ type: 'node', meshid: meshid, 'intelamt.uuid': uuid }, func); };
1344 + obj.isMaxType = function (max, type, domainid, func) { if (max == null) { func(false); } else { obj.file.count({ type: type, domain: domainid }, function (err, count) { func((err != null) || (count > max), count); }); } }
1345 +
1346 + // Database actions on the events collection
1347 + obj.GetAllEvents = function (func) { obj.eventsfile.find({}, func); };
1348 + obj.StoreEvent = function (event, func) { obj.eventsfile.insert(event, func); };
1349 + 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); } };
1350 + 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); } };
1351 + obj.GetUserEvents = function (ids, domain, username, func) {
1352 + if (obj.databaseType == 1) {
1353 + 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);
1354 + } else {
1355 + 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);
1356 + }
1357 + };
1358 + obj.GetUserEventsWithLimit = function (ids, domain, username, limit, func) {
1359 + if (obj.databaseType == 1) {
1360 + 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);
1361 + } else {
1362 + 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);
1363 + }
1364 + };
1365 + 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); } };
1366 + obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, func) { if (obj.databaseType == 1) { obj.eventsfile.find({ domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } }, { 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); } };
1367 + obj.RemoveAllEvents = function (domain) { obj.eventsfile.remove({ domain: domain }, { multi: true }); };
1368 + obj.RemoveAllNodeEvents = function (domain, nodeid) { obj.eventsfile.remove({ domain: domain, nodeid: nodeid }, { multi: true }); };
1369 + obj.RemoveAllUserEvents = function (domain, userid) { obj.eventsfile.remove({ domain: domain, userid: userid }, { multi: true }); };
1370 + obj.GetFailedLoginCount = function (username, domainid, lastlogin, func) { obj.eventsfile.count({ action: 'authfail', username: username, domain: domainid, time: { "$gte": lastlogin } }, function (err, count) { func((err == null) ? count : 0); }); }
1371 +
1372 + // Database actions on the power collection
1373 + obj.getAllPower = function (func) { obj.powerfile.find({}, func); };
1374 + obj.storePowerEvent = function (event, multiServer, func) { if (multiServer != null) { event.server = multiServer.serverid; } obj.powerfile.insert(event, func); };
1375 + 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); } };
1376 + obj.removeAllPowerEvents = function () { obj.powerfile.remove({}, { multi: true }); };
1377 + obj.removeAllPowerEventsForNode = function (nodeid) { obj.powerfile.remove({ nodeid: nodeid }, { multi: true }); };
1378 +
1379 + // Database actions on the SMBIOS collection
1380 + if (obj.smbiosfile != null) {
1381 + obj.GetAllSMBIOS = function (func) { obj.smbiosfile.find({}, func); };
1382 + obj.SetSMBIOS = function (smbios, func) { obj.smbiosfile.update({ _id: smbios._id }, smbios, { upsert: true }, func); };
1383 + obj.RemoveSMBIOS = function (id) { obj.smbiosfile.remove({ _id: id }); };
1384 + obj.GetSMBIOS = function (id, func) { obj.smbiosfile.find({ _id: id }, func); };
1385 + }
1386 +
1387 + // Database actions on the Server Stats collection
1388 + obj.SetServerStats = function (data, func) { obj.serverstatsfile.insert(data, func); };
1389 + 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); };
1390 +
1391 + // Read a configuration file from the database
1392 + obj.getConfigFile = function (path, func) { obj.Get('cfile/' + path, func); }
1393 +
1394 + // Write a configuration file to the database
1395 + obj.setConfigFile = function (path, data, func) { obj.Set({ _id: 'cfile/' + path, type: 'cfile', data: data.toString('base64') }, func); }
1396 +
1397 + // List all configuration files
1398 + obj.listConfigFiles = function (func) { obj.file.find({ type: 'cfile' }).sort({ _id: 1 }).exec(func); }
1399 +
1400 + // Get all configuration files
1401 + obj.getAllConfigFiles = function (password, func) {
1402 + obj.file.find({ type: 'cfile' }, function (err, docs) {
1403 + if (err != null) { func(null); return; }
1404 + var r = null;
1405 + for (var i = 0; i < docs.length; i++) {
1406 + var name = docs[i]._id.split('/')[1];
1407 + var data = obj.decryptData(password, docs[i].data);
1408 + if (data != null) { if (r == null) { r = {}; } r[name] = data; }
1409 + }
1410 + func(r);
1411 + });
1412 + }
1413 +
1414 + // Get database information
1415 + obj.getDbStats = function (func) {
1416 + obj.stats = { c: 5 };
1417 + obj.getStats(function (r) { obj.stats.recordTypes = r; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } })
1418 + obj.file.count({}, function (err, count) { obj.stats.meshcentral = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
1419 + obj.eventsfile.count({}, function (err, count) { obj.stats.events = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
1420 + obj.powerfile.count({}, function (err, count) { obj.stats.power = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
1421 + obj.serverstatsfile.count({}, function (err, count) { obj.stats.serverstats = { count: count }; if (--obj.stats.c == 0) { delete obj.stats.c; func(obj.stats); } });
1422 + }
1423 +
1424 + // Plugin operations
1425 + if (obj.pluginsActive) {
1426 + obj.addPlugin = function (plugin, func) { plugin.type = 'plugin'; obj.pluginsfile.insert(plugin, func); }; // Add a plugin
1427 + obj.getPlugins = function (func) { obj.pluginsfile.find({ 'type': 'plugin' }, { 'type': 0 }).sort({ name: 1 }).exec(func); }; // Get all plugins
1428 + obj.getPlugin = function (id, func) { obj.pluginsfile.find({ _id: id }).sort({ name: 1 }).exec(func); }; // Get plugin
1429 + obj.deletePlugin = function (id, func) { obj.pluginsfile.remove({ _id: id }, func); }; // Delete plugin
1430 + obj.setPluginStatus = function (id, status, func) { obj.pluginsfile.update({ _id: id }, { $set: { status: status } }, func); };
1431 + obj.updatePlugin = function (id, args, func) { delete args._id; obj.pluginsfile.update({ _id: id }, { $set: args }, func); };
1432 + }
1433 +
1434 + }
1435 +
1436 + func(obj); // Completed function setup
1437 + }
1438 +
1439 + // Return a human readable string with current backup configuration
1440 + obj.getBackupConfig = function () {
1441 + var r = '', backupPath = parent.backuppath;
1442 + if (parent.config.settings.autobackup && parent.config.settings.autobackup.backuppath) { backupPath = parent.config.settings.autobackup.backuppath; }
1443 + const dbname = (parent.args.mongodbname) ? (parent.args.mongodbname) : 'meshcentral';
1444 + const currentDate = new Date();
1445 + const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2);
1446 + const newAutoBackupFile = 'meshcentral-autobackup-' + fileSuffix;
1447 + const newAutoBackupPath = parent.path.join(backupPath, newAutoBackupFile);
1448 +
1449 + r += 'DB Name: ' + dbname + '\r\n';
1450 + r += 'DB Type: ' + ['None', 'NeDB', 'MongoJS', 'MongoDB'][obj.databaseType] + '\r\n';
1451 + r += 'BackupPath: ' + backupPath + '\r\n';
1452 + r += 'newAutoBackupFile: ' + newAutoBackupFile + '\r\n';
1453 + r += 'newAutoBackupPath: ' + newAutoBackupPath + '\r\n';
1454 +
1455 + if (parent.config.settings.autobackup == null) {
1456 + r += 'No Settings/AutoBackup\r\n';
1457 + } else {
1458 + if (parent.config.settings.autobackup.backupintervalhours != null) {
1459 + if (typeof parent.config.settings.autobackup.backupintervalhours != 'number') { r += 'Bad backupintervalhours type\r\n'; }
1460 + else { r += 'Backup Interval (Hours): ' + parent.config.settings.autobackup.backupintervalhours + '\r\n'; }
1461 + }
1462 + if (parent.config.settings.autobackup.keeplastdaysbackup != null) {
1463 + if (typeof parent.config.settings.autobackup.keeplastdaysbackup != 'number') { r += 'Bad keeplastdaysbackup type\r\n'; }
1464 + else { r += 'Keep Last Backups (Days): ' + parent.config.settings.autobackup.keeplastdaysbackup + '\r\n'; }
1465 + }
1466 + if (parent.config.settings.autobackup.zippassword != null) {
1467 + if (typeof parent.config.settings.autobackup.zippassword != 'string') { r += 'Bad zippassword type\r\n'; }
1468 + else { r += 'ZIP Password Set\r\n'; }
1469 + }
1470 + if (parent.config.settings.autobackup.mongodumppath != null) {
1471 + if (typeof parent.config.settings.autobackup.mongodumppath != 'string') { r += 'Bad mongodumppath type\r\n'; }
1472 + else { r += 'MongoDump Path: ' + parent.config.settings.autobackup.mongodumppath + '\r\n'; }
1473 + }
1474 + }
1475 +
1476 + return r;
1477 + }
1478 +
1479 + // Check that the server is capable of performing a backup
1480 + obj.checkBackupCapability = function (func) {
1481 + if ((parent.config.settings.autobackup == null) || (parent.config.settings.autobackup == false)) { func(); }
1482 + if ((obj.databaseType == 2) || (obj.databaseType == 3)) {
1483 + // Check that we have access to MongoDump
1484 + var backupPath = parent.backuppath;
1485 + if (parent.config.settings.autobackup && parent.config.settings.autobackup.backuppath) { backupPath = parent.config.settings.autobackup.backuppath; }
1486 + var mongoDumpPath = 'mongodump';
1487 + if (parent.config.settings.autobackup && parent.config.settings.autobackup.mongodumppath) { mongoDumpPath = parent.config.settings.autobackup.mongodumppath; }
1488 + const child_process = require('child_process');
1489 + child_process.exec('"' + mongoDumpPath + '"', { cwd: backupPath }, function (error, stdout, stderr) {
1490 + try {
1491 + if ((error != null) && (error != '')) {
1492 + if (parent.platform == 'win32') {
1493 + func(1, "Unable to find mongodump.exe, MongoDB database auto-backup will not be performed.");
1494 + } else {
1495 + func(1, "Unable to find mongodump, MongoDB database auto-backup will not be performed.");
1496 + }
1497 + } else {
1498 + func();
1499 + }
1500 + } catch (ex) { console.log(ex); }
1501 + });
1502 + } else {
1503 + func();
1504 + }
1505 + }
1506 +
1507 + // MongoDB pending bulk read operation, perform fast bulk document reads.
1508 + function fileBulkReadCompleted(err, docs) {
1509 + // Send out callbacks with results
1510 + if (docs != null) {
1511 + for (var i in docs) {
1512 + if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); }
1513 + const id = docs[i]._id;
1514 + if (obj.filePendingGets[id] != null) {
1515 + for (var j in obj.filePendingGets[id]) {
1516 + if (typeof obj.filePendingGets[id][j] == 'function') { obj.filePendingGets[id][j](err, performTypedRecordDecrypt([docs[i]])); }
1517 + }
1518 + delete obj.filePendingGets[id];
1519 + }
1520 + }
1521 + }
1522 +
1523 + // If there are not results, send out a null callback
1524 + for (var i in obj.filePendingGets) { for (var j in obj.filePendingGets[i]) { obj.filePendingGets[i][j](err, []); } }
1525 +
1526 + // Move on to process any more pending get operations
1527 + obj.filePendingGets = obj.filePendingGet;
1528 + obj.filePendingGet = null;
1529 + if (obj.filePendingGets != null) {
1530 + var findlist = [];
1531 + for (var i in obj.filePendingGets) { findlist.push(i); }
1532 + obj.file.find({ _id: { $in: findlist } }).toArray(fileBulkReadCompleted);
1533 + }
1534 + }
1535 +
1536 + // MongoDB pending bulk remove operation, perform fast bulk document removes.
1537 + function fileBulkRemoveCompleted(err) {
1538 + // Send out callbacks
1539 + for (var i in obj.filePendingRemoves) {
1540 + for (var j in obj.filePendingRemoves[i]) {
1541 + if (typeof obj.filePendingRemoves[i][j] == 'function') { obj.filePendingRemoves[i][j](err); }
1542 + }
1543 + }
1544 +
1545 + // Move on to process any more pending get operations
1546 + obj.filePendingRemoves = obj.filePendingRemove;
1547 + obj.filePendingRemove = null;
1548 + if (obj.filePendingRemoves != null) {
1549 + var findlist = [], count = 0;
1550 + for (var i in obj.filePendingRemoves) { findlist.push(i); count++; }
1551 + obj.file.deleteMany({ _id: { $in: findlist } }, { multi: true }, fileBulkRemoveCompleted);
1552 + }
1553 + }
1554 +
1555 + // MongoDB pending bulk write operation, perform fast bulk document replacement.
1556 + function fileBulkWriteCompleted() {
1557 + // Callbacks
1558 + if (obj.filePendingCbs != null) {
1559 + for (var i in obj.filePendingCbs) { if (typeof obj.filePendingCbs[i] == 'function') { obj.filePendingCbs[i](); } }
1560 + obj.filePendingCbs = null;
1561 + }
1562 + if (obj.filePendingSets != null) {
1563 + // Perform pending operations
1564 + var ops = [];
1565 + obj.filePendingCbs = obj.filePendingCb;
1566 + obj.filePendingCb = null;
1567 + for (var i in obj.filePendingSets) { ops.push({ replaceOne: { filter: { _id: i }, replacement: performTypedRecordEncrypt(common.escapeLinksFieldNameEx(obj.filePendingSets[i])), upsert: true } }); }
1568 + obj.file.bulkWrite(ops, fileBulkWriteCompleted);
1569 + obj.filePendingSets = null;
1570 + } else {
1571 + // All done, no pending operations.
1572 + obj.filePendingSet = false;
1573 + }
1574 + }
1575 +
1576 + // MongoDB pending bulk write operation, perform fast bulk document replacement.
1577 + function eventsFileBulkWriteCompleted() {
1578 + // Callbacks
1579 + if (obj.eventsFilePendingCbs != null) { for (var i in obj.eventsFilePendingCbs) { obj.eventsFilePendingCbs[i](); } obj.eventsFilePendingCbs = null; }
1580 + if (obj.eventsFilePendingSets != null) {
1581 + // Perform pending operations
1582 + var ops = [];
1583 + for (var i in obj.eventsFilePendingSets) { ops.push({ document: obj.eventsFilePendingSets[i] }); }
1584 + obj.eventsFilePendingCbs = obj.eventsFilePendingCb;
1585 + obj.eventsFilePendingCb = null;
1586 + obj.eventsFilePendingSets = null;
1587 + obj.eventsfile.bulkWrite(ops, eventsFileBulkWriteCompleted);
1588 + } else {
1589 + // All done, no pending operations.
1590 + obj.eventsFilePendingSet = false;
1591 + }
1592 + }
1593 +
1594 + // MongoDB pending bulk write operation, perform fast bulk document replacement.
1595 + function powerFileBulkWriteCompleted() {
1596 + // Callbacks
1597 + if (obj.powerFilePendingCbs != null) { for (var i in obj.powerFilePendingCbs) { obj.powerFilePendingCbs[i](); } obj.powerFilePendingCbs = null; }
1598 + if (obj.powerFilePendingSets != null) {
1599 + // Perform pending operations
1600 + var ops = [];
1601 + for (var i in obj.powerFilePendingSets) { ops.push({ document: obj.powerFilePendingSets[i] }); }
1602 + obj.powerFilePendingCbs = obj.powerFilePendingCb;
1603 + obj.powerFilePendingCb = null;
1604 + obj.powerFilePendingSets = null;
1605 + obj.powerfile.bulkWrite(ops, powerFileBulkWriteCompleted);
1606 + } else {
1607 + // All done, no pending operations.
1608 + obj.powerFilePendingSet = false;
1609 + }
1610 + }
1611 +
1612 + // Perform a server backup
1613 + obj.performingBackup = false;
1614 + obj.performBackup = function (func) {
1615 + try {
1616 + if (obj.performingBackup) return 1;
1617 + obj.performingBackup = true;
1618 + //console.log('Performing backup...');
1619 +
1620 + var backupPath = parent.backuppath;
1621 + if (parent.config.settings.autobackup && parent.config.settings.autobackup.backuppath) { backupPath = parent.config.settings.autobackup.backuppath; }
1622 + try { parent.fs.mkdirSync(backupPath); } catch (e) { }
1623 + const dbname = (parent.args.mongodbname) ? (parent.args.mongodbname) : 'meshcentral';
1624 + const dburl = parent.args.mongodb;
1625 + const currentDate = new Date();
1626 + const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2);
1627 + const newAutoBackupFile = 'meshcentral-autobackup-' + fileSuffix;
1628 + const newAutoBackupPath = parent.path.join(backupPath, newAutoBackupFile);
1629 +
1630 + if ((obj.databaseType == 2) || (obj.databaseType == 3)) {
1631 + // Perform a MongoDump backup
1632 + const newBackupFile = 'mongodump-' + fileSuffix;
1633 + var newBackupPath = parent.path.join(backupPath, newBackupFile);
1634 + var mongoDumpPath = 'mongodump';
1635 + if (parent.config.settings.autobackup && parent.config.settings.autobackup.mongodumppath) { mongoDumpPath = parent.config.settings.autobackup.mongodumppath; }
1636 + const child_process = require('child_process');
1637 + var cmd = '\"' + mongoDumpPath + '\" --db=\"' + dbname + '\" --archive=\"' + newBackupPath + '.archive\"';
1638 + if (dburl) { cmd = '\"' + mongoDumpPath + '\" --uri=\"' + dburl.replace('?', '/?') + '\" --archive=\"' + newBackupPath + '.archive\"'; }
1639 + var backupProcess = child_process.exec(cmd, { cwd: backupPath }, function (error, stdout, stderr) {
1640 + try {
1641 + var mongoDumpSuccess = true;
1642 + backupProcess = null;
1643 + if ((error != null) && (error != '')) { mongoDumpSuccess = false; console.log('ERROR: Unable to perform MongoDB backup: ' + error + '\r\n'); }
1644 +
1645 + // Perform archive compression
1646 + var archiver = require('archiver');
1647 + var output = parent.fs.createWriteStream(newAutoBackupPath + '.zip');
1648 + var archive = null;
1649 + if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.zippassword == 'string')) {
1650 + try { archiver.registerFormat('zip-encrypted', require("archiver-zip-encrypted")); } catch (ex) { }
1651 + archive = archiver.create('zip-encrypted', { zlib: { level: 9 }, encryptionMethod: 'aes256', password: parent.config.settings.autobackup.zippassword });
1652 + } else {
1653 + archive = archiver('zip', { zlib: { level: 9 } });
1654 + }
1655 + output.on('close', function () {
1656 + obj.performingBackup = false;
1657 + if (func) { if (mongoDumpSuccess) { func('Auto-backup completed.'); } else { func('Auto-backup completed without mongodb database: ' + error); } }
1658 + obj.performCloudBackup(newAutoBackupPath + '.zip', func);
1659 + setTimeout(function () { try { parent.fs.unlink(newBackupPath + '.archive', function () { }); } catch (ex) { console.log(ex); } }, 5000);
1660 + });
1661 + output.on('end', function () { });
1662 + archive.on('warning', function (err) { console.log('Backup warning: ' + err); if (func) { func('Backup warning: ' + err); } });
1663 + archive.on('error', function (err) { console.log('Backup error: ' + err); if (func) { func('Backup error: ' + err); } });
1664 + archive.pipe(output);
1665 + if (mongoDumpSuccess == true) { archive.file(newBackupPath + '.archive', { name: newBackupFile + '.archive' }); }
1666 + archive.directory(parent.datapath, 'meshcentral-data');
1667 + archive.finalize();
1668 + } catch (ex) { console.log(ex); }
1669 + });
1670 + } else {
1671 + // Perform a NeDB backup
1672 + var archiver = require('archiver');
1673 + var output = parent.fs.createWriteStream(newAutoBackupPath + '.zip');
1674 + var archive = null;
1675 + if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.zippassword == 'string')) {
1676 + try { archiver.registerFormat('zip-encrypted', require("archiver-zip-encrypted")); } catch (ex) { }
1677 + archive = archiver.create('zip-encrypted', { zlib: { level: 9 }, encryptionMethod: 'aes256', password: parent.config.settings.autobackup.zippassword });
1678 + } else {
1679 + archive = archiver('zip', { zlib: { level: 9 } });
1680 + }
1681 + output.on('close', function () { obj.performingBackup = false; if (func) { func('Auto-backup completed.'); } obj.performCloudBackup(newAutoBackupPath + '.zip', func); });
1682 + output.on('end', function () { });
1683 + archive.on('warning', function (err) { console.log('Backup warning: ' + err); if (func) { func('Backup warning: ' + err); } });
1684 + archive.on('error', function (err) { console.log('Backup error: ' + err); if (func) { func('Backup error: ' + err); } });
1685 + archive.pipe(output);
1686 + archive.directory(parent.datapath, 'meshcentral-data');
1687 + archive.finalize();
1688 + }
1689 +
1690 + // Remove old backups
1691 + if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.keeplastdaysbackup == 'number')) {
1692 + var cutoffDate = new Date();
1693 + cutoffDate.setDate(cutoffDate.getDate() - parent.config.settings.autobackup.keeplastdaysbackup);
1694 + parent.fs.readdir(parent.backuppath, function (err, dir) {
1695 + try {
1696 + if ((err == null) && (dir.length > 0)) {
1697 + for (var i in dir) {
1698 + var name = dir[i];
1699 + if (name.startsWith('meshcentral-autobackup-') && name.endsWith('.zip')) {
1700 + var timex = name.substring(23, name.length - 4).split('-');
1701 + if (timex.length == 5) {
1702 + var fileDate = new Date(parseInt(timex[0]), parseInt(timex[1]) - 1, parseInt(timex[2]), parseInt(timex[3]), parseInt(timex[4]));
1703 + if (fileDate && (cutoffDate > fileDate)) { try { parent.fs.unlink(parent.path.join(parent.backuppath, name), function () { }); } catch (ex) { } }
1704 + }
1705 + }
1706 + }
1707 + }
1708 + } catch (ex) { console.log(ex); }
1709 + });
1710 + }
1711 + } catch (ex) { console.log(ex); }
1712 + return 0;
1713 + }
1714 +
1715 + // Perform cloud backup
1716 + obj.performCloudBackup = function (filename, func) {
1717 +
1718 + // WebDAV Backup
1719 + if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.webdav == 'object')) {
1720 + const xdateTimeSort = function (a, b) { if (a.xdate > b.xdate) return 1; if (a.xdate < b.xdate) return -1; return 0; }
1721 +
1722 + // Fetch the folder name
1723 + var webdavfolderName = 'MeshCentral-Backups';
1724 + if (typeof parent.config.settings.autobackup.webdav.foldername == 'string') { webdavfolderName = parent.config.settings.autobackup.webdav.foldername; }
1725 +
1726 + // Clean up our WebDAV folder
1727 + function performWebDavCleanup(client) {
1728 + if ((typeof parent.config.settings.autobackup.webdav.maxfiles == 'number') && (parent.config.settings.autobackup.webdav.maxfiles > 1)) {
1729 + var directoryItems = client.getDirectoryContents(webdavfolderName);
1730 + directoryItems.then(
1731 + function (files) {
1732 + for (var i in files) { files[i].xdate = new Date(files[i].lastmod); }
1733 + files.sort(xdateTimeSort);
1734 + while (files.length >= parent.config.settings.autobackup.webdav.maxfiles) {
1735 + client.deleteFile(files.shift().filename).then(function (state) {
1736 + if (func) { func('WebDAV file deleted.'); }
1737 + }).catch(function (err) {
1738 + if (func) { func('WebDAV (deleteFile) error: ' + err); }
1739 + });
1740 + }
1741 + }
1742 + ).catch(function (err) {
1743 + if (func) { func('WebDAV (getDirectoryContents) error: ' + err); }
1744 + });
1745 + }
1746 + }
1747 +
1748 + // Upload to the WebDAV folder
1749 + function performWebDavUpload(client, filepath) {
1750 + var fileStream = require('fs').createReadStream(filepath);
1751 + fileStream.on('close', function () { if (func) { func('WebDAV upload completed'); } })
1752 + fileStream.on('error', function (err) { if (func) { func('WebDAV (fileUpload) error: ' + err); } })
1753 + fileStream.pipe(client.createWriteStream('/' + webdavfolderName + '/' + require('path').basename(filepath)));
1754 + if (func) { func('Uploading using WebDAV...'); }
1755 + }
1756 +
1757 + if (func) { func('Attempting WebDAV upload...'); }
1758 + const { createClient } = require('webdav');
1759 + const client = createClient(parent.config.settings.autobackup.webdav.url, { username: parent.config.settings.autobackup.webdav.username, password: parent.config.settings.autobackup.webdav.password });
1760 + var directoryItems = client.getDirectoryContents('/');
1761 + directoryItems.then(
1762 + function (files) {
1763 + var folderFound = false;
1764 + for (var i in files) { if ((files[i].basename == webdavfolderName) && (files[i].type == 'directory')) { folderFound = true; } }
1765 + if (folderFound == false) {
1766 + client.createDirectory(webdavfolderName).then(function (a) {
1767 + if (a.statusText == 'Created') {
1768 + if (func) { func('WebDAV folder created'); }
1769 + performWebDavUpload(client, filename);
1770 + } else {
1771 + if (func) { func('WebDAV (createDirectory) status: ' + a.statusText); }
1772 + }
1773 + }).catch(function (err) {
1774 + if (func) { func('WebDAV (createDirectory) error: ' + err); }
1775 + });
1776 + } else {
1777 + performWebDavCleanup(client);
1778 + performWebDavUpload(client, filename);
1779 + }
1780 + }
1781 + ).catch(function (err) {
1782 + if (func) { func('WebDAV (getDirectoryContents) error: ' + err); }
1783 + });
1784 + }
1785 +
1786 + // Google Drive Backup
1787 + if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.googledrive == 'object')) {
1788 + obj.Get('GoogleDriveBackup', function (err, docs) {
1789 + if ((err != null) || (docs.length != 1) || (docs[0].state != 3)) return;
1790 + if (func) { func('Attempting Google Drive upload...'); }
1791 + const {google} = require('googleapis');
1792 + const oAuth2Client = new google.auth.OAuth2(docs[0].clientid, docs[0].clientsecret, "urn:ietf:wg:oauth:2.0:oob");
1793 + oAuth2Client.on('tokens', function (tokens) { if (tokens.refresh_token) { docs[0].token = tokens.refresh_token; parent.db.Set(docs[0]); } }); // Update the token in the database
1794 + oAuth2Client.setCredentials(docs[0].token);
1795 + const drive = google.drive({ version: 'v3', auth: oAuth2Client });
1796 + const createdTimeSort = function (a, b) { if (a.createdTime > b.createdTime) return 1; if (a.createdTime < b.createdTime) return -1; return 0; }
1797 +
1798 + // Called once we know our folder id, clean up and upload a backup.
1799 + var useGoogleDrive = function (folderid) {
1800 + // List files to see if we need to delete older ones
1801 + if (typeof parent.config.settings.autobackup.googledrive.maxfiles == 'number') {
1802 + drive.files.list({
1803 + q: 'trashed = false and \'' + folderid + '\' in parents',
1804 + fields: 'nextPageToken, files(id, name, size, createdTime)',
1805 + }, function (err, res) {
1806 + if (err) {
1807 + console.log('GoogleDrive (files.list) error: ' + err);
1808 + if (func) { func('GoogleDrive (files.list) error: ' + err); }
1809 + return;
1810 + }
1811 + // Delete any old files if more than 10 files are present in the backup folder.
1812 + res.data.files.sort(createdTimeSort);
1813 + while (res.data.files.length >= parent.config.settings.autobackup.googledrive.maxfiles) { drive.files.delete({ fileId: res.data.files.shift().id }, function (err, res) { }); }
1814 + });
1815 + }
1816 +
1817 + //console.log('Uploading...');
1818 + if (func) { func('Uploading to Google Drive...'); }
1819 +
1820 + // Upload the backup
1821 + drive.files.create({
1822 + requestBody: { name: require('path').basename(filename), mimeType: 'text/plain', parents: [folderid] },
1823 + media: { mimeType: 'application/zip', body: require('fs').createReadStream(filename) },
1824 + }, function (err, res) {
1825 + if (err) {
1826 + console.log('GoogleDrive (files.create) error: ' + err);
1827 + if (func) { func('GoogleDrive (files.create) error: ' + err); }
1828 + return;
1829 + }
1830 + //console.log('Upload done.');
1831 + if (func) { func('Google Drive upload completed.'); }
1832 + });
1833 + }
1834 +
1835 + // Fetch the folder name
1836 + var folderName = 'MeshCentral-Backups';
1837 + if (typeof parent.config.settings.autobackup.googledrive.foldername == 'string') { folderName = parent.config.settings.autobackup.googledrive.foldername; }
1838 +
1839 + // Find our backup folder, create one if needed.
1840 + drive.files.list({
1841 + q: 'mimeType = \'application/vnd.google-apps.folder\' and name=\'' + folderName + '\' and trashed = false',
1842 + fields: 'nextPageToken, files(id, name)',
1843 + }, function (err, res) {
1844 + if (err) {
1845 + console.log('GoogleDrive error: ' + err);
1846 + if (func) { func('GoogleDrive error: ' + err); }
1847 + return;
1848 + }
1849 + if (res.data.files.length == 0) {
1850 + // Create a folder
1851 + drive.files.create({ resource: { 'name': folderName, 'mimeType': 'application/vnd.google-apps.folder' }, fields: 'id' }, function (err, file) {
1852 + if (err) {
1853 + console.log('GoogleDrive (folder.create) error: ' + err);
1854 + if (func) { func('GoogleDrive (folder.create) error: ' + err); }
1855 + return;
1856 + }
1857 + useGoogleDrive(file.data.id);
1858 + });
1859 + } else { useGoogleDrive(res.data.files[0].id); }
1860 + });
1861 + });
1862 + }
1863 + }
1864 +
1865 + // Transfer NeDB data into the current database
1866 + obj.nedbtodb = function (func) {
1867 + var nedbDatastore = require('nedb');
1868 + var datastoreOptions = { filename: parent.getConfigFilePath('meshcentral.db'), autoload: true };
1869 +
1870 + // If a DB encryption key is provided, perform database encryption
1871 + if ((typeof parent.args.dbencryptkey == 'string') && (parent.args.dbencryptkey.length != 0)) {
1872 + // Hash the database password into a AES256 key and setup encryption and decryption.
1873 + var nedbKey = parent.crypto.createHash('sha384').update(parent.args.dbencryptkey).digest('raw').slice(0, 32);
1874 + datastoreOptions.afterSerialization = function (plaintext) {
1875 + const iv = parent.crypto.randomBytes(16);
1876 + const aes = parent.crypto.createCipheriv('aes-256-cbc', nedbKey, iv);
1877 + var ciphertext = aes.update(plaintext);
1878 + ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
1879 + return ciphertext.toString('base64');
1880 + }
1881 + datastoreOptions.beforeDeserialization = function (ciphertext) {
1882 + const ciphertextBytes = Buffer.from(ciphertext, 'base64');
1883 + const iv = ciphertextBytes.slice(0, 16);
1884 + const data = ciphertextBytes.slice(16);
1885 + const aes = parent.crypto.createDecipheriv('aes-256-cbc', nedbKey, iv);
1886 + var plaintextBytes = Buffer.from(aes.update(data));
1887 + plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
1888 + return plaintextBytes.toString();
1889 + }
1890 + }
1891 +
1892 + // Setup all NeDB collections
1893 + var nedbfile = new nedbDatastore(datastoreOptions);
1894 + var nedbeventsfile = new nedbDatastore({ filename: parent.getConfigFilePath('meshcentral-events.db'), autoload: true, corruptAlertThreshold: 1 });
1895 + var nedbpowerfile = new nedbDatastore({ filename: parent.getConfigFilePath('meshcentral-power.db'), autoload: true, corruptAlertThreshold: 1 });
1896 + var nedbserverstatsfile = new nedbDatastore({ filename: parent.getConfigFilePath('meshcentral-stats.db'), autoload: true, corruptAlertThreshold: 1 });
1897 +
1898 + // Transfered record counts
1899 + var normalRecordsTransferCount = 0;
1900 + var eventRecordsTransferCount = 0;
1901 + var powerRecordsTransferCount = 0;
1902 + var statsRecordsTransferCount = 0;
1903 + var pendingTransfer = 0;
1904 +
1905 + // Transfer the data from main database
1906 + nedbfile.find({}, function (err, docs) {
1907 + if ((err == null) && (docs.length > 0)) {
1908 + performTypedRecordDecrypt(docs)
1909 + for (var i in docs) {
1910 + pendingTransfer++;
1911 + normalRecordsTransferCount++;
1912 + obj.Set(common.unEscapeLinksFieldName(docs[i]), function () { pendingTransfer--; });
1913 + }
1914 + }
1915 +
1916 + // Transfer events
1917 + nedbeventsfile.find({}, function (err, docs) {
1918 + if ((err == null) && (docs.length > 0)) {
1919 + for (var i in docs) {
1920 + pendingTransfer++;
1921 + eventRecordsTransferCount++;
1922 + obj.StoreEvent(docs[i], function () { pendingTransfer--; });
1923 + }
1924 + }
1925 +
1926 + // Transfer power events
1927 + nedbpowerfile.find({}, function (err, docs) {
1928 + if ((err == null) && (docs.length > 0)) {
1929 + for (var i in docs) {
1930 + pendingTransfer++;
1931 + powerRecordsTransferCount++;
1932 + obj.storePowerEvent(docs[i], null, function () { pendingTransfer--; });
1933 + }
1934 + }
1935 +
1936 + // Transfer server stats
1937 + nedbserverstatsfile.find({}, function (err, docs) {
1938 + if ((err == null) && (docs.length > 0)) {
1939 + for (var i in docs) {
1940 + pendingTransfer++;
1941 + statsRecordsTransferCount++;
1942 + obj.SetServerStats(docs[i], function () { pendingTransfer--; });
1943 + }
1944 + }
1945 +
1946 + // Only exit when all the records are stored.
1947 + setInterval(function () {
1948 + if (pendingTransfer == 0) { func("Done. " + normalRecordsTransferCount + " record(s), " + eventRecordsTransferCount + " event(s), " + powerRecordsTransferCount + " power change(s), " + statsRecordsTransferCount + " stat(s)."); }
1949 + }, 200)
1950 + });
1951 + });
1952 + });
1953 + });
1954 + }
1955 +
1956 + function padNumber(number, digits) { return Array(Math.max(digits - String(number).length + 1, 0)).join(0) + number; }
1957 +
1958 + // Called when a node has changed
1959 + function dbNodeChange(nodeChange, added) {
1960 + common.unEscapeLinksFieldName(nodeChange.fullDocument);
1961 + const node = performTypedRecordDecrypt([nodeChange.fullDocument])[0];
1962 + if (node.intelamt != null) { // Remove the Intel AMT password and MPS password before eventing this.
1963 + if (node.intelamt.pass != null) { node.intelamt.pass = 1; }
1964 + if (node.intelamt.mpspass != null) { node.intelamt.mpspass = 1; }
1965 + }
1966 + parent.DispatchEvent(['*', node.meshid], obj, { etype: 'node', action: (added ? 'addnode' : 'changenode'), node: node, nodeid: node._id, domain: node.domain, nolog: 1 });
1967 + }
1968 +
1969 + // Called when a device group has changed
1970 + function dbMeshChange(meshChange, added) {
1971 + if (parent.webserver == null) return;
1972 + common.unEscapeLinksFieldName(meshChange.fullDocument);
1973 + const mesh = performTypedRecordDecrypt([meshChange.fullDocument])[0];
1974 +
1975 + // Update the mesh object in memory
1976 + const mmesh = parent.webserver.meshes[mesh._id];
1977 + for (var i in mesh) { mmesh[i] = mesh[i]; }
1978 + for (var i in mmesh) { if (mesh[i] == null) { delete mmesh[i]; } }
1979 +
1980 + // Send the mesh update
1981 + if (mesh.deleted) { mesh.action = 'deletemesh'; } else { mesh.action = (added ? 'createmesh' : 'meshchange'); }
1982 + mesh.meshid = mesh._id;
1983 + mesh.nolog = 1;
1984 + delete mesh.type;
1985 + delete mesh._id;
1986 + if ((mesh.amt != null) && (mesh.amt.password != null)) {
1987 + mesh.amt = Object.assign({}, mesh.amt); // Shallow clone
1988 + if (mesh.amt.password != null) { mesh.amt.password = 1; } // Remove the Intel AMT password if present
1989 + }
1990 + parent.DispatchEvent(['*', mesh.meshid], obj, mesh);
1991 + }
1992 +
1993 + // Called when a user account has changed
1994 + function dbUserChange(userChange, added) {
1995 + if (parent.webserver == null) return;
1996 + common.unEscapeLinksFieldName(userChange.fullDocument);
1997 + const user = performTypedRecordDecrypt([userChange.fullDocument])[0];
1998 +
1999 + // Update the user object in memory
2000 + const muser = parent.webserver.users[user._id];
2001 + for (var i in user) { muser[i] = user[i]; }
2002 + for (var i in muser) { if (user[i] == null) { delete muser[i]; } }
2003 +
2004 + // Send the user update
2005 + var targets = ['*', 'server-users', user._id];
2006 + if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
2007 + parent.DispatchEvent(targets, obj, { etype: 'user', username: user.name, account: parent.webserver.CloneSafeUser(user), action: (added ? 'accountcreate' : 'accountchange'), domain: user.domain, nolog: 1 });
2008 + }
2009 +
2010 + // Called when a user group has changed
2011 + function dbUGrpChange(ugrpChange, added) {
2012 + if (parent.webserver == null) return;
2013 + common.unEscapeLinksFieldName(ugrpChange.fullDocument);
2014 + const usergroup = ugrpChange.fullDocument;
2015 +
2016 + // Update the user group object in memory
2017 + const uusergroup = parent.webserver.userGroups[usergroup._id];
2018 + for (var i in usergroup) { uusergroup[i] = usergroup[i]; }
2019 + for (var i in uusergroup) { if (usergroup[i] == null) { delete uusergroup[i]; } }
2020 +
2021 + // Send the user group update
2022 + usergroup.action = (added ? 'createusergroup' : 'usergroupchange');
2023 + usergroup.ugrpid = usergroup._id;
2024 + usergroup.nolog = 1;
2025 + delete usergroup.type;
2026 + delete usergroup._id;
2027 + parent.DispatchEvent(['*', usergroup.ugrpid], obj, usergroup);
2028 + }
2029 +
2030 + return obj;
2031 +};
db.js
+5 -209
@@ -39,24 +39,6 @@ module.exports.CreateDB = function (parent, func) {
39 obj.changeStream = false;
40 obj.pluginsActive = ((parent.config) && (parent.config.settings) && (parent.config.settings.plugins != null) && (parent.config.settings.plugins != false) && ((typeof parent.config.settings.plugins != 'object') || (parent.config.settings.plugins.enabled != false)));
41
42 - // MongoDB bulk operations state
43 - obj.filePendingGet = null;
44 - obj.filePendingGets = null;
45 - obj.filePendingRemove = null;
46 - obj.filePendingRemoves = null;
47 - obj.filePendingSet = false;
48 - obj.filePendingSets = null;
49 - obj.filePendingCb = null;
50 - obj.filePendingCbs = null;
51 - obj.powerFilePendingSet = false;
52 - obj.powerFilePendingSets = null;
53 - obj.powerFilePendingCb = null;
54 - obj.powerFilePendingCbs = null;
55 - obj.eventsFilePendingSet = false;
56 - obj.eventsFilePendingSets = null;
57 - obj.eventsFilePendingCb = null;
58 - obj.eventsFilePendingCbs = null;
59 -
42 obj.SetupDatabase = function (func) {
43 // Check if the database unique identifier is present
44 // This is used to check that in server peering mode, everyone is using the same database.
@@ -1046,21 +1028,7 @@ module.exports.CreateDB = function (parent, func) {
1028 }
1029 } else if (obj.databaseType == 3) {
1030 // Database actions on the main collection (MongoDB)
1049 - obj.Set = function (data, func) { // Fast Set operation using bulkWrite(), this is much faster then using replaceOne()
1050 - if (obj.filePendingSet == false) {
1051 - // Perform the operation now
1052 - obj.filePendingSet = true; obj.filePendingSets = null;
1053 - if (func != null) { obj.filePendingCbs = [func]; }
1054 - obj.file.bulkWrite([{ replaceOne: { filter: { _id: data._id }, replacement: performTypedRecordEncrypt(common.escapeLinksFieldNameEx(data)), upsert: true } }], fileBulkWriteCompleted);
1055 - } else {
1056 - // Add this operation to the pending list
1057 - if (obj.filePendingSets == null) { obj.filePendingSets = {} }
1058 - obj.filePendingSets[data._id] = data;
1059 - if (func != null) { if (obj.filePendingCb == null) { obj.filePendingCb = [ func ]; } else { obj.filePendingCb.push(func); } }
1060 - }
1061 - };
1062 -
1063 - /*
1031 + obj.Set = function (data, func) { data = common.escapeLinksFieldNameEx(data); obj.file.replaceOne({ _id: data._id }, performTypedRecordEncrypt(data), { upsert: true }, func); };
1032 obj.Get = function (id, func) {
1033 if (arguments.length > 2) {
1034 var parms = [func];
@@ -1083,35 +1051,6 @@ module.exports.CreateDB = function (parent, func) {
1051 });
1052 }
1053 };
1086 - */
1087 -
1088 - obj.Get = function (id, func) { // Fast Get operation using a bulk find() to reduce round trips to the database.
1089 - // Encode arguments into return function if any are present.
1090 - var func2 = func;
1091 - if (arguments.length > 2) {
1092 - var parms = [func];
1093 - for (var parmx = 2; parmx < arguments.length; ++parmx) { parms.push(arguments[parmx]); }
1094 - var func2 = function _func2(arg1, arg2) {
1095 - var userCallback = _func2.userArgs.shift();
1096 - _func2.userArgs.unshift(arg2);
1097 - _func2.userArgs.unshift(arg1);
1098 - userCallback.apply(obj, _func2.userArgs);
1099 - };
1100 - func2.userArgs = parms;
1101 - }
1102 -
1103 - if (obj.filePendingGets == null) {
1104 - // No pending gets, perform the operation now.
1105 - obj.filePendingGets = {};
1106 - obj.filePendingGets[id] = [func2];
1107 - obj.file.find({ _id: id }).toArray(fileBulkReadCompleted);
1108 - } else {
1109 - // Add get to pending list.
1110 - if (obj.filePendingGet == null) { obj.filePendingGet = {}; }
1111 - if (obj.filePendingGet[id] == null) { obj.filePendingGet[id] = [func2]; } else { obj.filePendingGet[id].push(func2); }
1112 - }
1113 - };
1114 -
1054 obj.GetAll = function (func) { obj.file.find({}).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
1055 obj.GetHash = function (id, func) { obj.file.find({ _id: id }).project({ _id: 0, hash: 1 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
1056 obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }).project({ type: 0 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
@@ -1135,20 +1074,7 @@ module.exports.CreateDB = function (parent, func) {
1074 obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.find({ type: type, domain: domain, _id: { $in: ids } }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
1075 obj.GetUserWithEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
1076 obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email, emailVerified: true }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
1138 -
1139 - obj.Remove = function (id, func) { // Fast remove operation using a bulk find() to reduce round trips to the database.
1140 - if (obj.filePendingRemoves == null) {
1141 - // No pending gets, perform the operation now.
1142 - obj.filePendingRemoves = {};
1143 - obj.filePendingRemoves[id] = [func];
1144 - obj.file.deleteOne({ _id: id }, fileBulkRemoveCompleted);
1145 - } else {
1146 - // Add remove to pending list.
1147 - if (obj.filePendingRemove == null) { obj.filePendingRemove = {}; }
1148 - if (obj.filePendingRemove[id] == null) { obj.filePendingRemove[id] = [func]; } else { obj.filePendingRemove[id].push(func); }
1149 - }
1150 - };
1151 -
1077 + obj.Remove = function (id, func) { obj.file.deleteOne({ _id: id }, func); };
1078 obj.RemoveAll = function (func) { obj.file.deleteMany({}, { multi: true }, func); };
1079 obj.RemoveAllOfType = function (type, func) { obj.file.deleteMany({ type: type }, { multi: true }, func); };
1080 obj.InsertMany = function (data, func) { obj.file.insertMany(data, func); };
@@ -1157,7 +1083,7 @@ module.exports.CreateDB = function (parent, func) {
1083 obj.DeleteDomain = function (domain, func) { obj.file.deleteMany({ domain: domain }, { multi: true }, func); };
1084 obj.SetUser = function (user) { if (user.subscriptions != null) { var u = Clone(user); if (u.subscriptions) { delete u.subscriptions; } obj.Set(u); } else { obj.Set(user); } };
1085 obj.dispose = function () { for (var x in obj) { if (obj[x].close) { obj[x].close(); } delete obj[x]; } };
1160 - obj.getLocalAmtNodes = function (func) { obj.file.find({ type: 'node', host: { $exists: true, $ne: null }, intelamt: { $exists: true } }).toArray(func); }; // TODO: This query is not optimized, but local mode only.
1086 + obj.getLocalAmtNodes = function (func) { obj.file.find({ type: 'node', host: { $exists: true, $ne: null }, intelamt: { $exists: true } }).toArray(func); };
1087 obj.getAmtUuidMeshNode = function (meshid, uuid, func) { obj.file.find({ type: 'node', meshid: meshid, 'intelamt.uuid': uuid }).toArray(func); };
1088
1089 // TODO: Starting in MongoDB 4.0.3, you should use countDocuments() instead of count() that is deprecated. We should detect MongoDB version and switch.
@@ -1173,19 +1099,7 @@ module.exports.CreateDB = function (parent, func) {
1099
1100 // Database actions on the events collection
1101 obj.GetAllEvents = function (func) { obj.eventsfile.find({}).toArray(func); };
1176 - obj.StoreEvent = function (event, func) { // Fast MongoDB event store using bulkWrite()
1177 - if (obj.eventsFilePendingSet == false) {
1178 - // Perform the operation now
1179 - obj.eventsFilePendingSet = true; obj.eventsFilePendingSets = null;
1180 - if (func != null) { obj.eventsFilePendingCbs = [func]; }
1181 - obj.eventsfile.bulkWrite([{ insertOne: { document: event } }], eventsFileBulkWriteCompleted);
1182 - } else {
1183 - // Add this operation to the pending list
1184 - if (obj.eventsFilePendingSets == null) { obj.eventsFilePendingSets = [] }
1185 - obj.eventsFilePendingSets.push(event);
1186 - if (func != null) { if (obj.eventsFilePendingCb == null) { obj.eventsFilePendingCb = [func]; } else { obj.eventsFilePendingCb.push(func); } }
1187 - }
1188 - };
1102 + obj.StoreEvent = function (event, func) { obj.eventsfile.insertOne(event, func); };
1103 obj.GetEvents = function (ids, domain, func) { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).toArray(func); };
1104 obj.GetEventsWithLimit = function (ids, domain, limit, func) { obj.eventsfile.find({ domain: domain, ids: { $in: ids } }).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).toArray(func); };
1105 obj.GetUserEvents = function (ids, domain, username, func) { obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }, { username: username }] }).project({ type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).toArray(func); };
@@ -1205,20 +1119,7 @@ module.exports.CreateDB = function (parent, func) {
1119
1120 // Database actions on the power collection
1121 obj.getAllPower = function (func) { obj.powerfile.find({}).toArray(func); };
1208 - obj.storePowerEvent = function (event, multiServer, func) { // Fast MongoDB event store using bulkWrite()
1209 - if (multiServer != null) { event.server = multiServer.serverid; }
1210 - if (obj.powerFilePendingSet == false) {
1211 - // Perform the operation now
1212 - obj.powerFilePendingSet = true; obj.powerFilePendingSets = null;
1213 - if (func != null) { obj.powerFilePendingCbs = [func]; }
1214 - obj.powerfile.bulkWrite([{ insertOne: { document: event } }], powerFileBulkWriteCompleted);
1215 - } else {
1216 - // Add this operation to the pending list
1217 - if (obj.powerFilePendingSets == null) { obj.powerFilePendingSets = [] }
1218 - obj.powerFilePendingSets.push(event);
1219 - if (func != null) { if (obj.powerFilePendingCb == null) { obj.powerFilePendingCb = [func]; } else { obj.powerFilePendingCb.push(func); } }
1220 - }
1221 - };
1122 + obj.storePowerEvent = function (event, multiServer, func) { if (multiServer != null) { event.server = multiServer.serverid; } obj.powerfile.insertOne(event, func); };
1123 obj.getPowerTimeline = function (nodeid, func) { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }).project({ _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }).toArray(func); };
1124 obj.removeAllPowerEvents = function () { obj.powerfile.deleteMany({}, { multi: true }); };
1125 obj.removeAllPowerEventsForNode = function (nodeid) { obj.powerfile.deleteMany({ nodeid: nodeid }, { multi: true }); };
@@ -1504,111 +1405,6 @@ module.exports.CreateDB = function (parent, func) {
1405 }
1406 }
1407
1507 - // MongoDB pending bulk read operation, perform fast bulk document reads.
1508 - function fileBulkReadCompleted(err, docs) {
1509 - // Send out callbacks with results
1510 - if (docs != null) {
1511 - for (var i in docs) {
1512 - if (docs[i].links != null) { docs[i] = common.unEscapeLinksFieldName(docs[i]); }
1513 - const id = docs[i]._id;
1514 - if (obj.filePendingGets[id] != null) {
1515 - for (var j in obj.filePendingGets[id]) {
1516 - if (typeof obj.filePendingGets[id][j] == 'function') { obj.filePendingGets[id][j](err, performTypedRecordDecrypt([docs[i]])); }
1517 - }
1518 - delete obj.filePendingGets[id];
1519 - }
1520 - }
1521 - }
1522 -
1523 - // If there are not results, send out a null callback
1524 - for (var i in obj.filePendingGets) { for (var j in obj.filePendingGets[i]) { obj.filePendingGets[i][j](err, []); } }
1525 -
1526 - // Move on to process any more pending get operations
1527 - obj.filePendingGets = obj.filePendingGet;
1528 - obj.filePendingGet = null;
1529 - if (obj.filePendingGets != null) {
1530 - var findlist = [];
1531 - for (var i in obj.filePendingGets) { findlist.push(i); }
1532 - obj.file.find({ _id: { $in: findlist } }).toArray(fileBulkReadCompleted);
1533 - }
1534 - }
1535 -
1536 - // MongoDB pending bulk remove operation, perform fast bulk document removes.
1537 - function fileBulkRemoveCompleted(err) {
1538 - // Send out callbacks
1539 - for (var i in obj.filePendingRemoves) {
1540 - for (var j in obj.filePendingRemoves[i]) {
1541 - if (typeof obj.filePendingRemoves[i][j] == 'function') { obj.filePendingRemoves[i][j](err); }
1542 - }
1543 - }
1544 -
1545 - // Move on to process any more pending get operations
1546 - obj.filePendingRemoves = obj.filePendingRemove;
1547 - obj.filePendingRemove = null;
1548 - if (obj.filePendingRemoves != null) {
1549 - var findlist = [], count = 0;
1550 - for (var i in obj.filePendingRemoves) { findlist.push(i); count++; }
1551 - obj.file.deleteMany({ _id: { $in: findlist } }, { multi: true }, fileBulkRemoveCompleted);
1552 - }
1553 - }
1554 -
1555 - // MongoDB pending bulk write operation, perform fast bulk document replacement.
1556 - function fileBulkWriteCompleted() {
1557 - // Callbacks
1558 - if (obj.filePendingCbs != null) {
1559 - for (var i in obj.filePendingCbs) { if (typeof obj.filePendingCbs[i] == 'function') { obj.filePendingCbs[i](); } }
1560 - obj.filePendingCbs = null;
1561 - }
1562 - if (obj.filePendingSets != null) {
1563 - // Perform pending operations
1564 - var ops = [];
1565 - obj.filePendingCbs = obj.filePendingCb;
1566 - obj.filePendingCb = null;
1567 - for (var i in obj.filePendingSets) { ops.push({ replaceOne: { filter: { _id: i }, replacement: performTypedRecordEncrypt(common.escapeLinksFieldNameEx(obj.filePendingSets[i])), upsert: true } }); }
1568 - obj.file.bulkWrite(ops, fileBulkWriteCompleted);
1569 - obj.filePendingSets = null;
1570 - } else {
1571 - // All done, no pending operations.
1572 - obj.filePendingSet = false;
1573 - }
1574 - }
1575 -
1576 - // MongoDB pending bulk write operation, perform fast bulk document replacement.
1577 - function eventsFileBulkWriteCompleted() {
1578 - // Callbacks
1579 - if (obj.eventsFilePendingCbs != null) { for (var i in obj.eventsFilePendingCbs) { obj.eventsFilePendingCbs[i](); } obj.eventsFilePendingCbs = null; }
1580 - if (obj.eventsFilePendingSets != null) {
1581 - // Perform pending operations
1582 - var ops = [];
1583 - for (var i in obj.eventsFilePendingSets) { ops.push({ document: obj.eventsFilePendingSets[i] }); }
1584 - obj.eventsFilePendingCbs = obj.eventsFilePendingCb;
1585 - obj.eventsFilePendingCb = null;
1586 - obj.eventsFilePendingSets = null;
1587 - obj.eventsfile.bulkWrite(ops, eventsFileBulkWriteCompleted);
1588 - } else {
1589 - // All done, no pending operations.
1590 - obj.eventsFilePendingSet = false;
1591 - }
1592 - }
1593 -
1594 - // MongoDB pending bulk write operation, perform fast bulk document replacement.
1595 - function powerFileBulkWriteCompleted() {
1596 - // Callbacks
1597 - if (obj.powerFilePendingCbs != null) { for (var i in obj.powerFilePendingCbs) { obj.powerFilePendingCbs[i](); } obj.powerFilePendingCbs = null; }
1598 - if (obj.powerFilePendingSets != null) {
1599 - // Perform pending operations
1600 - var ops = [];
1601 - for (var i in obj.powerFilePendingSets) { ops.push({ document: obj.powerFilePendingSets[i] }); }
1602 - obj.powerFilePendingCbs = obj.powerFilePendingCb;
1603 - obj.powerFilePendingCb = null;
1604 - obj.powerFilePendingSets = null;
1605 - obj.powerfile.bulkWrite(ops, powerFileBulkWriteCompleted);
1606 - } else {
1607 - // All done, no pending operations.
1608 - obj.powerFilePendingSet = false;
1609 - }
1610 - }
1611 -
1408 // Perform a server backup
1409 obj.performingBackup = false;
1410 obj.performBackup = function (func) {
views/default.handlebars
+2 -4
@@ -9037,10 +9037,8 @@
9037 }
9038 if (hardware.agentvers != null) {
9039 if (hardware.agentvers.compileTime) {
9040 - try {
9041 - var d = Date.parse(hardware.agentvers.compileTime)
9042 - x += addDetailItem("Compile time", printDateTime(new Date(d)));
9043 - } catch (ex) {}
9040 + var d = Date.parse(hardware.agentvers.compileTime)
9041 + x += addDetailItem("Compile time", isNaN(d)?hardware.agentvers.compileTime:printDateTime(new Date(d)));
9042 }
9043 }
9044 if (x != '') { sections.push({ name: "Mesh Agent", html: x, img: 'meshagent64.png'}); }
webserver-old.js new
+6619
@@ -0,0 +1,6619 @@
1 +/**
2 +* @description MeshCentral web server
3 +* @author Ylian Saint-Hilaire
4 +* @copyright Intel Corporation 2018-2021
5 +* @license Apache-2.0
6 +* @version v0.0.1
7 +*/
8 +
9 +/*jslint node: true */
10 +/*jshint node: true */
11 +/*jshint strict:false */
12 +/*jshint -W097 */
13 +/*jshint esversion: 6 */
14 +'use strict';
15 +
16 +// SerialTunnel object is used to embed TLS within another connection.
17 +function SerialTunnel(options) {
18 + var obj = new require('stream').Duplex(options);
19 + obj.forwardwrite = null;
20 + obj.updateBuffer = function (chunk) { this.push(chunk); };
21 + obj._write = function (chunk, encoding, callback) { if (obj.forwardwrite != null) { obj.forwardwrite(chunk); } else { console.err("Failed to fwd _write."); } if (callback) callback(); }; // Pass data written to forward
22 + obj._read = function (size) { }; // Push nothing, anything to read should be pushed from updateBuffer()
23 + return obj;
24 +}
25 +
26 +// ExpressJS login sample
27 +// https://github.com/expressjs/express/blob/master/examples/auth/index.js
28 +
29 +// Polyfill startsWith/endsWith for older NodeJS
30 +if (!String.prototype.startsWith) { String.prototype.startsWith = function (searchString, position) { position = position || 0; return this.substr(position, searchString.length) === searchString; }; }
31 +if (!String.prototype.endsWith) { String.prototype.endsWith = function (searchString, position) { var subjectString = this.toString(); if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) { position = subjectString.length; } position -= searchString.length; var lastIndex = subjectString.lastIndexOf(searchString, position); return lastIndex !== -1 && lastIndex === position; }; }
32 +
33 +// Construct a HTTP server object
34 +module.exports.CreateWebServer = function (parent, db, args, certificates) {
35 + var obj = {}, i = 0;
36 +
37 + // Modules
38 + obj.fs = require('fs');
39 + obj.net = require('net');
40 + obj.tls = require('tls');
41 + obj.path = require('path');
42 + obj.bodyParser = require('body-parser');
43 + obj.session = require('cookie-session');
44 + obj.exphbs = require('express-handlebars');
45 + obj.crypto = require('crypto');
46 + obj.common = require('./common.js');
47 + obj.express = require('express');
48 + obj.meshAgentHandler = require('./meshagent.js');
49 + obj.meshRelayHandler = require('./meshrelay.js');
50 + obj.meshDeviceFileHandler = require('./meshdevicefile.js');
51 + obj.meshDesktopMultiplexHandler = require('./meshdesktopmultiplex.js');
52 + obj.meshIderHandler = require('./amt/amt-ider.js');
53 + obj.meshUserHandler = require('./meshuser.js');
54 + obj.interceptor = require('./interceptor');
55 + const constants = (obj.crypto.constants ? obj.crypto.constants : require('constants')); // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead.
56 +
57 + // Setup WebAuthn / FIDO2
58 + obj.webauthn = require('./webauthn.js').CreateWebAuthnModule();
59 +
60 + // Variables
61 + obj.args = args;
62 + obj.parent = parent;
63 + obj.filespath = parent.filespath;
64 + obj.db = db;
65 + obj.app = obj.express();
66 + if (obj.args.agentport) { obj.agentapp = obj.express(); }
67 + if (args.compression !== false) { obj.app.use(require('compression')()); }
68 + obj.app.disable('x-powered-by');
69 + obj.tlsServer = null;
70 + obj.tcpServer = null;
71 + obj.certificates = certificates;
72 + obj.users = {}; // UserID --> User
73 + obj.meshes = {}; // MeshID --> Mesh (also called device group)
74 + obj.userGroups = {}; // UGrpID --> User Group
75 + obj.userAllowedIp = args.userallowedip; // List of allowed IP addresses for users
76 + obj.agentAllowedIp = args.agentallowedip; // List of allowed IP addresses for agents
77 + obj.agentBlockedIp = args.agentblockedip; // List of blocked IP addresses for agents
78 + obj.tlsSniCredentials = null;
79 + obj.dnsDomains = {};
80 + obj.relaySessionCount = 0;
81 + obj.relaySessionErrorCount = 0;
82 + obj.blockedUsers = 0;
83 + obj.blockedAgents = 0;
84 + obj.renderPages = null;
85 + obj.renderLanguages = [];
86 +
87 + // Mesh Rights
88 + const MESHRIGHT_EDITMESH = 1;
89 + const MESHRIGHT_MANAGEUSERS = 2;
90 + const MESHRIGHT_MANAGECOMPUTERS = 4;
91 + const MESHRIGHT_REMOTECONTROL = 8;
92 + const MESHRIGHT_AGENTCONSOLE = 16;
93 + const MESHRIGHT_SERVERFILES = 32;
94 + const MESHRIGHT_WAKEDEVICE = 64;
95 + const MESHRIGHT_SETNOTES = 128;
96 +
97 + // Site rights
98 + const SITERIGHT_SERVERBACKUP = 1;
99 + const SITERIGHT_MANAGEUSERS = 2;
100 + const SITERIGHT_SERVERRESTORE = 4;
101 + const SITERIGHT_FILEACCESS = 8;
102 + const SITERIGHT_SERVERUPDATE = 16;
103 + const SITERIGHT_LOCKED = 32;
104 +
105 + // Setup SSPI authentication if needed
106 + if ((obj.parent.platform == 'win32') && (obj.args.nousers != true) && (obj.parent.config != null) && (obj.parent.config.domains != null)) {
107 + for (i in obj.parent.config.domains) { if (obj.parent.config.domains[i].auth == 'sspi') { var nodeSSPI = require('node-sspi'); obj.parent.config.domains[i].sspi = new nodeSSPI({ retrieveGroups: true, offerBasic: false }); } }
108 + }
109 +
110 + // Perform hash on web certificate and agent certificate
111 + obj.webCertificateHash = obj.defaultWebCertificateHash = parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.web.cert);
112 + obj.webCertificateHashs = { '': obj.webCertificateHash };
113 + obj.webCertificateHashBase64 = Buffer.from(obj.webCertificateHash, 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
114 + obj.webCertificateFullHash = obj.defaultWebCertificateFullHash = parent.certificateOperations.getCertHashBinary(obj.certificates.web.cert);
115 + obj.webCertificateFullHashs = { '': obj.webCertificateFullHash };
116 + obj.agentCertificateHashHex = parent.certificateOperations.getPublicKeyHash(obj.certificates.agent.cert);
117 + obj.agentCertificateHashBase64 = Buffer.from(obj.agentCertificateHashHex, 'hex').toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
118 + obj.agentCertificateAsn1 = parent.certificateOperations.forge.asn1.toDer(parent.certificateOperations.forge.pki.certificateToAsn1(parent.certificateOperations.forge.pki.certificateFromPem(parent.certificates.agent.cert))).getBytes();
119 +
120 + // Compute the hash of all of the web certificates for each domain
121 + for (var i in obj.parent.config.domains) {
122 + if (obj.parent.config.domains[i].certhash != null) {
123 + // If the web certificate hash is provided, use it.
124 + obj.webCertificateHashs[i] = obj.webCertificateFullHashs[i] = Buffer.from(obj.parent.config.domains[i].certhash, 'hex').toString('binary');
125 + if (obj.parent.config.domains[i].certkeyhash != null) { obj.webCertificateHashs[i] = Buffer.from(obj.parent.config.domains[i].certkeyhash, 'hex').toString('binary'); }
126 + } else if ((obj.parent.config.domains[i].dns != null) && (obj.parent.config.domains[i].certs != null)) {
127 + // If the domain has a different DNS name, use a different certificate hash.
128 + // Hash the full certificate
129 + obj.webCertificateFullHashs[i] = parent.certificateOperations.getCertHashBinary(obj.parent.config.domains[i].certs.cert);
130 + try {
131 + // Decode a RSA certificate and hash the public key.
132 + obj.webCertificateHashs[i] = parent.certificateOperations.getPublicKeyHashBinary(obj.parent.config.domains[i].certs.cert);
133 + } catch (ex) {
134 + // This may be a ECDSA certificate, hash the entire cert.
135 + obj.webCertificateHashs[i] = obj.webCertificateFullHashs[i];
136 + }
137 + } else if ((obj.parent.config.domains[i].dns != null) && (obj.certificates.dns[i] != null)) {
138 + // If this domain has a DNS and a matching DNS cert, use it. This case works for wildcard certs.
139 + obj.webCertificateFullHashs[i] = parent.certificateOperations.getCertHashBinary(obj.certificates.dns[i].cert);
140 + obj.webCertificateHashs[i] = parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.dns[i].cert);
141 + } else if (i != '') {
142 + // For any other domain, use the default cert.
143 + obj.webCertificateFullHashs[i] = obj.webCertificateFullHashs[''];
144 + obj.webCertificateHashs[i] = obj.webCertificateHashs[''];
145 + }
146 + }
147 +
148 + // If we are running the legacy swarm server, compute the hash for that certificate
149 + if (parent.certificates.swarmserver != null) {
150 + obj.swarmCertificateAsn1 = parent.certificateOperations.forge.asn1.toDer(parent.certificateOperations.forge.pki.certificateToAsn1(parent.certificateOperations.forge.pki.certificateFromPem(parent.certificates.swarmserver.cert))).getBytes();
151 + obj.swarmCertificateHash384 = parent.certificateOperations.forge.pki.getPublicKeyFingerprint(parent.certificateOperations.forge.pki.certificateFromPem(obj.certificates.swarmserver.cert).publicKey, { md: parent.certificateOperations.forge.md.sha384.create(), encoding: 'binary' });
152 + obj.swarmCertificateHash256 = parent.certificateOperations.forge.pki.getPublicKeyFingerprint(parent.certificateOperations.forge.pki.certificateFromPem(obj.certificates.swarmserver.cert).publicKey, { md: parent.certificateOperations.forge.md.sha256.create(), encoding: 'binary' });
153 + }
154 +
155 + // Main lists
156 + obj.wsagents = {}; // NodeId --> Agent
157 + obj.wsagentsWithBadWebCerts = {}; // NodeId --> Agent
158 + obj.wsagentsDisconnections = {};
159 + obj.wsagentsDisconnectionsTimer = null;
160 + obj.duplicateAgentsLog = {};
161 + obj.wssessions = {}; // UserId --> Array Of Sessions
162 + obj.wssessions2 = {}; // "UserId + SessionRnd" --> Session (Note that the SessionId is the UserId + / + SessionRnd)
163 + obj.wsPeerSessions = {}; // ServerId --> Array Of "UserId + SessionRnd"
164 + obj.wsPeerSessions2 = {}; // "UserId + SessionRnd" --> ServerId
165 + obj.wsPeerSessions3 = {}; // ServerId --> UserId --> [ SessionId ]
166 + obj.sessionsCount = {}; // Merged session counters, used when doing server peering. UserId --> SessionCount
167 + obj.wsrelays = {}; // Id -> Relay
168 + obj.desktoprelays = {}; // Id -> Desktop Multiplexor Relay
169 + obj.wsPeerRelays = {}; // Id -> { ServerId, Time }
170 + var tlsSessionStore = {}; // Store TLS session information for quick resume.
171 + var tlsSessionStoreCount = 0; // Number of cached TLS session information in store.
172 +
173 + // Setup randoms
174 + obj.crypto.randomBytes(48, function (err, buf) { obj.httpAuthRandom = buf; });
175 + obj.crypto.randomBytes(16, function (err, buf) { obj.httpAuthRealm = buf.toString('hex'); });
176 + obj.crypto.randomBytes(48, function (err, buf) { obj.relayRandom = buf; });
177 +
178 + // Get non-english web pages and emails
179 + getRenderList();
180 + getEmailLanguageList();
181 +
182 + // Setup DNS domain TLS SNI credentials
183 + {
184 + var dnscount = 0;
185 + obj.tlsSniCredentials = {};
186 + for (i in obj.certificates.dns) { if (obj.parent.config.domains[i].dns != null) { obj.dnsDomains[obj.parent.config.domains[i].dns.toLowerCase()] = obj.parent.config.domains[i]; obj.tlsSniCredentials[obj.parent.config.domains[i].dns] = obj.tls.createSecureContext(obj.certificates.dns[i]).context; dnscount++; } }
187 + if (dnscount > 0) { obj.tlsSniCredentials[''] = obj.tls.createSecureContext({ cert: obj.certificates.web.cert, key: obj.certificates.web.key, ca: obj.certificates.web.ca }).context; } else { obj.tlsSniCredentials = null; }
188 + }
189 + function TlsSniCallback(name, cb) {
190 + var c = obj.tlsSniCredentials[name];
191 + if (c != null) {
192 + cb(null, c);
193 + } else {
194 + cb(null, obj.tlsSniCredentials['']);
195 + }
196 + }
197 +
198 + function EscapeHtml(x) { if (typeof x == 'string') return x.replace(/&/g, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;'); if (typeof x == 'boolean') return x; if (typeof x == 'number') return x; }
199 + //function EscapeHtmlBreaks(x) { if (typeof x == "string") return x.replace(/&/g, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;').replace(/\r/g, '<br />').replace(/\n/g, '').replace(/\t/g, '&nbsp;&nbsp;'); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
200 + // Fetch all users from the database, keep this in memory
201 + obj.db.GetAllType('user', function (err, docs) {
202 + obj.common.unEscapeAllLinksFieldName(docs);
203 + var domainUserCount = {}, i = 0;
204 + for (i in parent.config.domains) { domainUserCount[i] = 0; }
205 + for (i in docs) { var u = obj.users[docs[i]._id] = docs[i]; domainUserCount[u.domain]++; }
206 + for (i in parent.config.domains) {
207 + if ((parent.config.domains[i].share == null) && (domainUserCount[i] == 0)) {
208 + // If newaccounts is set to no new accounts, but no accounts exists, temporarly allow account creation.
209 + //if ((parent.config.domains[i].newaccounts === 0) || (parent.config.domains[i].newaccounts === false)) { parent.config.domains[i].newaccounts = 2; }
210 + console.log('Server ' + ((i == '') ? '' : (i + ' ')) + 'has no users, next new account will be site administrator.');
211 + }
212 + }
213 +
214 + // Fetch all device groups (meshes) from the database, keep this in memory
215 + // As we load things in memory, we will also be doing some cleaning up.
216 + // We will not save any clean up in the database right now, instead it will be saved next time there is a change.
217 + obj.db.GetAllType('mesh', function (err, docs) {
218 + obj.common.unEscapeAllLinksFieldName(docs);
219 + for (var i in docs) { obj.meshes[docs[i]._id] = docs[i]; } // Get all meshes, including deleted ones.
220 +
221 + // Fetch all user groups from the database, keep this in memory
222 + obj.db.GetAllType('ugrp', function (err, docs) {
223 + obj.common.unEscapeAllLinksFieldName(docs);
224 +
225 + // Perform user group link cleanup
226 + for (var i in docs) {
227 + const ugrp = docs[i];
228 + if (ugrp.links != null) {
229 + for (var j in ugrp.links) {
230 + if (j.startsWith('user/') && (obj.users[j] == null)) { delete ugrp.links[j]; } // User group has a link to a user that does not exist
231 + else if (j.startsWith('mesh/') && ((obj.meshes[j] == null) || (obj.meshes[j].deleted != null))) { delete ugrp.links[j]; } // User has a link to a device group that does not exist
232 + }
233 + }
234 + obj.userGroups[docs[i]._id] = docs[i]; // Get all user groups
235 + }
236 +
237 + // Perform device group link cleanup
238 + for (var i in obj.meshes) {
239 + const mesh = obj.meshes[i];
240 + if (mesh.links != null) {
241 + for (var j in mesh.links) {
242 + if (j.startsWith('ugrp/') && (obj.userGroups[j] == null)) { delete mesh.links[j]; } // Device group has a link to a user group that does not exist
243 + else if (j.startsWith('user/') && (obj.users[j] == null)) { delete mesh.links[j]; } // Device group has a link to a user that does not exist
244 + }
245 + }
246 + }
247 +
248 + // Perform user link cleanup
249 + for (var i in obj.users) {
250 + const user = obj.users[i];
251 + if (user.links != null) {
252 + for (var j in user.links) {
253 + if (j.startsWith('ugrp/') && (obj.userGroups[j] == null)) { delete user.links[j]; } // User has a link to a user group that does not exist
254 + else if (j.startsWith('mesh/') && ((obj.meshes[j] == null) || (obj.meshes[j].deleted != null))) { delete user.links[j]; } // User has a link to a device group that does not exist
255 + //else if (j.startsWith('node/') && (obj.nodes[j] == null)) { delete user.links[j]; } // TODO
256 + }
257 + //if (Object.keys(user.links).length == 0) { delete user.links; }
258 + }
259 + }
260 +
261 + // We loaded the users, device groups and user group state, start the server
262 + serverStart();
263 + });
264 + });
265 + });
266 +
267 + // Clean up a device, used before saving it in the database
268 + obj.cleanDevice = function (device) {
269 + // Check device links, if a link points to an unknown user, remove it.
270 + if (device.links != null) {
271 + for (var j in device.links) {
272 + if ((obj.users[j] == null) && (obj.userGroups[j] == null)) {
273 + delete device.links[j];
274 + if (Object.keys(device.links).length == 0) { delete device.links; }
275 + }
276 + }
277 + }
278 + return device;
279 + }
280 +
281 + // Return statistics about this web server
282 + obj.getStats = function () {
283 + return {
284 + users: Object.keys(obj.users).length,
285 + meshes: Object.keys(obj.meshes).length,
286 + dnsDomains: Object.keys(obj.dnsDomains).length,
287 + relaySessionCount: obj.relaySessionCount,
288 + relaySessionErrorCount: obj.relaySessionErrorCount,
289 + wsagents: Object.keys(obj.wsagents).length,
290 + wsagentsDisconnections: Object.keys(obj.wsagentsDisconnections).length,
291 + wsagentsDisconnectionsTimer: Object.keys(obj.wsagentsDisconnectionsTimer).length,
292 + wssessions: Object.keys(obj.wssessions).length,
293 + wssessions2: Object.keys(obj.wssessions2).length,
294 + wsPeerSessions: Object.keys(obj.wsPeerSessions).length,
295 + wsPeerSessions2: Object.keys(obj.wsPeerSessions2).length,
296 + wsPeerSessions3: Object.keys(obj.wsPeerSessions3).length,
297 + sessionsCount: Object.keys(obj.sessionsCount).length,
298 + wsrelays: Object.keys(obj.wsrelays).length,
299 + wsPeerRelays: Object.keys(obj.wsPeerRelays).length,
300 + tlsSessionStore: Object.keys(tlsSessionStore).length,
301 + blockedUsers: obj.blockedUsers,
302 + blockedAgents: obj.blockedAgents
303 + };
304 + }
305 +
306 + // Agent counters
307 + obj.agentStats = {
308 + createMeshAgentCount: 0,
309 + agentClose: 0,
310 + agentBinaryUpdate: 0,
311 + coreIsStableCount: 0,
312 + verifiedAgentConnectionCount: 0,
313 + clearingCoreCount: 0,
314 + updatingCoreCount: 0,
315 + recoveryCoreIsStableCount: 0,
316 + meshDoesNotExistCount: 0,
317 + invalidPkcsSignatureCount: 0,
318 + invalidRsaSignatureCount: 0,
319 + invalidJsonCount: 0,
320 + unknownAgentActionCount: 0,
321 + agentBadWebCertHashCount: 0,
322 + agentBadSignature1Count: 0,
323 + agentBadSignature2Count: 0,
324 + agentMaxSessionHoldCount: 0,
325 + invalidDomainMeshCount: 0,
326 + invalidMeshTypeCount: 0,
327 + invalidDomainMesh2Count: 0,
328 + invalidMeshType2Count: 0,
329 + duplicateAgentCount: 0,
330 + maxDomainDevicesReached: 0
331 + }
332 + obj.getAgentStats = function () { return obj.agentStats; }
333 +
334 + // Authenticate the user
335 + obj.authenticate = function (name, pass, domain, fn) {
336 + if ((typeof (name) != 'string') || (typeof (pass) != 'string') || (typeof (domain) != 'object')) { fn(new Error('invalid fields')); return; }
337 + if (domain.auth == 'ldap') {
338 + if (domain.ldapoptions.url == 'test') {
339 + // Fake LDAP login
340 + var xxuser = domain.ldapoptions[name.toLowerCase()];
341 + if (xxuser == null) {
342 + fn(new Error('invalid password'));
343 + return;
344 + } else {
345 + var username = xxuser['displayName'];
346 + if (domain.ldapusername) { username = xxuser[domain.ldapusername]; }
347 + var shortname = null;
348 + if (domain.ldapuserbinarykey) {
349 + // Use a binary key as the userid
350 + if (xxuser[domain.ldapuserbinarykey]) { shortname = Buffer.from(xxuser[domain.ldapuserbinarykey], 'binary').toString('hex'); }
351 + } else if (domain.ldapuserkey) {
352 + // Use a string key as the userid
353 + if (xxuser[domain.ldapuserkey]) { shortname = xxuser[domain.ldapuserkey]; }
354 + } else {
355 + // Use the default key as the userid
356 + if (xxuser.objectSid) { shortname = Buffer.from(xxuser.objectSid, 'binary').toString('hex').toLowerCase(); }
357 + else if (xxuser.objectGUID) { shortname = Buffer.from(xxuser.objectGUID, 'binary').toString('hex').toLowerCase(); }
358 + else if (xxuser.name) { shortname = xxuser.name; }
359 + else if (xxuser.cn) { shortname = xxuser.cn; }
360 + }
361 + if (username == null) { fn(new Error('no user name')); return; }
362 + if (shortname == null) { fn(new Error('no user identifier')); return; }
363 + var userid = 'user/' + domain.id + '/' + shortname;
364 + var user = obj.users[userid];
365 + var email = null;
366 + if (domain.ldapuseremail) {
367 + email = xxuser[domain.ldapuseremail];
368 + } else if (xxuser.mail) { // use default
369 + email = xxuser.mail;
370 + }
371 + if ('[object Array]' == Object.prototype.toString.call(email)) {
372 + // mail may be multivalued in ldap in which case, answer is an array. Use the 1st value.
373 + email = email[0];
374 + }
375 + if (email) { email = email.toLowerCase(); } // it seems some code otherwhere also lowercase the emailaddress. be compatible.
376 +
377 + if (user == null) {
378 + // Create a new user
379 + var user = { type: 'user', _id: userid, name: username, creation: Math.floor(Date.now() / 1000), login: Math.floor(Date.now() / 1000), domain: domain.id };
380 + if (email) { user['email'] = email; user['emailVerified'] = true; }
381 + if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; }
382 + if (obj.common.validateStrArray(domain.newaccountrealms)) { user.groups = domain.newaccountrealms; }
383 + var usercount = 0;
384 + for (var i in obj.users) { if (obj.users[i].domain == domain.id) { usercount++; } }
385 + if (usercount == 0) { user.siteadmin = 4294967295; /*if (domain.newaccounts === 2) { delete domain.newaccounts; }*/ } // If this is the first user, give the account site admin.
386 +
387 + // Auto-join any user groups
388 + if (typeof domain.newaccountsusergroups == 'object') {
389 + for (var i in domain.newaccountsusergroups) {
390 + var ugrpid = domain.newaccountsusergroups[i];
391 + if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
392 + var ugroup = obj.userGroups[ugrpid];
393 + if (ugroup != null) {
394 + // Add group to the user
395 + if (user.links == null) { user.links = {}; }
396 + user.links[ugroup._id] = { rights: 1 };
397 +
398 + // Add user to the group
399 + ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
400 + db.Set(ugroup);
401 +
402 + // Notify user group change
403 + var event = { etype: 'ugrp', ugrpid: ugroup._id, name: ugroup.name, desc: ugroup.desc, action: 'usergroupchange', links: ugroup.links, msg: 'Added user ' + user.name + ' to user group ' + ugroup.name, addUserDomain: domain.id };
404 + if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user group. Another event will come.
405 + parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
406 + }
407 + }
408 + }
409 +
410 + obj.users[user._id] = user;
411 + obj.db.SetUser(user);
412 + var event = { etype: 'user', userid: userid, username: username, account: obj.CloneSafeUser(user), action: 'accountcreate', msg: 'Account created, name is ' + name, domain: domain.id };
413 + if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to create the user. Another event will come.
414 + obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
415 + return fn(null, user._id);
416 + } else {
417 + // This is an existing user
418 + // If the display username has changes, update it.
419 + if (user.name != username) {
420 + user.name = username;
421 + obj.db.SetUser(user);
422 + var event = { etype: 'user', userid: userid, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Changed account display name to ' + username, domain: domain.id };
423 + if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
424 + parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
425 + }
426 + // Check if user email has changed
427 + var emailreason = null;
428 + if (user.email && !email) { // email unset in ldap => unset
429 + delete user.email;
430 + delete user.emailVerified;
431 + emailreason = 'Unset email (no more email in LDAP)'
432 + } else if (user.email != email) { // update email
433 + user['email'] = email;
434 + user['emailVerified'] = true;
435 + emailreason = 'Set account email to ' + email + '. Sync with LDAP.';
436 + }
437 + if (emailreason) {
438 + obj.db.SetUser(user);
439 + var event = { etype: 'user', userid: userid, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: emailreason, domain: domain.id };
440 + if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
441 + parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
442 + }
443 + // If user is locker out, block here.
444 + if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
445 + return fn(null, user._id);
446 + }
447 + }
448 + } else {
449 + // LDAP login
450 + var LdapAuth = require('ldapauth-fork');
451 + var ldap = new LdapAuth(domain.ldapoptions);
452 + ldap.authenticate(name, pass, function (err, xxuser) {
453 + try { ldap.close(); } catch (ex) { console.log(ex); } // Close the LDAP object
454 + if (err) { fn(new Error('invalid password')); return; }
455 + var shortname = null;
456 + var email = null;
457 + if (domain.ldapuseremail) {
458 + email = xxuser[domain.ldapuseremail];
459 + } else if (xxuser.mail) {
460 + email = xxuser.mail;
461 + }
462 + if ('[object Array]' == Object.prototype.toString.call(email)) {
463 + // mail may be multivalued in ldap in which case, answer would be an array. Use the 1st one.
464 + email = email[0];
465 + }
466 + if (email) { email = email.toLowerCase(); } // it seems some code otherwhere also lowercase the emailaddress. be compatible.
467 + var username = xxuser['displayName'];
468 + if (domain.ldapusername) { username = xxuser[domain.ldapusername]; }
469 + if (domain.ldapuserbinarykey) {
470 + // Use a binary key as the userid
471 + if (xxuser[domain.ldapuserbinarykey]) { shortname = Buffer.from(xxuser[domain.ldapuserbinarykey], 'binary').toString('hex').toLowerCase(); }
472 + } else if (domain.ldapuserkey) {
473 + // Use a string key as the userid
474 + if (xxuser[domain.ldapuserkey]) { shortname = xxuser[domain.ldapuserkey]; }
475 + } else {
476 + // Use the default key as the userid
477 + if (xxuser.objectSid) { shortname = Buffer.from(xxuser.objectSid, 'binary').toString('hex').toLowerCase(); }
478 + else if (xxuser.objectGUID) { shortname = Buffer.from(xxuser.objectGUID, 'binary').toString('hex').toLowerCase(); }
479 + else if (xxuser.name) { shortname = xxuser.name; }
480 + else if (xxuser.cn) { shortname = xxuser.cn; }
481 + }
482 + if (username == null) { fn(new Error('no user name')); return; }
483 + if (shortname == null) { fn(new Error('no user identifier')); return; }
484 + var userid = 'user/' + domain.id + '/' + shortname;
485 + var user = obj.users[userid];
486 +
487 + if (user == null) {
488 + // This user does not exist, create a new account.
489 + var user = { type: 'user', _id: userid, name: shortname, creation: Math.floor(Date.now() / 1000), login: Math.floor(Date.now() / 1000), domain: domain.id };
490 + if (email) {
491 + user['email'] = email;
492 + user['emailVerified'] = true;
493 + }
494 + if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; }
495 + if (obj.common.validateStrArray(domain.newaccountrealms)) { user.groups = domain.newaccountrealms; }
496 + var usercount = 0;
497 + for (var i in obj.users) { if (obj.users[i].domain == domain.id) { usercount++; } }
498 + if (usercount == 0) { user.siteadmin = 4294967295; /*if (domain.newaccounts === 2) { delete domain.newaccounts; }*/ } // If this is the first user, give the account site admin.
499 +
500 + // Auto-join any user groups
501 + if (typeof domain.newaccountsusergroups == 'object') {
502 + for (var i in domain.newaccountsusergroups) {
503 + var ugrpid = domain.newaccountsusergroups[i];
504 + if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
505 + var ugroup = obj.userGroups[ugrpid];
506 + if (ugroup != null) {
507 + // Add group to the user
508 + if (user.links == null) { user.links = {}; }
509 + user.links[ugroup._id] = { rights: 1 };
510 +
511 + // Add user to the group
512 + ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
513 + db.Set(ugroup);
514 +
515 + // Notify user group change
516 + var event = { etype: 'ugrp', ugrpid: ugroup._id, name: ugroup.name, desc: ugroup.desc, action: 'usergroupchange', links: ugroup.links, msg: 'Added user ' + user.name + ' to user group ' + ugroup.name, addUserDomain: domain.id };
517 + if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user group. Another event will come.
518 + parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
519 + }
520 + }
521 + }
522 +
523 + obj.users[user._id] = user;
524 + obj.db.SetUser(user);
525 + var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountcreate', msg: 'Account created, name is ' + name, domain: domain.id };
526 + if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to create the user. Another event will come.
527 + obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
528 + return fn(null, user._id);
529 + } else {
530 + // This is an existing user
531 + // If the display username has changes, update it.
532 + if (user.name != username) {
533 + user.name = username;
534 + obj.db.SetUser(user);
535 + var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Changed account display name to ' + username, domain: domain.id };
536 + if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
537 + parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
538 + }
539 + // Check if user email has changed
540 + var emailreason = null;
541 + if (user.email && !email) { // email unset in ldap => unset
542 + delete user.email;
543 + delete user.emailVerified;
544 + emailreason = 'Unset email (no more email in LDAP)'
545 + } else if (user.email != email) { // update email
546 + user['email'] = email;
547 + user['emailVerified'] = true;
548 + emailreason = 'Set account email to ' + email + '. Sync with LDAP.';
549 + }
550 + if (emailreason) {
551 + obj.db.SetUser(user);
552 + var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: emailreason, domain: domain.id };
553 + if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
554 + parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
555 + }
556 + // If user is locker out, block here.
557 + if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
558 + return fn(null, user._id);
559 + }
560 + });
561 + }
562 + } else {
563 + // Regular login
564 + var user = obj.users['user/' + domain.id + '/' + name.toLowerCase()];
565 + // Query the db for the given username
566 + if (!user) { fn(new Error('cannot find user')); return; }
567 + // Apply the same algorithm to the POSTed password, applying the hash against the pass / salt, if there is a match we found the user
568 + if (user.salt == null) {
569 + fn(new Error('invalid password'));
570 + } else {
571 + if (user.passtype != null) {
572 + // IIS default clear or weak password hashing (SHA-1)
573 + require('./pass').iishash(user.passtype, pass, user.salt, function (err, hash) {
574 + if (err) return fn(err);
575 + if (hash == user.hash) {
576 + // Update the password to the stronger format.
577 + require('./pass').hash(pass, function (err, salt, hash, tag) { if (err) throw err; user.salt = salt; user.hash = hash; delete user.passtype; obj.db.SetUser(user); }, 0);
578 + if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
579 + return fn(null, user._id);
580 + }
581 + fn(new Error('invalid password'), null, user.passhint);
582 + });
583 + } else {
584 + // Default strong password hashing (pbkdf2 SHA384)
585 + require('./pass').hash(pass, user.salt, function (err, hash, tag) {
586 + if (err) return fn(err);
587 + if (hash == user.hash) {
588 + if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
589 + return fn(null, user._id);
590 + }
591 + fn(new Error('invalid password'), null, user.passhint);
592 + }, 0);
593 + }
594 + }
595 + }
596 + };
597 +
598 + /*
599 + obj.restrict = function (req, res, next) {
600 + console.log('restrict', req.url);
601 + var domain = getDomain(req);
602 + if (req.session.userid) {
603 + next();
604 + } else {
605 + req.session.messageid = 111; // Access denied.
606 + res.redirect(domain.url + 'login');
607 + }
608 + };
609 + */
610 +
611 + // Check if the source IP address is in the IP list, return false if not.
612 + function checkIpAddressEx(req, res, ipList, closeIfThis) {
613 + try {
614 + if (req.connection) {
615 + // HTTP(S) request
616 + if (req.clientIp) { for (var i = 0; i < ipList.length; i++) { if (require('ipcheck').match(req.clientIp, ipList[i])) { if (closeIfThis === true) { res.sendStatus(401); } return true; } } }
617 + if (closeIfThis === false) { res.sendStatus(401); }
618 + } else {
619 + // WebSocket request
620 + if (res.clientIp) { for (var i = 0; i < ipList.length; i++) { if (require('ipcheck').match(res.clientIp, ipList[i])) { if (closeIfThis === true) { try { req.close(); } catch (e) { } } return true; } } }
621 + if (closeIfThis === false) { try { req.close(); } catch (e) { } }
622 + }
623 + } catch (e) { console.log(e); } // Should never happen
624 + return false;
625 + }
626 +
627 + // Check if the source IP address is allowed, return domain if allowed
628 + // If there is a fail and null is returned, the request or connection is closed already.
629 + function checkUserIpAddress(req, res) {
630 + if ((parent.config.settings.userblockedip != null) && (checkIpAddressEx(req, res, parent.config.settings.userblockedip, true) == true)) { obj.blockedUsers++; return null; }
631 + if ((parent.config.settings.userallowedip != null) && (checkIpAddressEx(req, res, parent.config.settings.userallowedip, false) == false)) { obj.blockedUsers++; return null; }
632 + const domain = (req.url ? getDomain(req) : getDomain(res));
633 + if (domain == null) { parent.debug('web', 'handleRootRequest: invalid domain.'); try { res.sendStatus(404); } catch (ex) { } return; }
634 + if ((domain.userblockedip != null) && (checkIpAddressEx(req, res, domain.userblockedip, true) == true)) { obj.blockedUsers++; return null; }
635 + if ((domain.userallowedip != null) && (checkIpAddressEx(req, res, domain.userallowedip, false) == false)) { obj.blockedUsers++; return null; }
636 + return domain;
637 + }
638 +
639 + // Check if the source IP address is allowed, return domain if allowed
640 + // If there is a fail and null is returned, the request or connection is closed already.
641 + function checkAgentIpAddress(req, res) {
642 + if ((parent.config.settings.agentblockedip != null) && (checkIpAddressEx(req, res, parent.config.settings.agentblockedip, null) == true)) { obj.blockedAgents++; return null; }
643 + if ((parent.config.settings.agentallowedip != null) && (checkIpAddressEx(req, res, parent.config.settings.agentallowedip, null) == false)) { obj.blockedAgents++; return null; }
644 + const domain = (req.url ? getDomain(req) : getDomain(res));
645 + if ((domain.agentblockedip != null) && (checkIpAddressEx(req, res, domain.agentblockedip, null) == true)) { obj.blockedAgents++; return null; }
646 + if ((domain.agentallowedip != null) && (checkIpAddressEx(req, res, domain.agentallowedip, null) == false)) { obj.blockedAgents++; return null; }
647 + return domain;
648 + }
649 +
650 + // Return the current domain of the request
651 + // Request or connection says open regardless of the response
652 + function getDomain(req) {
653 + if (req.xdomain != null) { return req.xdomain; } // Domain already set for this request, return it.
654 + if (req.headers.host != null) { var d = obj.dnsDomains[req.headers.host.split(':')[0].toLowerCase()]; if (d != null) return d; } // If this is a DNS name domain, return it here.
655 + var x = req.url.split('/');
656 + if (x.length < 2) return parent.config.domains[''];
657 + var y = parent.config.domains[x[1].toLowerCase()];
658 + if ((y != null) && (y.dns == null)) { return parent.config.domains[x[1].toLowerCase()]; }
659 + return parent.config.domains[''];
660 + }
661 +
662 + function handleLogoutRequest(req, res) {
663 + const domain = checkUserIpAddress(req, res);
664 + if (domain == null) { return; }
665 + if (domain.auth == 'sspi') { parent.debug('web', 'handleLogoutRequest: failed checks.'); res.sendStatus(404); return; }
666 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
667 +
668 + res.set({ 'Cache-Control': 'no-store' });
669 + // Destroy the user's session to log them out will be re-created next request
670 + if (req.session.userid) {
671 + var user = obj.users[req.session.userid];
672 + if (user != null) { obj.parent.DispatchEvent(['*'], obj, { etype: 'user', userid: user._id, username: user.name, action: 'logout', msgid: 2, msg: 'Account logout', domain: domain.id }); }
673 + }
674 + req.session = null;
675 + if (req.query.key != null) { res.redirect(domain.url + '?key=' + req.query.key); } else { res.redirect(domain.url); }
676 + parent.debug('web', 'handleLogoutRequest: success.');
677 + }
678 +
679 + // Return true if this user has 2-step auth active
680 + function checkUserOneTimePasswordRequired(domain, user, req) {
681 + // Check if we can skip 2nd factor auth because of the source IP address
682 + if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
683 + for (var i in domain.passwordrequirements.skip2factor) { if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) return false; }
684 + }
685 +
686 + // Check if a 2nd factor cookie is present
687 + if (typeof req.headers.cookie == 'string') {
688 + const cookies = req.headers.cookie.split('; ');
689 + for (var i in cookies) {
690 + if (cookies[i].startsWith('twofactor=')) {
691 + var twoFactorCookie = obj.parent.decodeCookie(decodeURIComponent(cookies[i].substring(10)), obj.parent.loginCookieEncryptionKey, (30 * 24 * 60)); // If the cookies does not have an expire feild, assume 30 day timeout.
692 + if ((twoFactorCookie != null) && ((obj.args.cookieipcheck === false) || (twoFactorCookie.ip == null) || (twoFactorCookie.ip === req.clientIp)) && (twoFactorCookie.userid == user._id)) { return false; }
693 + }
694 + }
695 + }
696 +
697 + // See if SMS 2FA is available
698 + var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
699 +
700 + // Check if a 2nd factor is present
701 + return ((parent.config.settings.no2factorauth !== true) && (sms2fa || (user.otpsecret != null) || ((user.email != null) && (user.emailVerified == true) && (parent.mailserver != null) && (user.otpekey != null)) || ((user.otphkeys != null) && (user.otphkeys.length > 0))));
702 + }
703 +
704 + // Check the 2-step auth token
705 + function checkUserOneTimePassword(req, domain, user, token, hwtoken, func) {
706 + parent.debug('web', 'checkUserOneTimePassword()');
707 + const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.nousers !== true) && (parent.config.settings.no2factorauth !== true));
708 + if (twoStepLoginSupported == false) { parent.debug('web', 'checkUserOneTimePassword: not supported.'); func(true); return; };
709 +
710 + // Check if we can use OTP tokens with email
711 + var otpemail = (parent.mailserver != null);
712 + if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; }
713 + var otpsms = (parent.smsserver != null);
714 + if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; }
715 +
716 + // Check 2FA login cookie
717 + if ((token != null) && (token.startsWith('cookie='))) {
718 + var twoFactorCookie = obj.parent.decodeCookie(decodeURIComponent(token.substring(7)), obj.parent.loginCookieEncryptionKey, (30 * 24 * 60)); // If the cookies does not have an expire feild, assume 30 day timeout.
719 + if ((twoFactorCookie != null) && ((obj.args.cookieipcheck === false) || (twoFactorCookie.ip == null) || (twoFactorCookie.ip === req.clientIp)) && (twoFactorCookie.userid == user._id)) { func(true); return; }
720 + }
721 +
722 + // Check email key
723 + if ((otpemail) && (user.otpekey != null) && (user.otpekey.d != null) && (user.otpekey.k === token)) {
724 + var deltaTime = (Date.now() - user.otpekey.d);
725 + if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the email token (10000 * 60 * 5).
726 + user.otpekey = {};
727 + obj.db.SetUser(user);
728 + parent.debug('web', 'checkUserOneTimePassword: success (email).');
729 + func(true);
730 + return;
731 + }
732 + }
733 +
734 + // Check sms key
735 + if ((otpsms) && (user.phone != null) && (user.otpsms != null) && (user.otpsms.d != null) && (user.otpsms.k === token)) {
736 + var deltaTime = (Date.now() - user.otpsms.d);
737 + if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the SMS token (10000 * 60 * 5).
738 + delete user.otpsms;
739 + obj.db.SetUser(user);
740 + parent.debug('web', 'checkUserOneTimePassword: success (SMS).');
741 + func(true);
742 + return;
743 + }
744 + }
745 +
746 + // Check hardware key
747 + if (user.otphkeys && (user.otphkeys.length > 0) && (typeof (hwtoken) == 'string') && (hwtoken.length > 0)) {
748 + var authResponse = null;
749 + try { authResponse = JSON.parse(hwtoken); } catch (ex) { }
750 + if ((authResponse != null) && (authResponse.clientDataJSON)) {
751 + // Get all WebAuthn keys
752 + var webAuthnKeys = [];
753 + for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 3) { webAuthnKeys.push(user.otphkeys[i]); } }
754 + if (webAuthnKeys.length > 0) {
755 + // Decode authentication response
756 + var clientAssertionResponse = { response: {} };
757 + clientAssertionResponse.id = authResponse.id;
758 + clientAssertionResponse.rawId = Buffer.from(authResponse.id, 'base64');
759 + clientAssertionResponse.response.authenticatorData = Buffer.from(authResponse.authenticatorData, 'base64');
760 + clientAssertionResponse.response.clientDataJSON = Buffer.from(authResponse.clientDataJSON, 'base64');
761 + clientAssertionResponse.response.signature = Buffer.from(authResponse.signature, 'base64');
762 + clientAssertionResponse.response.userHandle = Buffer.from(authResponse.userHandle, 'base64');
763 +
764 + // Look for the key with clientAssertionResponse.id
765 + var webAuthnKey = null;
766 + for (var i = 0; i < webAuthnKeys.length; i++) { if (webAuthnKeys[i].keyId == clientAssertionResponse.id) { webAuthnKey = webAuthnKeys[i]; } }
767 +
768 + // If we found a valid key to use, let's validate the response
769 + if (webAuthnKey != null) {
770 + // Figure out the origin
771 + var httpport = ((args.aliasport != null) ? args.aliasport : args.port);
772 + var origin = 'https://' + (domain.dns ? domain.dns : parent.certificates.CommonName);
773 + if (httpport != 443) { origin += ':' + httpport; }
774 +
775 + var assertionExpectations = {
776 + challenge: req.session.u2fchallenge,
777 + origin: origin,
778 + factor: 'either',
779 + fmt: 'fido-u2f',
780 + publicKey: webAuthnKey.publicKey,
781 + prevCounter: webAuthnKey.counter,
782 + userHandle: Buffer.from(user._id, 'binary').toString('base64')
783 + };
784 +
785 + var webauthnResponse = null;
786 + try { webauthnResponse = obj.webauthn.verifyAuthenticatorAssertionResponse(clientAssertionResponse.response, assertionExpectations); } catch (ex) { parent.debug('web', 'checkUserOneTimePassword: exception ' + ex); console.log(ex); }
787 + if ((webauthnResponse != null) && (webauthnResponse.verified === true)) {
788 + // Update the hardware key counter and accept the 2nd factor
789 + webAuthnKey.counter = webauthnResponse.counter;
790 + obj.db.SetUser(user);
791 + parent.debug('web', 'checkUserOneTimePassword: success (hardware).');
792 + func(true);
793 + } else {
794 + parent.debug('web', 'checkUserOneTimePassword: fail (hardware).');
795 + func(false);
796 + }
797 + return;
798 + }
799 + }
800 + }
801 + }
802 +
803 + // Check Google Authenticator
804 + const otplib = require('otplib')
805 + otplib.authenticator.options = { window: 2 }; // Set +/- 1 minute window
806 + if (user.otpsecret && (typeof (token) == 'string') && (token.length == 6) && (otplib.authenticator.check(token, user.otpsecret) == true)) {
807 + parent.debug('web', 'checkUserOneTimePassword: success (authenticator).');
808 + func(true);
809 + return;
810 + };
811 +
812 + // Check written down keys
813 + if ((user.otpkeys != null) && (user.otpkeys.keys != null) && (typeof (token) == 'string') && (token.length == 8)) {
814 + var tokenNumber = parseInt(token);
815 + for (var i = 0; i < user.otpkeys.keys.length; i++) {
816 + if ((tokenNumber === user.otpkeys.keys[i].p) && (user.otpkeys.keys[i].u === true)) {
817 + parent.debug('web', 'checkUserOneTimePassword: success (one-time).');
818 + user.otpkeys.keys[i].u = false; func(true); return;
819 + }
820 + }
821 + }
822 +
823 + // Check OTP hardware key
824 + if ((domain.yubikey != null) && (domain.yubikey.id != null) && (domain.yubikey.secret != null) && (user.otphkeys != null) && (user.otphkeys.length > 0) && (typeof (token) == 'string') && (token.length == 44)) {
825 + var keyId = token.substring(0, 12);
826 +
827 + // Find a matching OTP key
828 + var match = false;
829 + for (var i = 0; i < user.otphkeys.length; i++) { if ((user.otphkeys[i].type === 2) && (user.otphkeys[i].keyid === keyId)) { match = true; } }
830 +
831 + // If we have a match, check the OTP
832 + if (match === true) {
833 + var yubikeyotp = require('yubikeyotp');
834 + var request = { otp: token, id: domain.yubikey.id, key: domain.yubikey.secret, timestamp: true }
835 + if (domain.yubikey.proxy) { request.requestParams = { proxy: domain.yubikey.proxy }; }
836 + yubikeyotp.verifyOTP(request, function (err, results) {
837 + if ((results != null) && (results.status == 'OK')) {
838 + parent.debug('web', 'checkUserOneTimePassword: success (Yubikey).');
839 + func(true);
840 + } else {
841 + parent.debug('web', 'checkUserOneTimePassword: fail (Yubikey).');
842 + func(false);
843 + }
844 + });
845 + return;
846 + }
847 + }
848 +
849 + parent.debug('web', 'checkUserOneTimePassword: fail (2).');
850 + func(false);
851 + }
852 +
853 + // Return a U2F hardware key challenge
854 + function getHardwareKeyChallenge(req, domain, user, func) {
855 + if (req.session.u2fchallenge) { delete req.session.u2fchallenge; };
856 + if (user.otphkeys && (user.otphkeys.length > 0)) {
857 + // Get all WebAuthn keys
858 + var webAuthnKeys = [];
859 + for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 3) { webAuthnKeys.push(user.otphkeys[i]); } }
860 + if (webAuthnKeys.length > 0) {
861 + // Generate a Webauthn challenge, this is really easy, no need to call any modules to do this.
862 + var authnOptions = { type: 'webAuthn', keyIds: [], timeout: 60000, challenge: obj.crypto.randomBytes(64).toString('base64') };
863 + for (var i = 0; i < webAuthnKeys.length; i++) { authnOptions.keyIds.push(webAuthnKeys[i].keyId); }
864 + req.session.u2fchallenge = authnOptions.challenge;
865 + parent.debug('web', 'getHardwareKeyChallenge: success');
866 + func(JSON.stringify(authnOptions));
867 + return;
868 + }
869 + }
870 + parent.debug('web', 'getHardwareKeyChallenge: fail');
871 + func('');
872 + }
873 +
874 + // Redirect a root request to a different page
875 + function handleRootRedirect(req, res, direct) {
876 + const domain = checkUserIpAddress(req, res);
877 + if (domain == null) { return; }
878 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
879 + res.redirect(domain.rootredirect + getQueryPortion(req));
880 + }
881 +
882 + function handleLoginRequest(req, res, direct) {
883 + const domain = checkUserIpAddress(req, res);
884 + if (domain == null) { return; }
885 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
886 +
887 + // Check if this is a banned ip address
888 + if (obj.checkAllowLogin(req) == false) {
889 + // Wait and redirect the user
890 + setTimeout(function () {
891 + req.session.messageid = 114; // IP address blocked, try again later.
892 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
893 + }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
894 + return;
895 + }
896 +
897 + // Normally, use the body username/password. If this is a token, use the username/password in the session.
898 + var xusername = req.body.username, xpassword = req.body.password;
899 + if ((xusername == null) && (xpassword == null) && (req.body.token != null)) { xusername = req.session.tokenusername; xpassword = req.session.tokenpassword; }
900 +
901 + // Authenticate the user
902 + obj.authenticate(xusername, xpassword, domain, function (err, userid, passhint) {
903 + if (userid) {
904 + var user = obj.users[userid];
905 +
906 + // Check if we are in maintenance mode
907 + if ((parent.config.settings.maintenancemode != null) && (user.siteadmin != 4294967295)) {
908 + req.session.messageid = 115; // Server under maintenance
909 + req.session.loginmode = '1';
910 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
911 + return;
912 + }
913 +
914 + var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (parent.mailserver != null) && (user.email != null) && (user.emailVerified == true) && (user.otpekey != null));
915 + var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
916 +
917 + // Check if this user has 2-step login active
918 + if ((req.session.loginmode != '6') && checkUserOneTimePasswordRequired(domain, user, req)) {
919 + if ((req.body.hwtoken == '**email**') && email2fa) {
920 + user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
921 + obj.db.SetUser(user);
922 + parent.debug('web', 'Sending 2FA email to: ' + user.email);
923 + parent.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key);
924 + req.session.messageid = 2; // "Email sent" message
925 + req.session.loginmode = '4';
926 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
927 + return;
928 + }
929 +
930 + if ((req.body.hwtoken == '**sms**') && sms2fa) {
931 + // Cause a token to be sent to the user's phone number
932 + user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
933 + obj.db.SetUser(user);
934 + parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
935 + parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
936 + // Ask for a login token & confirm sms was sent
937 + req.session.messageid = 4; // "SMS sent" message
938 + req.session.loginmode = '4';
939 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
940 + return;
941 + }
942 +
943 + checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result) {
944 + if (result == false) {
945 + var randomWaitTime = 0;
946 +
947 + // 2-step auth is required, but the token is not present or not valid.
948 + if ((req.body.token != null) || (req.body.hwtoken != null)) {
949 + randomWaitTime = 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095); // This is a fail, wait a random time. 2 to 6 seconds.
950 + req.session.messageid = 108; // Invalid token, try again.
951 + if (obj.parent.authlog) { obj.parent.authLog('https', 'Failed 2FA for ' + xusername + ' from ' + cleanRemoteAddr(req.clientIp) + ' port ' + req.port); }
952 + parent.debug('web', 'handleLoginRequest: invalid 2FA token');
953 + obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, { action: 'authfail', username: user.name, userid: user._id, domain: domain.id, msg: 'User login attempt with incorrect 2nd factor from ' + req.clientIp });
954 + obj.setbadLogin(req);
955 + } else {
956 + parent.debug('web', 'handleLoginRequest: 2FA token required');
957 + }
958 +
959 + // Wait and redirect the user
960 + setTimeout(function () {
961 + req.session.loginmode = '4';
962 + req.session.tokenemail = ((user.email != null) && (user.emailVerified == true) && (parent.mailserver != null) && (user.otpekey != null));
963 + req.session.tokensms = ((user.phone != null) && (parent.smsserver != null));
964 + req.session.tokenuserid = userid;
965 + req.session.tokenusername = xusername;
966 + req.session.tokenpassword = xpassword;
967 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
968 + }, randomWaitTime);
969 + } else {
970 + // Check if we need to remember this device
971 + if ((req.body.remembertoken === 'on') && ((domain.twofactorcookiedurationdays == null) || (domain.twofactorcookiedurationdays > 0))) {
972 + var maxCookieAge = domain.twofactorcookiedurationdays;
973 + if (typeof maxCookieAge != 'number') { maxCookieAge = 30; }
974 + const twoFactorCookie = obj.parent.encodeCookie({ userid: user._id, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey);
975 + res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: 'strict', secure: true });
976 + }
977 +
978 + // Check if email address needs to be confirmed
979 + var emailcheck = ((obj.parent.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap'))
980 + if (emailcheck && (user.emailVerified !== true)) {
981 + parent.debug('web', 'Redirecting using ' + user.name + ' to email check login page');
982 + req.session.messageid = 3; // "Email verification required" message
983 + req.session.loginmode = '7';
984 + req.session.passhint = user.email;
985 + req.session.cuserid = userid;
986 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
987 + return;
988 + }
989 +
990 + // Login successful
991 + if (obj.parent.authlog) { obj.parent.authLog('https', 'Accepted password for ' + xusername + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); }
992 + parent.debug('web', 'handleLoginRequest: successful 2FA login');
993 + completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct);
994 + }
995 + });
996 + return;
997 + }
998 +
999 + // Check if email address needs to be confirmed
1000 + var emailcheck = ((obj.parent.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap'))
1001 + if (emailcheck && (user.emailVerified !== true)) {
1002 + parent.debug('web', 'Redirecting using ' + user.name + ' to email check login page');
1003 + req.session.messageid = 3; // "Email verification required" message
1004 + req.session.loginmode = '7';
1005 + req.session.passhint = user.email;
1006 + req.session.cuserid = userid;
1007 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1008 + return;
1009 + }
1010 +
1011 + // Login successful
1012 + if (obj.parent.authlog) { obj.parent.authLog('https', 'Accepted password for ' + xusername + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); }
1013 + parent.debug('web', 'handleLoginRequest: successful login');
1014 + completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct);
1015 + } else {
1016 + // Login failed, log the error
1017 + if (obj.parent.authlog) { obj.parent.authLog('https', 'Failed password for ' + xusername + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); }
1018 +
1019 + // Wait a random delay
1020 + setTimeout(function () {
1021 + // If the account is locked, display that.
1022 + if (typeof xusername == 'string') {
1023 + var xuserid = 'user/' + domain.id + '/' + xusername.toLowerCase();
1024 + if (err == 'locked') {
1025 + parent.debug('web', 'handleLoginRequest: login failed, locked account');
1026 + req.session.messageid = 110; // Account locked.
1027 + obj.parent.DispatchEvent(['*', 'server-users', xuserid], obj, { action: 'authfail', userid: xuserid, username: xusername, domain: domain.id, msg: 'User login attempt on locked account from ' + req.clientIp });
1028 + obj.setbadLogin(req);
1029 + } else {
1030 + parent.debug('web', 'handleLoginRequest: login failed, bad username and password');
1031 + req.session.messageid = 112; // Login failed, check username and password.
1032 + obj.parent.DispatchEvent(['*', 'server-users', xuserid], obj, { action: 'authfail', userid: xuserid, username: xusername, domain: domain.id, msg: 'Invalid user login attempt from ' + req.clientIp });
1033 + obj.setbadLogin(req);
1034 + }
1035 + }
1036 +
1037 + // Clean up login mode and display password hint if present.
1038 + delete req.session.loginmode;
1039 + if ((passhint != null) && (passhint.length > 0)) {
1040 + req.session.passhint = passhint;
1041 + } else {
1042 + delete req.session.passhint;
1043 + }
1044 +
1045 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1046 + }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095)); // Wait for 2 to ~6 seconds.
1047 + }
1048 + });
1049 + }
1050 +
1051 + function completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct) {
1052 + // Check if we need to change the password
1053 + if ((typeof user.passchange == 'number') && ((user.passchange == -1) || ((typeof domain.passwordrequirements == 'object') && (typeof domain.passwordrequirements.reset == 'number') && (user.passchange + (domain.passwordrequirements.reset * 86400) < Math.floor(Date.now() / 1000))))) {
1054 + // Request a password change
1055 + parent.debug('web', 'handleLoginRequest: login ok, password change requested');
1056 + req.session.loginmode = '6';
1057 + req.session.messageid = 113; // Password change requested.
1058 + req.session.resettokenuserid = userid;
1059 + req.session.resettokenusername = xusername;
1060 + req.session.resettokenpassword = xpassword;
1061 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1062 + return;
1063 + }
1064 +
1065 + // Save login time
1066 + user.pastlogin = user.login;
1067 + user.login = Math.floor(Date.now() / 1000);
1068 + obj.db.SetUser(user);
1069 +
1070 + // Notify account login
1071 + var targets = ['*', 'server-users'];
1072 + if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
1073 + obj.parent.DispatchEvent(targets, obj, { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'login', msgid: 1, msg: 'Account login', domain: domain.id });
1074 +
1075 + // Regenerate session when signing in to prevent fixation
1076 + //req.session.regenerate(function () {
1077 + // Store the user's primary key in the session store to be retrieved, or in this case the entire user object
1078 + delete req.session.loginmode;
1079 + delete req.session.tokenuserid;
1080 + delete req.session.tokenusername;
1081 + delete req.session.tokenpassword;
1082 + delete req.session.tokenemail;
1083 + delete req.session.tokensms;
1084 + delete req.session.messageid;
1085 + delete req.session.passhint;
1086 + delete req.session.cuserid;
1087 + req.session.userid = userid;
1088 + req.session.domainid = domain.id;
1089 + req.session.currentNode = '';
1090 + req.session.ip = req.clientIp;
1091 + if (req.body.viewmode) { req.session.viewmode = req.body.viewmode; }
1092 + if (req.body.host) {
1093 + // TODO: This is a terrible search!!! FIX THIS.
1094 + /*
1095 + obj.db.GetAllType('node', function (err, docs) {
1096 + for (var i = 0; i < docs.length; i++) {
1097 + if (docs[i].name == req.body.host) {
1098 + req.session.currentNode = docs[i]._id;
1099 + break;
1100 + }
1101 + }
1102 + console.log("CurrentNode: " + req.session.currentNode);
1103 + // This redirect happens after finding node is completed
1104 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1105 + });
1106 + */
1107 + parent.debug('web', 'handleLoginRequest: login ok (1)');
1108 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } // Temporary
1109 + } else {
1110 + parent.debug('web', 'handleLoginRequest: login ok (2)');
1111 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1112 + }
1113 + //});
1114 + }
1115 +
1116 + function handleCreateAccountRequest(req, res, direct) {
1117 + const domain = checkUserIpAddress(req, res);
1118 + if (domain == null) { return; }
1119 + if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handleCreateAccountRequest: failed checks.'); res.sendStatus(404); return; }
1120 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1121 +
1122 + // Check if we are in maintenance mode
1123 + if (parent.config.settings.maintenancemode != null) {
1124 + req.session.messageid = 115; // Server under maintenance
1125 + req.session.loginmode = '1';
1126 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1127 + return;
1128 + }
1129 +
1130 + // Always lowercase the email address
1131 + if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
1132 +
1133 + // If the email is the username, set this here.
1134 + if (domain.usernameisemail) { req.body.username = req.body.email; }
1135 +
1136 + // Accounts that start with ~ are not allowed
1137 + if ((typeof req.body.username != 'string') || (req.body.username.length < 1) || (req.body.username[0] == '~')) {
1138 + parent.debug('web', 'handleCreateAccountRequest: unable to create account (0)');
1139 + req.session.loginmode = '2';
1140 + req.session.messageid = 100; // Unable to create account.
1141 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1142 + return;
1143 + }
1144 +
1145 + // Count the number of users in this domain
1146 + var domainUserCount = 0;
1147 + for (var i in obj.users) { if (obj.users[i].domain == domain.id) { domainUserCount++; } }
1148 +
1149 + // Check if we are allowed to create new users using the login screen
1150 + if ((domain.newaccounts !== 1) && (domain.newaccounts !== true) && (domainUserCount > 0)) {
1151 + parent.debug('web', 'handleCreateAccountRequest: domainUserCount > 1.');
1152 + res.sendStatus(401);
1153 + return;
1154 + }
1155 +
1156 + // Check if this request is for an allows email domain
1157 + if ((domain.newaccountemaildomains != null) && Array.isArray(domain.newaccountemaildomains)) {
1158 + var i = -1;
1159 + if (typeof req.body.email == 'string') { i = req.body.email.indexOf('@'); }
1160 + if (i == -1) {
1161 + parent.debug('web', 'handleCreateAccountRequest: unable to create account (1)');
1162 + req.session.loginmode = '2';
1163 + req.session.messageid = 100; // Unable to create account.
1164 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1165 + return;
1166 + }
1167 + var emailok = false, emaildomain = req.body.email.substring(i + 1).toLowerCase();
1168 + for (var i in domain.newaccountemaildomains) { if (emaildomain == domain.newaccountemaildomains[i].toLowerCase()) { emailok = true; } }
1169 + if (emailok == false) {
1170 + parent.debug('web', 'handleCreateAccountRequest: unable to create account (2)');
1171 + req.session.loginmode = '2';
1172 + req.session.messageid = 100; // Unable to create account.
1173 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1174 + return;
1175 + }
1176 + }
1177 +
1178 + // Check if we exceed the maximum number of user accounts
1179 + obj.db.isMaxType(domain.limits.maxuseraccounts, 'user', domain.id, function (maxExceed) {
1180 + if (maxExceed) {
1181 + parent.debug('web', 'handleCreateAccountRequest: account limit reached');
1182 + req.session.loginmode = '2';
1183 + req.session.messageid = 101; // Account limit reached.
1184 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1185 + } else {
1186 + if (!obj.common.validateUsername(req.body.username, 1, 64) || !obj.common.validateEmail(req.body.email, 1, 256) || !obj.common.validateString(req.body.password1, 1, 256) || !obj.common.validateString(req.body.password2, 1, 256) || (req.body.password1 != req.body.password2) || req.body.username == '~' || !obj.common.checkPasswordRequirements(req.body.password1, domain.passwordrequirements)) {
1187 + parent.debug('web', 'handleCreateAccountRequest: unable to create account (3)');
1188 + req.session.loginmode = '2';
1189 + req.session.messageid = 100; // Unable to create account.
1190 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1191 + } else {
1192 + // Check if this email was already verified
1193 + obj.db.GetUserWithVerifiedEmail(domain.id, req.body.email, function (err, docs) {
1194 + if ((docs != null) && (docs.length > 0)) {
1195 + parent.debug('web', 'handleCreateAccountRequest: Existing account with this email address');
1196 + req.session.loginmode = '2';
1197 + req.session.messageid = 102; // Existing account with this email address.
1198 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1199 + } else {
1200 + // Check if there is domain.newAccountToken, check if supplied token is valid
1201 + if ((domain.newaccountspass != null) && (domain.newaccountspass != '') && (req.body.anewaccountpass != domain.newaccountspass)) {
1202 + parent.debug('web', 'handleCreateAccountRequest: Invalid account creation token');
1203 + req.session.loginmode = '2';
1204 + req.session.messageid = 103; // Invalid account creation token.
1205 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1206 + return;
1207 + }
1208 + // Check if user exists
1209 + if (obj.users['user/' + domain.id + '/' + req.body.username.toLowerCase()]) {
1210 + parent.debug('web', 'handleCreateAccountRequest: Username already exists');
1211 + req.session.loginmode = '2';
1212 + req.session.messageid = 104; // Username already exists.
1213 + } else {
1214 + var user = { type: 'user', _id: 'user/' + domain.id + '/' + req.body.username.toLowerCase(), name: req.body.username, email: req.body.email, creation: Math.floor(Date.now() / 1000), login: Math.floor(Date.now() / 1000), domain: domain.id };
1215 + if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; }
1216 + if (obj.common.validateStrArray(domain.newaccountrealms)) { user.groups = domain.newaccountrealms; }
1217 + if ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true) && (req.body.apasswordhint)) { var hint = req.body.apasswordhint; if (hint.length > 250) { hint = hint.substring(0, 250); } user.passhint = hint; }
1218 + if (domainUserCount == 0) { user.siteadmin = 4294967295; /*if (domain.newaccounts === 2) { delete domain.newaccounts; }*/ } // If this is the first user, give the account site admin.
1219 +
1220 + // Auto-join any user groups
1221 + if (typeof domain.newaccountsusergroups == 'object') {
1222 + for (var i in domain.newaccountsusergroups) {
1223 + var ugrpid = domain.newaccountsusergroups[i];
1224 + if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
1225 + var ugroup = obj.userGroups[ugrpid];
1226 + if (ugroup != null) {
1227 + // Add group to the user
1228 + if (user.links == null) { user.links = {}; }
1229 + user.links[ugroup._id] = { rights: 1 };
1230 +
1231 + // Add user to the group
1232 + ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
1233 + db.Set(ugroup);
1234 +
1235 + // Notify user group change
1236 + var event = { etype: 'ugrp', ugrpid: ugroup._id, name: ugroup.name, desc: ugroup.desc, action: 'usergroupchange', links: ugroup.links, msg: 'Added user ' + user.name + ' to user group ' + ugroup.name, addUserDomain: domain.id };
1237 + if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user group. Another event will come.
1238 + parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
1239 + }
1240 + }
1241 + }
1242 +
1243 + obj.users[user._id] = user;
1244 + req.session.userid = user._id;
1245 + req.session.domainid = domain.id;
1246 + req.session.ip = req.clientIp; // Bind this session to the IP address of the request
1247 + // Create a user, generate a salt and hash the password
1248 + require('./pass').hash(req.body.password1, function (err, salt, hash, tag) {
1249 + if (err) throw err;
1250 + user.salt = salt;
1251 + user.hash = hash;
1252 + delete user.passtype;
1253 + obj.db.SetUser(user);
1254 +
1255 + // Send the verification email
1256 + if ((obj.parent.mailserver != null) && (domain.auth != 'sspi') && (domain.auth != 'ldap') && (obj.common.validateEmail(user.email, 1, 256) == true)) { obj.parent.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key); }
1257 + }, 0);
1258 + var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountcreate', msg: 'Account created, email is ' + req.body.email, domain: domain.id };
1259 + if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to create the user. Another event will come.
1260 + obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
1261 + }
1262 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1263 + }
1264 + });
1265 + }
1266 + }
1267 + });
1268 + }
1269 +
1270 + // Called to process an account password reset
1271 + function handleResetPasswordRequest(req, res, direct) {
1272 + const domain = checkUserIpAddress(req, res);
1273 + if (domain == null) { return; }
1274 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1275 +
1276 + // Check everything is ok
1277 + if ((domain == null) || (domain.auth == 'sspi') || (domain.auth == 'ldap') || (typeof req.body.rpassword1 != 'string') || (typeof req.body.rpassword2 != 'string') || (req.body.rpassword1 != req.body.rpassword2) || (typeof req.body.rpasswordhint != 'string') || (req.session == null) || (typeof req.session.resettokenusername != 'string') || (typeof req.session.resettokenpassword != 'string')) {
1278 + parent.debug('web', 'handleResetPasswordRequest: checks failed');
1279 + delete req.session.loginmode;
1280 + delete req.session.tokenuserid;
1281 + delete req.session.tokenusername;
1282 + delete req.session.tokenpassword;
1283 + delete req.session.resettokenuserid;
1284 + delete req.session.resettokenusername;
1285 + delete req.session.resettokenpassword;
1286 + delete req.session.tokenemail;
1287 + delete req.session.tokensms;
1288 + delete req.session.messageid;
1289 + delete req.session.passhint;
1290 + delete req.session.cuserid;
1291 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1292 + return;
1293 + }
1294 +
1295 + // Authenticate the user
1296 + obj.authenticate(req.session.resettokenusername, req.session.resettokenpassword, domain, function (err, userid, passhint) {
1297 + if (userid) {
1298 + // Login
1299 + var user = obj.users[userid];
1300 +
1301 + // If we have password requirements, check this here.
1302 + if (!obj.common.checkPasswordRequirements(req.body.rpassword1, domain.passwordrequirements)) {
1303 + parent.debug('web', 'handleResetPasswordRequest: password rejected, use a different one (1)');
1304 + req.session.loginmode = '6';
1305 + req.session.messageid = 105; // Password rejected, use a different one.
1306 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1307 + return;
1308 + }
1309 +
1310 + // Check if the password is the same as a previous one
1311 + obj.checkOldUserPasswords(domain, user, req.body.rpassword1, function (result) {
1312 + if (result != 0) {
1313 + // This is the same password as an older one, request a password change again
1314 + parent.debug('web', 'handleResetPasswordRequest: password rejected, use a different one (2)');
1315 + req.session.loginmode = '6';
1316 + req.session.messageid = 105; // Password rejected, use a different one.
1317 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1318 + } else {
1319 + // Update the password, use a different salt.
1320 + require('./pass').hash(req.body.rpassword1, function (err, salt, hash, tag) {
1321 + const nowSeconds = Math.floor(Date.now() / 1000);
1322 + if (err) { parent.debug('web', 'handleResetPasswordRequest: hash error.'); throw err; }
1323 +
1324 + if (domain.passwordrequirements != null) {
1325 + // Save password hint if this feature is enabled
1326 + if ((domain.passwordrequirements.hint === true) && (req.body.apasswordhint)) { var hint = req.body.apasswordhint; if (hint.length > 250) hint = hint.substring(0, 250); user.passhint = hint; } else { delete user.passhint; }
1327 +
1328 + // Save previous password if this feature is enabled
1329 + if ((typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
1330 + if (user.oldpasswords == null) { user.oldpasswords = []; }
1331 + user.oldpasswords.push({ salt: user.salt, hash: user.hash, start: user.passchange, end: nowSeconds });
1332 + const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
1333 + if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
1334 + }
1335 + }
1336 +
1337 + user.salt = salt;
1338 + user.hash = hash;
1339 + user.passchange = nowSeconds;
1340 + delete user.passtype;
1341 + obj.db.SetUser(user);
1342 +
1343 + // Event the account change
1344 + var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'User password reset', domain: domain.id };
1345 + if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
1346 + obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
1347 +
1348 + // Login successful
1349 + parent.debug('web', 'handleResetPasswordRequest: success');
1350 + req.session.userid = userid;
1351 + req.session.domainid = domain.id;
1352 + req.session.ip = req.clientIp; // Bind this session to the IP address of the request
1353 + completeLoginRequest(req, res, domain, obj.users[userid], userid, req.session.tokenusername, req.session.tokenpassword, direct);
1354 + }, 0);
1355 + }
1356 + }, 0);
1357 + } else {
1358 + // Failed, error out.
1359 + parent.debug('web', 'handleResetPasswordRequest: failed authenticate()');
1360 + delete req.session.loginmode;
1361 + delete req.session.tokenuserid;
1362 + delete req.session.tokenusername;
1363 + delete req.session.tokenpassword;
1364 + delete req.session.resettokenuserid;
1365 + delete req.session.resettokenusername;
1366 + delete req.session.resettokenpassword;
1367 + delete req.session.tokenemail;
1368 + delete req.session.tokensms;
1369 + delete req.session.messageid;
1370 + delete req.session.passhint;
1371 + delete req.session.cuserid;
1372 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1373 + return;
1374 + }
1375 + });
1376 + }
1377 +
1378 + // Called to process an account reset request
1379 + function handleResetAccountRequest(req, res, direct) {
1380 + const domain = checkUserIpAddress(req, res);
1381 + if (domain == null) { return; }
1382 + if ((domain.auth == 'sspi') || (domain.auth == 'ldap') || (obj.args.lanonly == true) || (obj.parent.certificates.CommonName == null) || (obj.parent.certificates.CommonName.indexOf('.') == -1)) { parent.debug('web', 'handleResetAccountRequest: check failed'); res.sendStatus(404); return; }
1383 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1384 +
1385 + // Always lowercase the email address
1386 + if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
1387 +
1388 + // Get the email from the body or session.
1389 + var email = req.body.email;
1390 + if ((email == null) || (email == '')) { email = req.session.tokenemail; }
1391 +
1392 + // Check the email string format
1393 + if (!email || checkEmail(email) == false) {
1394 + parent.debug('web', 'handleResetAccountRequest: Invalid email');
1395 + req.session.loginmode = '3';
1396 + req.session.messageid = 106; // Invalid email.
1397 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1398 + } else {
1399 + obj.db.GetUserWithVerifiedEmail(domain.id, email, function (err, docs) {
1400 + // Remove all accounts that start with ~ since they are special accounts.
1401 + var cleanDocs = [];
1402 + if ((err == null) && (docs.length > 0)) {
1403 + for (var i in docs) {
1404 + const user = docs[i];
1405 + const locked = ((user.siteadmin != null) && (user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)); // No password recovery for locked accounts
1406 + const specialAccount = (user._id.split('/')[2].startsWith('~')); // No password recovery for special accounts
1407 + if ((specialAccount == false) && (locked == false)) { cleanDocs.push(user); }
1408 + }
1409 + }
1410 + docs = cleanDocs;
1411 +
1412 + // Check if we have any account that match this email address
1413 + if ((err != null) || (docs.length == 0)) {
1414 + parent.debug('web', 'handleResetAccountRequest: Account not found');
1415 + req.session.loginmode = '3';
1416 + req.session.messageid = 1; // If valid, reset mail sent. Instead of "Account not found" (107), we send this hold on message so users can't know if this account exists or not.
1417 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1418 + } else {
1419 + // If many accounts have the same validated e-mail, we are going to use the first one for display, but sent a reset email for all accounts.
1420 + var responseSent = false;
1421 + for (var i in docs) {
1422 + var user = docs[i];
1423 + if (checkUserOneTimePasswordRequired(domain, user, req) == true) {
1424 + // Second factor setup, request it now.
1425 + checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result) {
1426 + if (result == false) {
1427 + if (i == 0) {
1428 + // 2-step auth is required, but the token is not present or not valid.
1429 + parent.debug('web', 'handleResetAccountRequest: Invalid 2FA token, try again');
1430 + if ((req.body.token != null) || (req.body.hwtoken != null)) {
1431 + var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
1432 + if ((req.body.hwtoken == '**sms**') && sms2fa) {
1433 + // Cause a token to be sent to the user's phone number
1434 + user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
1435 + obj.db.SetUser(user);
1436 + parent.debug('web', 'Sending 2FA SMS for password recovery to: ' + user.phone);
1437 + parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
1438 + req.session.messageid = 4; // SMS sent.
1439 + } else {
1440 + req.session.messageid = 108; // Invalid token, try again.
1441 + obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, { action: 'authfail', username: user.name, userid: user._id, domain: domain.id, msg: 'User login attempt with incorrect 2nd factor from ' + req.clientIp });
1442 + obj.setbadLogin(req);
1443 + }
1444 + }
1445 + req.session.loginmode = '5';
1446 + delete req.session.tokenemail;
1447 + req.session.tokenemail = email;
1448 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1449 + }
1450 + } else {
1451 + // Send email to perform recovery.
1452 + delete req.session.tokenemail;
1453 + if (obj.parent.mailserver != null) {
1454 + obj.parent.mailserver.sendAccountResetMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
1455 + if (i == 0) {
1456 + parent.debug('web', 'handleResetAccountRequest: Hold on, reset mail sent.');
1457 + req.session.loginmode = '1';
1458 + req.session.messageid = 1; // If valid, reset mail sent.
1459 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1460 + }
1461 + } else {
1462 + if (i == 0) {
1463 + parent.debug('web', 'handleResetAccountRequest: Unable to sent email.');
1464 + req.session.loginmode = '3';
1465 + req.session.messageid = 109; // Unable to sent email.
1466 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1467 + }
1468 + }
1469 + }
1470 + });
1471 + } else {
1472 + // No second factor, send email to perform recovery.
1473 + if (obj.parent.mailserver != null) {
1474 + obj.parent.mailserver.sendAccountResetMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
1475 + if (i == 0) {
1476 + parent.debug('web', 'handleResetAccountRequest: Hold on, reset mail sent.');
1477 + req.session.loginmode = '1';
1478 + req.session.messageid = 1; // If valid, reset mail sent.
1479 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1480 + }
1481 + } else {
1482 + if (i == 0) {
1483 + parent.debug('web', 'handleResetAccountRequest: Unable to sent email.');
1484 + req.session.loginmode = '3';
1485 + req.session.messageid = 109; // Unable to sent email.
1486 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1487 + }
1488 + }
1489 + }
1490 + }
1491 + }
1492 + });
1493 + }
1494 + }
1495 +
1496 + // Handle account email change and email verification request
1497 + function handleCheckAccountEmailRequest(req, res, direct) {
1498 + const domain = checkUserIpAddress(req, res);
1499 + if (domain == null) { return; }
1500 + if ((obj.parent.mailserver == null) || (domain.auth == 'sspi') || (domain.auth == 'ldap') || (typeof req.session.cuserid != 'string') || (obj.users[req.session.cuserid] == null) || (!obj.common.validateEmail(req.body.email, 1, 256))) { parent.debug('web', 'handleCheckAccountEmailRequest: failed checks.'); res.sendStatus(404); return; }
1501 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1502 +
1503 + // Always lowercase the email address
1504 + if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
1505 +
1506 + // Get the email from the body or session.
1507 + var email = req.body.email;
1508 + if ((email == null) || (email == '')) { email = req.session.tokenemail; }
1509 +
1510 + // Check if this request is for an allows email domain
1511 + if ((domain.newaccountemaildomains != null) && Array.isArray(domain.newaccountemaildomains)) {
1512 + var i = -1;
1513 + if (typeof req.body.email == 'string') { i = req.body.email.indexOf('@'); }
1514 + if (i == -1) {
1515 + parent.debug('web', 'handleCreateAccountRequest: unable to create account (1)');
1516 + req.session.loginmode = '7';
1517 + req.session.messageid = 106; // Invalid email.
1518 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1519 + return;
1520 + }
1521 + var emailok = false, emaildomain = req.body.email.substring(i + 1).toLowerCase();
1522 + for (var i in domain.newaccountemaildomains) { if (emaildomain == domain.newaccountemaildomains[i].toLowerCase()) { emailok = true; } }
1523 + if (emailok == false) {
1524 + parent.debug('web', 'handleCreateAccountRequest: unable to create account (2)');
1525 + req.session.loginmode = '7';
1526 + req.session.messageid = 106; // Invalid email.
1527 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1528 + return;
1529 + }
1530 + }
1531 +
1532 + // Check the email string format
1533 + if (!email || checkEmail(email) == false) {
1534 + parent.debug('web', 'handleCheckAccountEmailRequest: Invalid email');
1535 + req.session.loginmode = '7';
1536 + req.session.messageid = 106; // Invalid email.
1537 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1538 + } else {
1539 + // Check is email already exists
1540 + obj.db.GetUserWithVerifiedEmail(domain.id, email, function (err, docs) {
1541 + if ((err != null) || (docs.length > 0)) {
1542 + // Email already exitst
1543 + req.session.messageid = 102; // Existing account with this email address.
1544 + } else {
1545 + // Update the user and notify of user email address change
1546 + var user = obj.users[req.session.cuserid];
1547 + if (user.email != email) {
1548 + user.email = email;
1549 + db.SetUser(user);
1550 + var targets = ['*', 'server-users', user._id];
1551 + if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
1552 + var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Account changed: ' + user.name, domain: domain.id };
1553 + if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
1554 + parent.DispatchEvent(targets, obj, event);
1555 + }
1556 +
1557 + // Send the verification email
1558 + obj.parent.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
1559 +
1560 + // Send the response
1561 + req.session.messageid = 2; // Email sent.
1562 + }
1563 + req.session.loginmode = '7';
1564 + delete req.session.cuserid;
1565 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1566 + });
1567 + }
1568 + }
1569 +
1570 + // Called to process a web based email verification request
1571 + function handleCheckMailRequest(req, res) {
1572 + const domain = checkUserIpAddress(req, res);
1573 + if (domain == null) { return; }
1574 + if ((domain.auth == 'sspi') || (domain.auth == 'ldap') || (obj.parent.mailserver == null)) { parent.debug('web', 'handleCheckMailRequest: failed checks.'); res.sendStatus(404); return; }
1575 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1576 +
1577 + if (req.query.c != null) {
1578 + var cookie = obj.parent.decodeCookie(req.query.c, obj.parent.mailserver.mailCookieEncryptionKey, 30);
1579 + if ((cookie != null) && (cookie.u != null) && (cookie.u.startsWith('user/')) && (cookie.e != null)) {
1580 + var idsplit = cookie.u.split('/');
1581 + if ((idsplit.length != 3) || (idsplit[1] != domain.id)) {
1582 + parent.debug('web', 'handleCheckMailRequest: Invalid domain.');
1583 + render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 1, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
1584 + } else {
1585 + obj.db.Get(cookie.u, function (err, docs) {
1586 + if (docs.length == 0) {
1587 + parent.debug('web', 'handleCheckMailRequest: Invalid username.');
1588 + render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 2, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), arg1: encodeURIComponent(idsplit[1]).replace(/'/g, '%27') }, req, domain));
1589 + } else {
1590 + var user = docs[0];
1591 + if (user.email != cookie.e) {
1592 + parent.debug('web', 'handleCheckMailRequest: Invalid e-mail.');
1593 + render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 3, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), arg1: encodeURIComponent(user.email).replace(/'/g, '%27'), arg2: encodeURIComponent(user.name).replace(/'/g, '%27') }, req, domain));
1594 + } else {
1595 + if (cookie.a == 1) {
1596 + // Account email verification
1597 + if (user.emailVerified == true) {
1598 + parent.debug('web', 'handleCheckMailRequest: email already verified.');
1599 + render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 4, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), arg1: encodeURIComponent(user.email).replace(/'/g, '%27'), arg2: encodeURIComponent(user.name).replace(/'/g, '%27') }, req, domain));
1600 + } else {
1601 + obj.db.GetUserWithVerifiedEmail(domain.id, user.email, function (err, docs) {
1602 + if (docs.length > 0) {
1603 + parent.debug('web', 'handleCheckMailRequest: email already in use.');
1604 + render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 5, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), arg1: encodeURIComponent(user.email).replace(/'/g, '%27') }, req, domain));
1605 + } else {
1606 + parent.debug('web', 'handleCheckMailRequest: email verification success.');
1607 +
1608 + // Set the verified flag
1609 + obj.users[user._id].emailVerified = true;
1610 + user.emailVerified = true;
1611 + obj.db.SetUser(user);
1612 +
1613 + // Event the change
1614 + var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Verified email of user ' + EscapeHtml(user.name) + ' (' + EscapeHtml(user.email) + ')', domain: domain.id };
1615 + if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
1616 + obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
1617 +
1618 + // Send the confirmation page
1619 + render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 6, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), arg1: encodeURIComponent(user.email).replace(/'/g, '%27'), arg2: encodeURIComponent(user.name).replace(/'/g, '%27') }, req, domain));
1620 +
1621 + // Send a notification
1622 + obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: 'Email verified', value: user.email, nolog: 1, id: Math.random() });
1623 +
1624 + // Send to authlog
1625 + if (obj.parent.authlog) { obj.parent.authLog('https', 'Verified email address ' + user.email + ' for user ' + user.name); }
1626 + }
1627 + });
1628 + }
1629 + } else if (cookie.a == 2) {
1630 + // Account reset
1631 + if (user.emailVerified != true) {
1632 + parent.debug('web', 'handleCheckMailRequest: email not verified.');
1633 + render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 7, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), arg1: EscapeHtml(user.email), arg2: EscapeHtml(user.name) }, req, domain));
1634 + } else {
1635 + if (req.query.confirm == 1) {
1636 + // Set a temporary password
1637 + obj.crypto.randomBytes(16, function (err, buf) {
1638 + var newpass = buf.toString('base64').split('=').join('').split('/').join('').split('+').join('');
1639 + require('./pass').hash(newpass, function (err, salt, hash, tag) {
1640 + if (err) throw err;
1641 +
1642 + // Change the password
1643 + var userinfo = obj.users[user._id];
1644 + userinfo.salt = salt;
1645 + userinfo.hash = hash;
1646 + delete userinfo.passtype;
1647 + userinfo.passchange = Math.floor(Date.now() / 1000);
1648 + delete userinfo.passhint;
1649 + obj.db.SetUser(userinfo);
1650 +
1651 + // Event the change
1652 + var event = { etype: 'user', userid: user._id, username: userinfo.name, account: obj.CloneSafeUser(userinfo), action: 'accountchange', msg: 'Password reset for user ' + EscapeHtml(user.name), domain: domain.id };
1653 + if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
1654 + obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
1655 +
1656 + // Send the new password
1657 + render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 8, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), arg1: EscapeHtml(user.name), arg2: EscapeHtml(newpass) }, req, domain));
1658 + parent.debug('web', 'handleCheckMailRequest: send temporary password.');
1659 +
1660 + // Send to authlog
1661 + if (obj.parent.authlog) { obj.parent.authLog('https', 'Performed account reset for user ' + user.name); }
1662 + }, 0);
1663 + });
1664 + } else {
1665 + // Display a link for the user to confirm password reset
1666 + // We must do this because GMail will also load this URL a few seconds after the user does and we don't want to cause two password resets.
1667 + render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 14, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
1668 + }
1669 + }
1670 + } else {
1671 + render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 9, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
1672 + }
1673 + }
1674 + }
1675 + });
1676 + }
1677 + } else {
1678 + render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 1, msgid: 10, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
1679 + }
1680 + }
1681 + }
1682 +
1683 + // Called to process an agent invite GET/POST request
1684 + function handleInviteRequest(req, res) {
1685 + const domain = getDomain(req);
1686 + if (domain == null) { parent.debug('web', 'handleInviteRequest: failed checks.'); res.sendStatus(404); return; }
1687 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1688 + if ((req.body.inviteCode == null) || (req.body.inviteCode == '')) { render(req, res, getRenderPage('invite', req, domain), getRenderArgs({ messageid: 0 }, req, domain)); return; } // No invitation code
1689 +
1690 + // Each for a device group that has this invite code.
1691 + for (var i in obj.meshes) {
1692 + if ((obj.meshes[i].domain == domain.id) && (obj.meshes[i].invite != null) && (obj.meshes[i].invite.codes.indexOf(req.body.inviteCode) >= 0)) {
1693 + // Send invitation link, valid for 1 minute.
1694 + res.redirect(domain.url + 'agentinvite?c=' + parent.encodeCookie({ a: 4, mid: i, f: obj.meshes[i].invite.flags, expire: 1 }, parent.invitationLinkEncryptionKey) + (req.query.key ? ('&key=' + req.query.key) : ''));
1695 + return;
1696 + }
1697 + }
1698 +
1699 + render(req, res, getRenderPage('invite', req, domain), getRenderArgs({ messageid: 100 }, req, domain)); // Bad invitation code
1700 + }
1701 +
1702 + // Called to render the MSTSC (RDP) web page
1703 + function handleMSTSCRequest(req, res) {
1704 + const domain = getDomain(req);
1705 + if (domain == null) { parent.debug('web', 'handleMSTSCRequest: failed checks.'); res.sendStatus(404); return; }
1706 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1707 +
1708 + // Check if we are in maintenance mode
1709 + if ((parent.config.settings.maintenancemode != null) && (req.query.admin !== '1')) {
1710 + render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 3, msgid: 13, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
1711 + return;
1712 + }
1713 +
1714 + if (req.query.ws != null) {
1715 + // This is a query with a websocket relay cookie, check that the cookie is valid and use it.
1716 + var rcookie = parent.decodeCookie(req.query.ws, parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
1717 + if ((rcookie != null) && (rcookie.domainid == domain.id) && (rcookie.nodeid != null) && (rcookie.tcpport != null)) {
1718 + render(req, res, getRenderPage('mstsc', req, domain), getRenderArgs({ cookie: req.query.ws, name: encodeURIComponent(req.query.name).replace(/'/g, '%27') }, req, domain)); return;
1719 + }
1720 + }
1721 +
1722 + // Get the logged in user if present
1723 + var user = null;
1724 +
1725 + // If there is a login token, use that
1726 + if (req.query.login != null) {
1727 + var ucookie = parent.decodeCookie(req.query.login, parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
1728 + if ((ucookie != null) && (ucookie.a === 3) && (typeof ucookie.u == 'string')) { user = obj.users[ucookie.u]; }
1729 + }
1730 +
1731 + // If no token, see if we have an active session
1732 + if ((user == null) && (req.session.userid != null)) { user = obj.users[req.session.userid]; }
1733 +
1734 + // If still no user, see if we have a default user
1735 + if ((user == null) && (obj.args.user)) { user = obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]; }
1736 +
1737 + // No user login, exit now
1738 + if (user == null) { res.sendStatus(401); return; }
1739 +
1740 + // Check the nodeid
1741 + if (req.query.node != null) {
1742 + var nodeidsplit = req.query.node.split('/');
1743 + if (nodeidsplit.length == 1) {
1744 + req.query.node = 'node/' + domain.id + '/' + nodeidsplit[0]; // Format the nodeid correctly
1745 + } else if (nodeidsplit.length == 3) {
1746 + if ((nodeidsplit[0] != 'node') || (nodeidsplit[1] != domain.id)) { req.query.node = null; } // Check the nodeid format
1747 + } else {
1748 + req.query.node = null; // Bad nodeid
1749 + }
1750 + }
1751 +
1752 + // If there is no nodeid, exit now
1753 + if (req.query.node == null) { render(req, res, getRenderPage('mstsc', req, domain), getRenderArgs({ cookie: '', name: '' }, req, domain)); return; }
1754 +
1755 + // Fetch the node from the database
1756 + obj.db.Get(req.query.node, function (err, nodes) {
1757 + if ((err != null) || (nodes.length != 1)) { res.sendStatus(404); return; }
1758 + const node = nodes[0];
1759 +
1760 + // Check access rights, must have remote control rights
1761 + if ((obj.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0) { res.sendStatus(401); return; }
1762 +
1763 + // Figure out the target port
1764 + var port = 3389;
1765 + if (typeof node.rdpport == 'number') { port = node.rdpport; }
1766 + if (req.query.port != null) { var qport = 0; try { qport = parseInt(req.query.port); } catch (ex) { } if ((typeof qport == 'number') && (qport > 0) && (qport < 65536)) { port = qport; } }
1767 +
1768 + // Generate a cookie and respond
1769 + var cookie = parent.encodeCookie({ userid: user._id, domainid: user.domain, nodeid: node._id, tcpport: port }, parent.loginCookieEncryptionKey);
1770 + render(req, res, getRenderPage('mstsc', req, domain), getRenderArgs({ cookie: cookie, name: encodeURIComponent(node.name).replace(/'/g, '%27') }, req, domain));
1771 + });
1772 + }
1773 +
1774 + // Called to process an agent invite request
1775 + function handleAgentInviteRequest(req, res) {
1776 + const domain = getDomain(req);
1777 + if ((domain == null) || ((req.query.m == null) && (req.query.c == null))) { parent.debug('web', 'handleAgentInviteRequest: failed checks.'); res.sendStatus(404); return; }
1778 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1779 +
1780 + if (req.query.c != null) {
1781 + // A cookie is specified in the query string, use that
1782 + var cookie = obj.parent.decodeCookie(req.query.c, obj.parent.invitationLinkEncryptionKey);
1783 + if (cookie == null) { res.sendStatus(404); return; }
1784 + var mesh = obj.meshes[cookie.mid];
1785 + if (mesh == null) { res.sendStatus(404); return; }
1786 + var installflags = cookie.f;
1787 + if (typeof installflags != 'number') { installflags = 0; }
1788 + parent.debug('web', 'handleAgentInviteRequest using cookie.');
1789 + var meshcookie = parent.encodeCookie({ m: mesh._id.split('/')[2] }, parent.invitationLinkEncryptionKey);
1790 + render(req, res, getRenderPage('agentinvite', req, domain), getRenderArgs({ meshid: meshcookie, serverport: ((args.aliasport != null) ? args.aliasport : args.port), serverhttps: 1, servernoproxy: ((domain.agentnoproxy === true) ? '1' : '0'), meshname: encodeURIComponent(mesh.name).replace(/'/g, '%27'), installflags: installflags }, req, domain));
1791 + } else if (req.query.m != null) {
1792 + // The MeshId is specified in the query string, use that
1793 + var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.m.toLowerCase()];
1794 + if (mesh == null) { res.sendStatus(404); return; }
1795 + var installflags = 0;
1796 + if (req.query.f) { installflags = parseInt(req.query.f); }
1797 + if (typeof installflags != 'number') { installflags = 0; }
1798 + parent.debug('web', 'handleAgentInviteRequest using meshid.');
1799 + var meshcookie = parent.encodeCookie({ m: mesh._id.split('/')[2] }, parent.invitationLinkEncryptionKey);
1800 + render(req, res, getRenderPage('agentinvite', req, domain), getRenderArgs({ meshid: meshcookie, serverport: ((args.aliasport != null) ? args.aliasport : args.port), serverhttps: 1, servernoproxy: ((domain.agentnoproxy === true) ? '1' : '0'), meshname: encodeURIComponent(mesh.name).replace(/'/g, '%27'), installflags: installflags }, req, domain));
1801 + }
1802 + }
1803 +
1804 + function handleDeleteAccountRequest(req, res, direct) {
1805 + parent.debug('web', 'handleDeleteAccountRequest()');
1806 + const domain = checkUserIpAddress(req, res);
1807 + if (domain == null) { return; }
1808 + if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handleDeleteAccountRequest: failed checks.'); res.sendStatus(404); return; }
1809 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1810 +
1811 + var user = null;
1812 + if (req.body.authcookie) {
1813 + // If a authentication cookie is provided, decode it here
1814 + var loginCookie = obj.parent.decodeCookie(req.body.authcookie, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
1815 + if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { user = obj.users[loginCookie.userid]; }
1816 + } else {
1817 + // Check if the user is logged and we have all required parameters
1818 + if (!req.session || !req.session.userid || !req.body.apassword1 || (req.body.apassword1 != req.body.apassword2) || (req.session.domainid != domain.id)) {
1819 + parent.debug('web', 'handleDeleteAccountRequest: required parameters not present.');
1820 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1821 + return;
1822 + } else {
1823 + user = obj.users[req.session.userid];
1824 + }
1825 + }
1826 + if (!user) { parent.debug('web', 'handleDeleteAccountRequest: user not found.'); res.sendStatus(404); return; }
1827 + if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) { parent.debug('web', 'handleDeleteAccountRequest: account settings locked.'); res.sendStatus(404); return; }
1828 +
1829 + // Check if the password is correct
1830 + obj.authenticate(user._id.split('/')[2], req.body.apassword1, domain, function (err, userid) {
1831 + var deluser = obj.users[userid];
1832 + if ((userid != null) && (deluser != null)) {
1833 + // Remove all links to this user
1834 + if (deluser.links != null) {
1835 + for (var i in deluser.links) {
1836 + if (i.startsWith('mesh/')) {
1837 + // Get the device group
1838 + var mesh = obj.meshes[i];
1839 + if (mesh) {
1840 + // Remove user from the mesh
1841 + if (mesh.links[deluser._id] != null) { delete mesh.links[deluser._id]; parent.db.Set(mesh); }
1842 +
1843 + // Notify mesh change
1844 + var change = 'Removed user ' + deluser.name + ' from group ' + mesh.name;
1845 + var event = { etype: 'mesh', userid: user._id, username: user.name, meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, action: 'meshchange', links: mesh.links, msg: change, domain: domain.id, invite: mesh.invite };
1846 + if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the mesh. Another event will come.
1847 + parent.DispatchEvent(['*', mesh._id, deluser._id, user._id], obj, event);
1848 + }
1849 + } else if (i.startsWith('node/')) {
1850 + // Get the node and the rights for this node
1851 + obj.GetNodeWithRights(domain, deluser, i, function (node, rights, visible) {
1852 + if ((node == null) || (node.links == null) || (node.links[deluser._id] == null)) return;
1853 +
1854 + // Remove the link and save the node to the database
1855 + delete node.links[deluser._id];
1856 + if (Object.keys(node.links).length == 0) { delete node.links; }
1857 + db.Set(obj.cleanDevice(node));
1858 +
1859 + // Event the node change
1860 + var event = { etype: 'node', userid: user._id, username: user.name, action: 'changenode', nodeid: node._id, domain: domain.id, msg: ('Removed user device rights for ' + node.name), node: obj.CloneSafeNode(node) }
1861 + if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the mesh. Another event will come.
1862 + parent.DispatchEvent(['*', node.meshid, node._id], obj, event);
1863 + });
1864 + } else if (i.startsWith('ugrp/')) {
1865 + // Get the device group
1866 + var ugroup = obj.userGroups[i];
1867 + if (ugroup) {
1868 + // Remove user from the user group
1869 + if (ugroup.links[deluser._id] != null) { delete ugroup.links[deluser._id]; parent.db.Set(ugroup); }
1870 +
1871 + // Notify user group change
1872 + var change = 'Removed user ' + deluser.name + ' from user group ' + ugroup.name;
1873 + var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: ugroup._id, name: ugroup.name, desc: ugroup.desc, action: 'usergroupchange', links: ugroup.links, msg: 'Removed user ' + deluser.name + ' from user group ' + ugroup.name, addUserDomain: domain.id };
1874 + if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user group. Another event will come.
1875 + parent.DispatchEvent(['*', ugroup._id, user._id, deluser._id], obj, event);
1876 + }
1877 + }
1878 + }
1879 + }
1880 +
1881 + // Remove notes for this user
1882 + obj.db.Remove('nt' + deluser._id);
1883 +
1884 + // Remove the user
1885 + obj.db.Remove(deluser._id);
1886 + delete obj.users[deluser._id];
1887 + req.session = null;
1888 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1889 + obj.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', userid: deluser._id, username: deluser.name, action: 'accountremove', msg: 'Account removed', domain: domain.id });
1890 + parent.debug('web', 'handleDeleteAccountRequest: removed user.');
1891 + } else {
1892 + parent.debug('web', 'handleDeleteAccountRequest: auth failed.');
1893 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1894 + }
1895 + });
1896 + }
1897 +
1898 + // Check a user's password
1899 + obj.checkUserPassword = function (domain, user, password, func) {
1900 + // Check the old password
1901 + if (user.passtype != null) {
1902 + // IIS default clear or weak password hashing (SHA-1)
1903 + require('./pass').iishash(user.passtype, password, user.salt, function (err, hash) {
1904 + if (err) { parent.debug('web', 'checkUserPassword: SHA-1 fail.'); return func(false); }
1905 + if (hash == user.hash) {
1906 + if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { parent.debug('web', 'checkUserPassword: SHA-1 locked.'); return func(false); } // Account is locked
1907 + parent.debug('web', 'checkUserPassword: SHA-1 ok.');
1908 + return func(true); // Allow password change
1909 + }
1910 + func(false);
1911 + });
1912 + } else {
1913 + // Default strong password hashing (pbkdf2 SHA384)
1914 + require('./pass').hash(password, user.salt, function (err, hash, tag) {
1915 + if (err) { parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 fail.'); return func(false); }
1916 + if (hash == user.hash) {
1917 + if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 locked.'); return func(false); } // Account is locked
1918 + parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 ok.');
1919 + return func(true); // Allow password change
1920 + }
1921 + func(false);
1922 + }, 0);
1923 + }
1924 + }
1925 +
1926 + // Check a user's old passwords
1927 + // Callback: 0=OK, 1=OldPass, 2=CommonPass
1928 + obj.checkOldUserPasswords = function (domain, user, password, func) {
1929 + // Check how many old passwords we need to check
1930 + if ((domain.passwordrequirements != null) && (typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
1931 + if (user.oldpasswords != null) {
1932 + const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
1933 + if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
1934 + }
1935 + } else {
1936 + delete user.oldpasswords;
1937 + }
1938 +
1939 + // If there is no old passwords, exit now.
1940 + var oldPassCount = 1;
1941 + if (user.oldpasswords != null) { oldPassCount += user.oldpasswords.length; }
1942 + var oldPassCheckState = { response: 0, count: oldPassCount, user: user, func: func };
1943 +
1944 + // Test against common passwords if this feature is enabled
1945 + // Example of common passwords: 123456789, password123
1946 + if ((domain.passwordrequirements != null) && (domain.passwordrequirements.bancommonpasswords == true)) {
1947 + oldPassCheckState.count++;
1948 + require('wildleek')(password).then(function (wild) {
1949 + if (wild == true) { oldPassCheckState.response = 2; }
1950 + if (--oldPassCheckState.count == 0) { oldPassCheckState.func(oldPassCheckState.response); }
1951 + });
1952 + }
1953 +
1954 + // Try current password
1955 + require('./pass').hash(password, user.salt, function oldPassCheck(err, hash, tag) {
1956 + if ((err == null) && (hash == tag.user.hash)) { tag.response = 1; }
1957 + if (--tag.count == 0) { tag.func(tag.response); }
1958 + }, oldPassCheckState);
1959 +
1960 + // Try each old password
1961 + if (user.oldpasswords != null) {
1962 + for (var i in user.oldpasswords) {
1963 + const oldpassword = user.oldpasswords[i];
1964 + // Default strong password hashing (pbkdf2 SHA384)
1965 + require('./pass').hash(password, oldpassword.salt, function oldPassCheck(err, hash, tag) {
1966 + if ((err == null) && (hash == tag.oldPassword.hash)) { tag.state.response = 1; }
1967 + if (--tag.state.count == 0) { tag.state.func(tag.state.response); }
1968 + }, { oldPassword: oldpassword, state: oldPassCheckState });
1969 + }
1970 + }
1971 + }
1972 +
1973 + // Handle password changes
1974 + function handlePasswordChangeRequest(req, res, direct) {
1975 + const domain = checkUserIpAddress(req, res);
1976 + if (domain == null) { return; }
1977 + if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handlePasswordChangeRequest: failed checks (1).'); res.sendStatus(404); return; }
1978 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1979 +
1980 + // Check if the user is logged and we have all required parameters
1981 + if (!req.session || !req.session.userid || !req.body.apassword0 || !req.body.apassword1 || (req.body.apassword1 != req.body.apassword2) || (req.session.domainid != domain.id)) {
1982 + parent.debug('web', 'handlePasswordChangeRequest: failed checks (2).');
1983 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1984 + return;
1985 + }
1986 +
1987 + // Get the current user
1988 + var user = obj.users[req.session.userid];
1989 + if (!user) {
1990 + parent.debug('web', 'handlePasswordChangeRequest: user not found.');
1991 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1992 + return;
1993 + }
1994 +
1995 + // Check account settings locked
1996 + if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) {
1997 + parent.debug('web', 'handlePasswordChangeRequest: account settings locked.');
1998 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1999 + return;
2000 + }
2001 +
2002 + // Check old password
2003 + obj.checkUserPassword(domain, user, req.body.apassword1, function (result) {
2004 + if (result == true) {
2005 + // Check if the new password is allowed, only do this if this feature is enabled.
2006 + parent.checkOldUserPasswords(domain, user, command.newpass, function (result) {
2007 + if (result == 1) {
2008 + parent.debug('web', 'handlePasswordChangeRequest: old password reuse attempt.');
2009 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2010 + } else if (result == 2) {
2011 + parent.debug('web', 'handlePasswordChangeRequest: commonly used password use attempt.');
2012 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2013 + } else {
2014 + // Update the password
2015 + require('./pass').hash(req.body.apassword1, function (err, salt, hash, tag) {
2016 + const nowSeconds = Math.floor(Date.now() / 1000);
2017 + if (err) { parent.debug('web', 'handlePasswordChangeRequest: hash error.'); throw err; }
2018 + if (domain.passwordrequirements != null) {
2019 + // Save password hint if this feature is enabled
2020 + if ((domain.passwordrequirements.hint === true) && (req.body.apasswordhint)) { var hint = req.body.apasswordhint; if (hint.length > 250) hint = hint.substring(0, 250); user.passhint = hint; } else { delete user.passhint; }
2021 +
2022 + // Save previous password if this feature is enabled
2023 + if ((typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
2024 + if (user.oldpasswords == null) { user.oldpasswords = []; }
2025 + user.oldpasswords.push({ salt: user.salt, hash: user.hash, start: user.passchange, end: nowSeconds });
2026 + const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
2027 + if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
2028 + }
2029 + }
2030 + user.salt = salt;
2031 + user.hash = hash;
2032 + user.passchange = nowSeconds;
2033 + delete user.passtype;
2034 +
2035 + obj.db.SetUser(user);
2036 + req.session.viewmode = 2;
2037 + if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2038 + obj.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', userid: user._id, username: user.name, action: 'passchange', msg: 'Account password changed: ' + user.name, domain: domain.id });
2039 + }, 0);
2040 + }
2041 + });
2042 + }
2043 + });
2044 + }
2045 +
2046 + // Called when a strategy login occured
2047 + // This is called after a succesful Oauth to Twitter, Google, GitHub...
2048 + function handleStrategyLogin(req, res) {
2049 + const domain = checkUserIpAddress(req, res);
2050 + if (domain == null) { return; }
2051 + parent.debug('web', 'handleStrategyLogin: ' + JSON.stringify(req.user));
2052 + if ((req.user != null) && (req.user.sid != null)) {
2053 + const userid = 'user/' + domain.id + '/' + req.user.sid;
2054 + var user = obj.users[userid];
2055 + if (user == null) {
2056 + var newAccountAllowed = false;
2057 + var newAccountRealms = null;
2058 +
2059 + if (domain.newaccounts === true) { newAccountAllowed = true; }
2060 + if (obj.common.validateStrArray(domain.newaccountrealms)) { newAccountRealms = domain.newaccountrealms; }
2061 +
2062 + if ((domain.authstrategies != null) && (domain.authstrategies[req.user.strategy] != null)) {
2063 + if (domain.authstrategies[req.user.strategy].newaccounts === true) { newAccountAllowed = true; }
2064 + if (obj.common.validateStrArray(domain.authstrategies[req.user.strategy].newaccountrealms)) { newAccountRealms = domain.authstrategies[req.user.strategy].newaccountrealms; }
2065 + }
2066 +
2067 + if (newAccountAllowed === true) {
2068 + // Create the user
2069 + parent.debug('web', 'handleStrategyLogin: creating new user: ' + userid);
2070 + user = { type: 'user', _id: userid, name: req.user.name, email: req.user.email, creation: Math.floor(Date.now() / 1000), domain: domain.id };
2071 + if (req.user.email != null) { user.email = req.user.email; user.emailVerified = true; }
2072 + if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; } // New accounts automatically assigned server rights.
2073 + if (domain.authstrategies[req.user.strategy].newaccountsrights) { user.siteadmin = obj.common.meshServerRightsArrayToNumber(domain.authstrategies[req.user.strategy].newaccountsrights); } // If there are specific SSO server rights, use these instead.
2074 + if (newAccountRealms) { user.groups = newAccountRealms; } // New accounts automatically part of some groups (Realms).
2075 + obj.users[userid] = user;
2076 +
2077 + // Auto-join any user groups
2078 + var newaccountsusergroups = null;
2079 + if (typeof domain.newaccountsusergroups == 'object') { newaccountsusergroups = domain.newaccountsusergroups; }
2080 + if (typeof domain.authstrategies[req.user.strategy].newaccountsusergroups == 'object') { newaccountsusergroups = domain.authstrategies[req.user.strategy].newaccountsusergroups; }
2081 + if (newaccountsusergroups) {
2082 + for (var i in newaccountsusergroups) {
2083 + var ugrpid = newaccountsusergroups[i];
2084 + if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
2085 + var ugroup = obj.userGroups[ugrpid];
2086 + if (ugroup != null) {
2087 + // Add group to the user
2088 + if (user.links == null) { user.links = {}; }
2089 + user.links[ugroup._id] = { rights: 1 };
2090 +
2091 + // Add user to the group
2092 + ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
2093 + db.Set(ugroup);
2094 +
2095 + // Notify user group change
2096 + var event = { etype: 'ugrp', ugrpid: ugroup._id, name: ugroup.name, desc: ugroup.desc, action: 'usergroupchange', links: ugroup.links, msg: 'Added user ' + user.name + ' to user group ' + ugroup.name, addUserDomain: domain.id };
2097 + if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user group. Another event will come.
2098 + parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
2099 + }
2100 + }
2101 + }
2102 +
2103 + // Save the user
2104 + obj.db.SetUser(user);
2105 +
2106 + // Event user creation
2107 + var targets = ['*', 'server-users'];
2108 + var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountcreate', msg: 'Account created, username is ' + user.name, domain: domain.id };
2109 + if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to create the user. Another event will come.
2110 + parent.DispatchEvent(targets, obj, event);
2111 +
2112 + req.session.userid = userid;
2113 + req.session.domainid = domain.id;
2114 + } else {
2115 + // New users not allowed
2116 + parent.debug('web', 'handleStrategyLogin: Can\'t create new accounts');
2117 + req.session.loginmode = '1';
2118 + req.session.messageid = 100; // Unable to create account.
2119 + res.redirect(domain.url + getQueryPortion(req));
2120 + return;
2121 + }
2122 + } else {
2123 + // Login success
2124 + var userChange = false;
2125 + if ((req.user.name != null) && (req.user.name != user.name)) { user.name = req.user.name; userChange = true; }
2126 + if ((req.user.email != null) && (req.user.email != user.email)) { user.email = req.user.email; user.emailVerified = true; userChange = true; }
2127 + if (userChange) {
2128 + obj.db.SetUser(user);
2129 +
2130 + // Event user creation
2131 + var targets = ['*', 'server-users'];
2132 + var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Account changed', domain: domain.id };
2133 + if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to create the user. Another event will come.
2134 + parent.DispatchEvent(targets, obj, event);
2135 + }
2136 + parent.debug('web', 'handleStrategyLogin: succesful login: ' + userid);
2137 + req.session.userid = userid;
2138 + req.session.domainid = domain.id;
2139 + }
2140 + }
2141 + //res.redirect(domain.url); // This does not handle cookie correctly.
2142 + res.set('Content-Type', 'text/html');
2143 + res.end('<html><head><meta http-equiv="refresh" content=0;url="' + domain.url + '"></head><body></body></html>');
2144 + }
2145 +
2146 + // Indicates that any request to "/" should render "default" or "login" depending on login state
2147 + function handleRootRequest(req, res, direct) {
2148 + const domain = checkUserIpAddress(req, res);
2149 + if (domain == null) { return; }
2150 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2151 + if (!obj.args) { parent.debug('web', 'handleRootRequest: no obj.args.'); res.sendStatus(500); return; }
2152 +
2153 + // Check if we are in maintenance mode
2154 + if ((parent.config.settings.maintenancemode != null) && (req.query.admin !== '1')) {
2155 + parent.debug('web', 'handleLoginRequest: Server under maintenance.');
2156 + render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 3, msgid: 13, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
2157 + return;
2158 + }
2159 +
2160 + if ((domain.sspi != null) && ((req.query.login == null) || (obj.parent.loginCookieEncryptionKey == null))) {
2161 + // Login using SSPI
2162 + domain.sspi.authenticate(req, res, function (err) {
2163 + if ((err != null) || (req.connection.user == null)) {
2164 + if (obj.parent.authlog) { obj.parent.authLog('https', 'Failed SSPI-auth for ' + req.connection.user + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); }
2165 + parent.debug('web', 'handleRootRequest: SSPI auth required.');
2166 + res.end('Authentication Required...');
2167 + } else {
2168 + if (obj.parent.authlog) { obj.parent.authLog('https', 'Accepted SSPI-auth for ' + req.connection.user + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); }
2169 + parent.debug('web', 'handleRootRequest: SSPI auth ok.');
2170 + handleRootRequestEx(req, res, domain, direct);
2171 + }
2172 + });
2173 + } else if (req.query.user && req.query.pass) {
2174 + // User credentials are being passed in the URL. WARNING: Putting credentials in a URL is bad security... but people are requesting this option.
2175 + obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid) {
2176 + if (obj.parent.authlog) { obj.parent.authLog('https', 'Accepted password for ' + req.connection.user + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); }
2177 + parent.debug('web', 'handleRootRequest: user/pass in URL auth ok.');
2178 + req.session.userid = userid;
2179 + req.session.domainid = domain.id;
2180 + req.session.currentNode = '';
2181 + req.session.ip = req.clientIp; // Bind this session to the IP address of the request
2182 + handleRootRequestEx(req, res, domain, direct);
2183 + });
2184 + } else {
2185 + // Login using a different system
2186 + handleRootRequestEx(req, res, domain, direct);
2187 + }
2188 + }
2189 +
2190 + function handleRootRequestEx(req, res, domain, direct) {
2191 + var nologout = false, user = null, features = 0, features2 = 0;
2192 + res.set({ 'Cache-Control': 'no-store' });
2193 +
2194 + // Check if we have an incomplete domain name in the path
2195 + if ((domain.id != '') && (domain.dns == null) && (req.url.split('/').length == 2)) {
2196 + parent.debug('web', 'handleRootRequestEx: incomplete domain name in the path.');
2197 + res.redirect(domain.url + getQueryPortion(req)); // BAD***
2198 + return;
2199 + }
2200 +
2201 + if (obj.args.nousers == true) {
2202 + // If in single user mode, setup things here.
2203 + if (req.session && req.session.loginmode) { delete req.session.loginmode; }
2204 + req.session.userid = 'user/' + domain.id + '/~';
2205 + req.session.domainid = domain.id;
2206 + req.session.currentNode = '';
2207 + req.session.ip = req.clientIp; // Bind this session to the IP address of the request
2208 + if (obj.users[req.session.userid] == null) {
2209 + // Create the dummy user ~ with impossible password
2210 + parent.debug('web', 'handleRootRequestEx: created dummy user in nouser mode.');
2211 + obj.users[req.session.userid] = { type: 'user', _id: req.session.userid, name: '~', email: '~', domain: domain.id, siteadmin: 4294967295 };
2212 + obj.db.SetUser(obj.users[req.session.userid]);
2213 + }
2214 + } else if (obj.args.user && obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]) {
2215 + // If a default user is active, setup the session here.
2216 + parent.debug('web', 'handleRootRequestEx: auth using default user.');
2217 + if (req.session && req.session.loginmode) { delete req.session.loginmode; }
2218 + req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase();
2219 + req.session.domainid = domain.id;
2220 + req.session.currentNode = '';
2221 + req.session.ip = req.clientIp; // Bind this session to the IP address of the request
2222 + } else if (req.query.login && (obj.parent.loginCookieEncryptionKey != null)) {
2223 + var loginCookie = obj.parent.decodeCookie(req.query.login, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
2224 + //if ((loginCookie != null) && (obj.args.cookieipcheck !== false) && (loginCookie.ip != null) && (loginCookie.ip != req.clientIp)) { loginCookie = null; } // If the cookie if binded to an IP address, check here.
2225 + if ((loginCookie != null) && (loginCookie.a == 3) && (loginCookie.u != null) && (loginCookie.u.split('/')[1] == domain.id)) {
2226 + // If a login cookie was provided, setup the session here.
2227 + parent.debug('web', 'handleRootRequestEx: cookie auth ok.');
2228 + if (req.session && req.session.loginmode) { delete req.session.loginmode; }
2229 + req.session.userid = loginCookie.u;
2230 + req.session.domainid = domain.id;
2231 + req.session.currentNode = '';
2232 + req.session.ip = req.clientIp; // Bind this session to the IP address of the request
2233 + } else {
2234 + parent.debug('web', 'handleRootRequestEx: cookie auth failed.');
2235 + }
2236 + } else if (domain.sspi != null) {
2237 + // SSPI login (Windows only)
2238 + //console.log(req.connection.user, req.connection.userSid);
2239 + if ((req.connection.user == null) || (req.connection.userSid == null)) {
2240 + parent.debug('web', 'handleRootRequestEx: SSPI no user auth.');
2241 + res.sendStatus(404); return;
2242 + } else {
2243 + nologout = true;
2244 + req.session.userid = 'user/' + domain.id + '/' + req.connection.user.toLowerCase();
2245 + req.session.usersid = req.connection.userSid;
2246 + req.session.usersGroups = req.connection.userGroups;
2247 + req.session.domainid = domain.id;
2248 + req.session.currentNode = '';
2249 + req.session.ip = req.clientIp; // Bind this session to the IP address of the request
2250 +
2251 + // Check if this user exists, create it if not.
2252 + user = obj.users[req.session.userid];
2253 + if ((user == null) || (user.sid != req.session.usersid)) {
2254 + // Create the domain user
2255 + var usercount = 0, user2 = { type: 'user', _id: req.session.userid, name: req.connection.user, domain: domain.id, sid: req.session.usersid, creation: Math.floor(Date.now() / 1000), login: Math.floor(Date.now() / 1000) };
2256 + if (domain.newaccountsrights) { user2.siteadmin = domain.newaccountsrights; }
2257 + if (obj.common.validateStrArray(domain.newaccountrealms)) { user2.groups = domain.newaccountrealms; }
2258 + for (var i in obj.users) { if (obj.users[i].domain == domain.id) { usercount++; } }
2259 + if (usercount == 0) { user2.siteadmin = 4294967295; } // If this is the first user, give the account site admin.
2260 +
2261 + // Auto-join any user groups
2262 + if (typeof domain.newaccountsusergroups == 'object') {
2263 + for (var i in domain.newaccountsusergroups) {
2264 + var ugrpid = domain.newaccountsusergroups[i];
2265 + if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
2266 + var ugroup = obj.userGroups[ugrpid];
2267 + if (ugroup != null) {
2268 + // Add group to the user
2269 + if (user2.links == null) { user2.links = {}; }
2270 + user2.links[ugroup._id] = { rights: 1 };
2271 +
2272 + // Add user to the group
2273 + ugroup.links[user2._id] = { userid: user2._id, name: user2.name, rights: 1 };
2274 + db.Set(ugroup);
2275 +
2276 + // Notify user group change
2277 + var event = { etype: 'ugrp', ugrpid: ugroup._id, name: ugroup.name, desc: ugroup.desc, action: 'usergroupchange', links: ugroup.links, msg: 'Added user ' + user2.name + ' to user group ' + ugroup.name, addUserDomain: domain.id };
2278 + if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user group. Another event will come.
2279 + parent.DispatchEvent(['*', ugroup._id, user2._id], obj, event);
2280 + }
2281 + }
2282 + }
2283 +
2284 + obj.users[req.session.userid] = user2;
2285 + obj.db.SetUser(user2);
2286 + var event = { etype: 'user', userid: req.session.userid, username: req.connection.user, account: obj.CloneSafeUser(user2), action: 'accountcreate', msg: 'Domain account created, user ' + req.connection.user, domain: domain.id };
2287 + if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to create the user. Another event will come.
2288 + obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
2289 + parent.debug('web', 'handleRootRequestEx: SSPI new domain user.');
2290 + }
2291 + }
2292 + }
2293 +
2294 + // Figure out the minimal password requirement
2295 + var passRequirements = null;
2296 + if (domain.passwordrequirements != null) {
2297 + if (domain.passrequirementstr == null) {
2298 + var passRequirements = {};
2299 + if (typeof domain.passwordrequirements.min == 'number') { passRequirements.min = domain.passwordrequirements.min; }
2300 + if (typeof domain.passwordrequirements.max == 'number') { passRequirements.max = domain.passwordrequirements.max; }
2301 + if (typeof domain.passwordrequirements.upper == 'number') { passRequirements.upper = domain.passwordrequirements.upper; }
2302 + if (typeof domain.passwordrequirements.lower == 'number') { passRequirements.lower = domain.passwordrequirements.lower; }
2303 + if (typeof domain.passwordrequirements.numeric == 'number') { passRequirements.numeric = domain.passwordrequirements.numeric; }
2304 + if (typeof domain.passwordrequirements.nonalpha == 'number') { passRequirements.nonalpha = domain.passwordrequirements.nonalpha; }
2305 + domain.passwordrequirementsstr = encodeURIComponent(JSON.stringify(passRequirements));
2306 + }
2307 + passRequirements = domain.passwordrequirementsstr;
2308 + }
2309 +
2310 + // If a user exists and is logged in, serve the default app, otherwise server the login app.
2311 + if (req.session && req.session.userid && obj.users[req.session.userid]) {
2312 + var user = obj.users[req.session.userid];
2313 + if (req.session.domainid != domain.id) { // Check if the session is for the correct domain
2314 + parent.debug('web', 'handleRootRequestEx: incorrect domain.');
2315 + req.session = null;
2316 + res.redirect(domain.url + getQueryPortion(req)); // BAD***
2317 + return;
2318 + }
2319 +
2320 + // Check if this is a locked account
2321 + if ((user.siteadmin != null) && ((user.siteadmin & 32) != 0) && (user.siteadmin != 0xFFFFFFFF)) {
2322 + // Locked account
2323 + parent.debug('web', 'handleRootRequestEx: locked account.');
2324 + delete req.session.userid;
2325 + delete req.session.domainid;
2326 + delete req.session.currentNode;
2327 + delete req.session.passhint;
2328 + delete req.session.cuserid;
2329 + req.session.messageid = 110; // Account locked.
2330 + res.redirect(domain.url + getQueryPortion(req)); // BAD***
2331 + return;
2332 + }
2333 +
2334 + var viewmode = 1;
2335 + if (req.session.viewmode) {
2336 + viewmode = req.session.viewmode;
2337 + delete req.session.viewmode;
2338 + } else if (req.query.viewmode) {
2339 + viewmode = req.query.viewmode;
2340 + }
2341 + var currentNode = '';
2342 + if (req.session.currentNode) {
2343 + currentNode = req.session.currentNode;
2344 + delete req.session.currentNode;
2345 + } else if (req.query.node) {
2346 + currentNode = 'node/' + domain.id + '/' + req.query.node;
2347 + }
2348 + var logoutcontrols = {};
2349 + if (obj.args.nousers != true) { logoutcontrols.name = user.name; }
2350 +
2351 + // Give the web page a list of supported server features
2352 + features = 0;
2353 + features2 = 0;
2354 + if (obj.args.wanonly == true) { features += 0x00000001; } // WAN-only mode
2355 + if (obj.args.lanonly == true) { features += 0x00000002; } // LAN-only mode
2356 + if (obj.args.nousers == true) { features += 0x00000004; } // Single user mode
2357 + if (domain.userQuota == -1) { features += 0x00000008; } // No server files mode
2358 + if (obj.args.mpstlsoffload) { features += 0x00000010; } // No mutual-auth CIRA
2359 + if ((parent.config.settings.allowframing != null) || (domain.allowframing != null)) { features += 0x00000020; } // Allow site within iframe
2360 + if ((obj.parent.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true)) { features += 0x00000040; } // Email invites
2361 + if (obj.args.webrtc == true) { features += 0x00000080; } // Enable WebRTC (Default false for now)
2362 + // 0x00000100 --> This feature flag is free for future use.
2363 + if (obj.args.allowhighqualitydesktop !== false) { features += 0x00000200; } // Enable AllowHighQualityDesktop (Default true)
2364 + if ((obj.args.lanonly == true) || (obj.args.mpsport == 0)) { features += 0x00000400; } // No CIRA
2365 + if ((obj.parent.serverSelfWriteAllowed == true) && (user != null) && (user.siteadmin == 0xFFFFFFFF)) { features += 0x00000800; } // Server can self-write (Allows self-update)
2366 + if ((parent.config.settings.no2factorauth !== true) && (domain.auth != 'sspi') && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.nousers !== true) && (user._id.split('/')[2][0] != '~')) { features += 0x00001000; } // 2FA login supported
2367 + if (domain.agentnoproxy === true) { features += 0x00002000; } // Indicates that agents should be installed without using a HTTP proxy
2368 + if ((parent.config.settings.no2factorauth !== true) && domain.yubikey && domain.yubikey.id && domain.yubikey.secret && (user._id.split('/')[2][0] != '~')) { features += 0x00004000; } // Indicates Yubikey support
2369 + if (domain.geolocation == true) { features += 0x00008000; } // Enable geo-location features
2370 + if ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true)) { features += 0x00010000; } // Enable password hints
2371 + if (parent.config.settings.no2factorauth !== true) { features += 0x00020000; } // Enable WebAuthn/FIDO2 support
2372 + if ((obj.args.nousers != true) && (domain.passwordrequirements != null) && (domain.passwordrequirements.force2factor === true) && (user._id.split('/')[2][0] != '~')) {
2373 + // Check if we can skip 2nd factor auth because of the source IP address
2374 + var skip2factor = false;
2375 + if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
2376 + for (var i in domain.passwordrequirements.skip2factor) {
2377 + if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) { skip2factor = true; }
2378 + }
2379 + }
2380 + if (skip2factor == false) { features += 0x00040000; } // Force 2-factor auth
2381 + }
2382 + if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { features += 0x00080000; } // LDAP or SSPI in use, warn that users must login first before adding a user to a group.
2383 + if (domain.amtacmactivation) { features += 0x00100000; } // Intel AMT ACM activation/upgrade is possible
2384 + if (domain.usernameisemail) { features += 0x00200000; } // Username is email address
2385 + if (parent.mqttbroker != null) { features += 0x00400000; } // This server supports MQTT channels
2386 + if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (parent.mailserver != null)) { features += 0x00800000; } // using email for 2FA is allowed
2387 + if (domain.agentinvitecodes == true) { features += 0x01000000; } // Support for agent invite codes
2388 + if (parent.smsserver != null) { features += 0x02000000; } // SMS messaging is supported
2389 + if ((parent.smsserver != null) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false))) { features += 0x04000000; } // SMS 2FA is allowed
2390 + if (domain.sessionrecording != null) { features += 0x08000000; } // Server recordings enabled
2391 + if (domain.urlswitching === false) { features += 0x10000000; } // Disables the URL switching feature
2392 + if (domain.novnc === false) { features += 0x20000000; } // Disables noVNC
2393 + if (domain.mstsc !== true) { features += 0x40000000; } // Disables MSTSC.js
2394 + if (obj.isTrustedCert(domain) == false) { features += 0x80000000; } // Indicate we are not using a trusted certificate
2395 + if (obj.parent.amtManager != null) { features2 += 1; } // Indicates that the Intel AMT manager is active
2396 +
2397 + // Create a authentication cookie
2398 + const authCookie = obj.parent.encodeCookie({ userid: user._id, domainid: domain.id, ip: req.clientIp }, obj.parent.loginCookieEncryptionKey);
2399 + const authRelayCookie = obj.parent.encodeCookie({ ruserid: user._id, domainid: domain.id }, obj.parent.loginCookieEncryptionKey);
2400 +
2401 + // Send the main web application
2402 + var extras = (req.query.key != null) ? ('&key=' + req.query.key) : '';
2403 + if ((!obj.args.user) && (obj.args.nousers != true) && (nologout == false)) { logoutcontrols.logoutUrl = (domain.url + 'logout?' + Math.random() + extras); } // If a default user is in use or no user mode, don't display the logout button
2404 + var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
2405 +
2406 + // Clean up the U2F challenge if needed
2407 + if (req.session.u2fchallenge) { delete req.session.u2fchallenge; };
2408 +
2409 + // Intel AMT Scanning options
2410 + var amtscanoptions = '';
2411 + if (typeof domain.amtscanoptions == 'string') { amtscanoptions = encodeURIComponent(domain.amtscanoptions); }
2412 + else if (obj.common.validateStrArray(domain.amtscanoptions)) { domain.amtscanoptions = domain.amtscanoptions.join(','); amtscanoptions = encodeURIComponent(domain.amtscanoptions); }
2413 +
2414 + // Fetch the web state
2415 + parent.debug('web', 'handleRootRequestEx: success.');
2416 + obj.db.Get('ws' + user._id, function (err, states) {
2417 + var webstate = '';
2418 + if ((err == null) && (states != null) && (Array.isArray(states))) {
2419 + webstate = (states.length == 1) ? obj.filterUserWebState(states[0].state) : '';
2420 + if ((webstate == '') && (typeof domain.defaultuserwebstate == 'object')) { webstate = JSON.stringify(domain.defaultuserwebstate); } // User has no web state, use defaults.
2421 + if (typeof domain.forceduserwebstate == 'object') { // Forces initial user web state is present, use it.
2422 + var webstate2 = {};
2423 + try { if (webstate != '') { webstate2 = JSON.parse(webstate); } } catch (ex) { }
2424 + for (var i in domain.forceduserwebstate) { webstate2[i] = domain.forceduserwebstate[i]; }
2425 + webstate = JSON.stringify(webstate2);
2426 + }
2427 + }
2428 +
2429 + // Custom user interface
2430 + var customui = '';
2431 + if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); }
2432 +
2433 + // Server features
2434 + var serverFeatures = 127;
2435 + if (domain.myserver === false) { serverFeatures = 0; } // 64 = Show "My Server" tab
2436 + else if (typeof domain.myserver == 'object') {
2437 + if (domain.myserver.backup !== true) { serverFeatures -= 1; } // Disallow simple server backups
2438 + if (domain.myserver.restore !== true) { serverFeatures -= 2; } // Disallow simple server restore
2439 + if (domain.myserver.upgrade !== true) { serverFeatures -= 4; } // Disallow server upgrade
2440 + if (domain.myserver.errorlog !== true) { serverFeatures -= 8; } // Disallow show server crash log
2441 + if (domain.myserver.console !== true) { serverFeatures -= 16; } // Disallow server console
2442 + if (domain.myserver.trace !== true) { serverFeatures -= 32; } // Disallow server tracing
2443 + }
2444 + if (obj.db.databaseType != 1) { // If not using NeDB, we can't backup using the simple system.
2445 + if ((serverFeatures & 1) != 0) { serverFeatures -= 1; } // Disallow server backups
2446 + if ((serverFeatures & 2) != 0) { serverFeatures -= 2; } // Disallow simple server restore
2447 + }
2448 +
2449 + // Refresh the session
2450 + render(req, res, getRenderPage('default', req, domain), getRenderArgs({
2451 + authCookie: authCookie,
2452 + authRelayCookie: authRelayCookie,
2453 + viewmode: viewmode,
2454 + currentNode: currentNode,
2455 + logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27'),
2456 + domain: domain.id,
2457 + debuglevel: parent.debugLevel,
2458 + serverDnsName: obj.getWebServerName(domain),
2459 + serverRedirPort: args.redirport,
2460 + serverPublicPort: httpsPort,
2461 + serverfeatures: serverFeatures,
2462 + features: features,
2463 + features2: features2,
2464 + sessiontime: (args.sessiontime) ? args.sessiontime : 60,
2465 + mpspass: args.mpspass,
2466 + passRequirements: passRequirements,
2467 + customui: customui,
2468 + webcerthash: Buffer.from(obj.webCertificateFullHashs[domain.id], 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'),
2469 + footer: (domain.footer == null) ? '' : domain.footer,
2470 + webstate: encodeURIComponent(webstate).replace(/'/g, '%27'),
2471 + amtscanoptions: amtscanoptions,
2472 + pluginHandler: (parent.pluginHandler == null) ? 'null' : parent.pluginHandler.prepExports()
2473 + }, req, domain));
2474 + });
2475 + } else {
2476 + // Send back the login application
2477 + // If this is a 2 factor auth request, look for a hardware key challenge.
2478 + // Normal login 2 factor request
2479 + if (req.session && (req.session.loginmode == '4') && (req.session.tokenuserid)) {
2480 + var user = obj.users[req.session.tokenuserid];
2481 + if (user != null) {
2482 + parent.debug('web', 'handleRootRequestEx: sending 2FA challenge.');
2483 + getHardwareKeyChallenge(req, domain, user, function (hwchallenge) { handleRootRequestLogin(req, res, domain, hwchallenge, passRequirements); });
2484 + return;
2485 + }
2486 + }
2487 + // Password recovery 2 factor request
2488 + if (req.session && (req.session.loginmode == '5') && (req.session.tokenemail)) {
2489 + obj.db.GetUserWithVerifiedEmail(domain.id, req.session.tokenemail, function (err, docs) {
2490 + if ((err != null) || (docs.length == 0)) {
2491 + parent.debug('web', 'handleRootRequestEx: password recover 2FA fail.');
2492 + req.session = null;
2493 + res.redirect(domain.url + getQueryPortion(req)); // BAD***
2494 + } else {
2495 + var user = obj.users[docs[0]._id];
2496 + if (user != null) {
2497 + parent.debug('web', 'handleRootRequestEx: password recover 2FA challenge.');
2498 + getHardwareKeyChallenge(req, domain, user, function (hwchallenge) { handleRootRequestLogin(req, res, domain, hwchallenge, passRequirements); });
2499 + } else {
2500 + parent.debug('web', 'handleRootRequestEx: password recover 2FA no user.');
2501 + req.session = null;
2502 + res.redirect(domain.url + getQueryPortion(req)); // BAD***
2503 + }
2504 + }
2505 + });
2506 + return;
2507 + }
2508 + handleRootRequestLogin(req, res, domain, '', passRequirements);
2509 + }
2510 + }
2511 +
2512 + function handleRootRequestLogin(req, res, domain, hardwareKeyChallenge, passRequirements) {
2513 + parent.debug('web', 'handleRootRequestLogin()');
2514 + var features = 0;
2515 + if ((parent.config != null) && (parent.config.settings != null) && ((parent.config.settings.allowframing == true) || (typeof parent.config.settings.allowframing == 'string'))) { features += 32; } // Allow site within iframe
2516 + if (domain.usernameisemail) { features += 0x00200000; } // Username is email address
2517 + var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
2518 + var loginmode = '';
2519 + if (req.session) { loginmode = req.session.loginmode; delete req.session.loginmode; } // Clear this state, if the user hits refresh, we want to go back to the login page.
2520 +
2521 + // Format an error message if needed
2522 + var passhint = null, msgid = 0;
2523 + if (req.session != null) {
2524 + msgid = req.session.messageid;
2525 + if ((loginmode == '7') || ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true))) { passhint = EscapeHtml(req.session.passhint); }
2526 + delete req.session.messageid;
2527 + delete req.session.passhint;
2528 + }
2529 + var emailcheck = ((obj.parent.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap'))
2530 +
2531 + // Check if we are allowed to create new users using the login screen
2532 + var newAccountsAllowed = true;
2533 + if ((domain.newaccounts !== 1) && (domain.newaccounts !== true)) { for (var i in obj.users) { if (obj.users[i].domain == domain.id) { newAccountsAllowed = false; break; } } }
2534 + if (parent.config.settings.maintenancemode != null) { newAccountsAllowed = false; }
2535 +
2536 + // Encrypt the hardware key challenge state if needed
2537 + var hwstate = null;
2538 + if (hardwareKeyChallenge) { hwstate = obj.parent.encodeCookie({ u: req.session.tokenusername, p: req.session.tokenpassword, c: req.session.u2fchallenge }, obj.parent.loginCookieEncryptionKey) }
2539 +
2540 + // Check if we can use OTP tokens with email. We can't use email for 2FA password recovery (loginmode 5).
2541 + var otpemail = (loginmode != 5) && (parent.mailserver != null) && (req.session != null) && ((req.session.tokenemail == true) || (typeof req.session.tokenemail == 'string'));
2542 + if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; }
2543 + var otpsms = (parent.smsserver != null) && (req.session != null) && (req.session.tokensms == true);
2544 + if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; }
2545 +
2546 + // See if we support two-factor trusted cookies
2547 + var twoFactorCookieDays = 30;
2548 + if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
2549 +
2550 + // See what authentication strategies we have
2551 + var authStrategies = [];
2552 + if (typeof domain.authstrategies == 'object') {
2553 + if (typeof domain.authstrategies.twitter == 'object') { authStrategies.push('twitter'); }
2554 + if (typeof domain.authstrategies.google == 'object') { authStrategies.push('google'); }
2555 + if (typeof domain.authstrategies.github == 'object') { authStrategies.push('github'); }
2556 + if (typeof domain.authstrategies.reddit == 'object') { authStrategies.push('reddit'); }
2557 + if (typeof domain.authstrategies.azure == 'object') { authStrategies.push('azure'); }
2558 + if (typeof domain.authstrategies.intel == 'object') { authStrategies.push('intel'); }
2559 + if (typeof domain.authstrategies.jumpcloud == 'object') { authStrategies.push('jumpcloud'); }
2560 + if (typeof domain.authstrategies.saml == 'object') { authStrategies.push('saml'); }
2561 + }
2562 +
2563 + // Custom user interface
2564 + var customui = '';
2565 + if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); }
2566 +
2567 + // Render the login page
2568 + render(req, res,
2569 + getRenderPage((domain.sitestyle == 2) ? 'login2' : 'login', req, domain),
2570 + getRenderArgs({
2571 + loginmode: loginmode,
2572 + rootCertLink: getRootCertLink(),
2573 + newAccount: newAccountsAllowed,
2574 + newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1),
2575 + serverDnsName: obj.getWebServerName(domain),
2576 + serverPublicPort: httpsPort,
2577 + passlogin: (typeof domain.showpasswordlogin == 'boolean') ? domain.showpasswordlogin : true,
2578 + emailcheck: emailcheck,
2579 + features: features,
2580 + sessiontime: (args.sessiontime) ? args.sessiontime : 60,
2581 + passRequirements: passRequirements,
2582 + customui: customui,
2583 + footer: (domain.loginfooter == null) ? '' : domain.loginfooter,
2584 + hkey: encodeURIComponent(hardwareKeyChallenge).replace(/'/g, '%27'),
2585 + messageid: msgid,
2586 + passhint: passhint,
2587 + welcometext: domain.welcometext ? encodeURIComponent(domain.welcometext).split('\'').join('\\\'') : null,
2588 + hwstate: hwstate,
2589 + otpemail: otpemail,
2590 + otpsms: otpsms,
2591 + twoFactorCookieDays: twoFactorCookieDays,
2592 + authStrategies: authStrategies.join(','),
2593 + loginpicture: (typeof domain.loginpicture == 'string')
2594 + }, req, domain, (domain.sitestyle == 2) ? 'login2' : 'login'));
2595 + }
2596 +
2597 + // Handle a post request on the root
2598 + function handleRootPostRequest(req, res) {
2599 + const domain = checkUserIpAddress(req, res);
2600 + if (domain == null) { return; }
2601 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.end("Not Found"); return; } // Check 3FA URL key
2602 + parent.debug('web', 'handleRootPostRequest, action: ' + req.body.action);
2603 +
2604 + switch (req.body.action) {
2605 + case 'login': { handleLoginRequest(req, res, true); break; }
2606 + case 'tokenlogin': {
2607 + if (req.body.hwstate) {
2608 + var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 10);
2609 + if (cookie != null) { req.session.tokenusername = cookie.u; req.session.tokenpassword = cookie.p; req.session.u2fchallenge = cookie.c; }
2610 + }
2611 + handleLoginRequest(req, res, true); break;
2612 + }
2613 + case 'changepassword': { handlePasswordChangeRequest(req, res, true); break; }
2614 + case 'deleteaccount': { handleDeleteAccountRequest(req, res, true); break; }
2615 + case 'createaccount': { handleCreateAccountRequest(req, res, true); break; }
2616 + case 'resetpassword': { handleResetPasswordRequest(req, res, true); break; }
2617 + case 'resetaccount': { handleResetAccountRequest(req, res, true); break; }
2618 + case 'checkemail': { handleCheckAccountEmailRequest(req, res, true); break; }
2619 + default: { handleLoginRequest(req, res, true); break; }
2620 + }
2621 + }
2622 +
2623 + // Return true if it looks like we are using a real TLS certificate.
2624 + obj.isTrustedCert = function (domain) {
2625 + if ((domain != null) && (typeof domain.trustedcert == 'boolean')) return domain.trustedcert; // If the status of the cert specified, use that.
2626 + if (typeof obj.args.trustedcert == 'boolean') return obj.args.trustedcert; // If the status of the cert specified, use that.
2627 + if (obj.args.tlsoffload != null) return true; // We are using TLS offload, a real cert is likely used.
2628 + if (obj.parent.config.letsencrypt != null) return (obj.parent.config.letsencrypt.production === true); // We are using Let's Encrypt, real cert in use if production is set to true.
2629 + if (obj.certificates.WebIssuer.indexOf('MeshCentralRoot-') == 0) return false; // Our cert is issued by self-signed cert.
2630 + if (obj.certificates.CommonName.indexOf('.') == -1) return false; // Our cert is named with a fake name
2631 + return true; // This is a guess
2632 + }
2633 +
2634 + // Get the link to the root certificate if needed
2635 + function getRootCertLink() {
2636 + // Check if the HTTPS certificate is issued from MeshCentralRoot, if so, add download link to root certificate.
2637 + if ((obj.args.tlsoffload == null) && (obj.parent.config.letsencrypt == null) && (obj.tlsSniCredentials == null) && (obj.certificates.WebIssuer.indexOf('MeshCentralRoot-') == 0) && (obj.certificates.CommonName.indexOf('.') != -1)) { return '<a href=/MeshServerRootCert.cer title="Download the root certificate for this server">Root Certificate</a>'; }
2638 + return '';
2639 + }
2640 +
2641 + // Serve the xterm page
2642 + function handleXTermRequest(req, res) {
2643 + const domain = checkUserIpAddress(req, res);
2644 + if (domain == null) { return; }
2645 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2646 +
2647 + parent.debug('web', 'handleXTermRequest: sending xterm');
2648 + res.set({ 'Cache-Control': 'no-store' });
2649 + if (req.session && req.session.userid) {
2650 + if (req.session.domainid != domain.id) { res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain
2651 + var user = obj.users[req.session.userid];
2652 + if ((user == null) || (req.query.nodeid == null)) { res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the user exists
2653 +
2654 + // Check permissions
2655 + obj.GetNodeWithRights(domain, user, req.query.nodeid, function (node, rights, visible) {
2656 + if ((node == null) || ((rights & 8) == 0) || ((rights != 0xFFFFFFFF) && ((rights & 512) != 0))) { res.redirect(domain.url + getQueryPortion(req)); return; }
2657 +
2658 + var logoutcontrols = { name: user.name };
2659 + var extras = (req.query.key != null) ? ('&key=' + req.query.key) : '';
2660 + if ((domain.ldap == null) && (domain.sspi == null) && (obj.args.user == null) && (obj.args.nousers != true)) { logoutcontrols.logoutUrl = (domain.url + 'logout?' + Math.random() + extras); } // If a default user is in use or no user mode, don't display the logout button
2661 +
2662 + // Create a authentication cookie
2663 + const authCookie = obj.parent.encodeCookie({ userid: user._id, domainid: domain.id, ip: req.clientIp }, obj.parent.loginCookieEncryptionKey);
2664 + const authRelayCookie = obj.parent.encodeCookie({ ruserid: user._id, domainid: domain.id }, obj.parent.loginCookieEncryptionKey);
2665 + var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
2666 + render(req, res, getRenderPage('xterm', req, domain), getRenderArgs({ serverDnsName: obj.getWebServerName(domain), serverRedirPort: args.redirport, serverPublicPort: httpsPort, authCookie: authCookie, authRelayCookie: authRelayCookie, logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27'), name: EscapeHtml(node.name) }, req, domain));
2667 + });
2668 + } else {
2669 + res.redirect(domain.url + getQueryPortion(req));
2670 + return;
2671 + }
2672 + }
2673 +
2674 + // Render the terms of service.
2675 + function handleTermsRequest(req, res) {
2676 + const domain = checkUserIpAddress(req, res);
2677 + if (domain == null) { return; }
2678 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2679 +
2680 + // See if term.txt was loaded from the database
2681 + if ((parent.configurationFiles != null) && (parent.configurationFiles['terms.txt'] != null)) {
2682 + // Send the terms from the database
2683 + res.set({ 'Cache-Control': 'no-store' });
2684 + if (req.session && req.session.userid) {
2685 + if (req.session.domainid != domain.id) { req.session = null; res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain
2686 + var user = obj.users[req.session.userid];
2687 + var logoutcontrols = { name: user.name };
2688 + var extras = (req.query.key != null) ? ('&key=' + req.query.key) : '';
2689 + if ((domain.ldap == null) && (domain.sspi == null) && (obj.args.user == null) && (obj.args.nousers != true)) { logoutcontrols.logoutUrl = (domain.url + 'logout?' + Math.random() + extras); } // If a default user is in use or no user mode, don't display the logout button
2690 + render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(parent.configurationFiles['terms.txt'].toString()).split('\'').join('\\\''), logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain));
2691 + } else {
2692 + render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(parent.configurationFiles['terms.txt'].toString()).split('\'').join('\\\''), logoutControls: encodeURIComponent('{}') }, req, domain));
2693 + }
2694 + } else {
2695 + // See if there is a terms.txt file in meshcentral-data
2696 + var p = obj.path.join(obj.parent.datapath, 'terms.txt');
2697 + if (obj.fs.existsSync(p)) {
2698 + obj.fs.readFile(p, 'utf8', function (err, data) {
2699 + if (err != null) { parent.debug('web', 'handleTermsRequest: no terms.txt'); res.sendStatus(404); return; }
2700 +
2701 + // Send the terms from terms.txt
2702 + res.set({ 'Cache-Control': 'no-store' });
2703 + if (req.session && req.session.userid) {
2704 + if (req.session.domainid != domain.id) { req.session = null; res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain
2705 + var user = obj.users[req.session.userid];
2706 + var logoutcontrols = { name: user.name };
2707 + var extras = (req.query.key != null) ? ('&key=' + req.query.key) : '';
2708 + if ((domain.ldap == null) && (domain.sspi == null) && (obj.args.user == null) && (obj.args.nousers != true)) { logoutcontrols.logoutUrl = (domain.url + 'logout?' + Math.random() + extras); } // If a default user is in use or no user mode, don't display the logout button
2709 + render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(data).split('\'').join('\\\''), logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain));
2710 + } else {
2711 + render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(data).split('\'').join('\\\''), logoutControls: encodeURIComponent('{}') }, req, domain));
2712 + }
2713 + });
2714 + } else {
2715 + // Send the default terms
2716 + parent.debug('web', 'handleTermsRequest: sending default terms');
2717 + res.set({ 'Cache-Control': 'no-store' });
2718 + if (req.session && req.session.userid) {
2719 + if (req.session.domainid != domain.id) { req.session = null; res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain
2720 + var user = obj.users[req.session.userid];
2721 + var logoutcontrols = { name: user.name };
2722 + var extras = (req.query.key != null) ? ('&key=' + req.query.key) : '';
2723 + if ((domain.ldap == null) && (domain.sspi == null) && (obj.args.user == null) && (obj.args.nousers != true)) { logoutcontrols.logoutUrl = (domain.url + 'logout?' + Math.random() + extras); } // If a default user is in use or no user mode, don't display the logout button
2724 + render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain));
2725 + } else {
2726 + render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ logoutControls: encodeURIComponent('{}') }, req, domain));
2727 + }
2728 + }
2729 + }
2730 + }
2731 +
2732 + // Render the messenger application.
2733 + function handleMessengerRequest(req, res) {
2734 + const domain = getDomain(req);
2735 + if (domain == null) { parent.debug('web', 'handleMessengerRequest: no domain'); res.sendStatus(404); return; }
2736 + parent.debug('web', 'handleMessengerRequest()');
2737 +
2738 + // Check if we are in maintenance mode
2739 + if (parent.config.settings.maintenancemode != null) {
2740 + render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 3, msgid: 13, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain));
2741 + return;
2742 + }
2743 +
2744 + var webRtcConfig = null;
2745 + if (obj.parent.config.settings && obj.parent.config.settings.webrtconfig && (typeof obj.parent.config.settings.webrtconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(obj.parent.config.settings.webrtconfig)).replace(/'/g, '%27'); }
2746 + else if (args.webrtconfig && (typeof args.webrtconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(args.webrtconfig)).replace(/'/g, '%27'); }
2747 + res.set({ 'Cache-Control': 'no-store' });
2748 + render(req, res, getRenderPage('messenger', req, domain), getRenderArgs({ webrtconfig: webRtcConfig }, req, domain));
2749 + }
2750 +
2751 + // Returns the server root certificate encoded in base64
2752 + function getRootCertBase64() {
2753 + var rootcert = obj.certificates.root.cert;
2754 + var i = rootcert.indexOf('-----BEGIN CERTIFICATE-----\r\n');
2755 + if (i >= 0) { rootcert = rootcert.substring(i + 29); }
2756 + i = rootcert.indexOf('-----END CERTIFICATE-----');
2757 + if (i >= 0) { rootcert = rootcert.substring(i, 0); }
2758 + return Buffer.from(rootcert, 'base64').toString('base64');
2759 + }
2760 +
2761 + // Returns the mesh server root certificate
2762 + function handleRootCertRequest(req, res) {
2763 + const domain = getDomain(req);
2764 + if (domain == null) { parent.debug('web', 'handleRootCertRequest: no domain'); res.sendStatus(404); return; }
2765 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2766 + if ((obj.userAllowedIp != null) && (checkIpAddressEx(req, res, obj.userAllowedIp, false) === false)) { parent.debug('web', 'handleRootCertRequest: invalid ip'); return; } // Check server-wide IP filter only.
2767 + parent.debug('web', 'handleRootCertRequest()');
2768 + setContentDispositionHeader(res, 'application/octet-stream', certificates.RootName + '.cer', null, 'rootcert.cer');
2769 + res.send(Buffer.from(getRootCertBase64(), 'base64'));
2770 + }
2771 +
2772 + // Handle user public file downloads
2773 + function handleDownloadUserFiles(req, res) {
2774 + const domain = checkUserIpAddress(req, res);
2775 + if (domain == null) { return; }
2776 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2777 +
2778 + if (obj.common.validateString(req.path, 1, 4096) == false) { res.sendStatus(404); return; }
2779 + var domainname = 'domain', spliturl = decodeURIComponent(req.path).split('/'), filename = '';
2780 + if ((spliturl.length < 3) || (obj.common.IsFilenameValid(spliturl[2]) == false) || (domain.userQuota == -1)) { res.sendStatus(404); return; }
2781 + if (domain.id != '') { domainname = 'domain-' + domain.id; }
2782 + var path = obj.path.join(obj.filespath, domainname + '/user-' + spliturl[2] + '/Public');
2783 + for (var i = 3; i < spliturl.length; i++) { if (obj.common.IsFilenameValid(spliturl[i]) == true) { path += '/' + spliturl[i]; filename = spliturl[i]; } else { res.sendStatus(404); return; } }
2784 +
2785 + var stat = null;
2786 + try { stat = obj.fs.statSync(path); } catch (e) { }
2787 + if ((stat != null) && ((stat.mode & 0x004000) == 0)) {
2788 + if (req.query.download == 1) {
2789 + setContentDispositionHeader(res, 'application/octet-stream', filename, null, 'file.bin');
2790 + try { res.sendFile(obj.path.resolve(__dirname, path)); } catch (e) { res.sendStatus(404); }
2791 + } else {
2792 + render(req, res, getRenderPage((domain.sitestyle == 2) ? 'download2' : 'download', req, domain), getRenderArgs({ rootCertLink: getRootCertLink(), messageid: 1, fileurl: req.path + '?download=1', filename: filename, filesize: stat.size }, req, domain));
2793 + }
2794 + } else {
2795 + render(req, res, getRenderPage((domain.sitestyle == 2) ? 'download2' : 'download', req, domain), getRenderArgs({ rootCertLink: getRootCertLink(), messageid: 2 }, req, domain));
2796 + }
2797 + }
2798 +
2799 + // Handle device file request
2800 + function handleDeviceFile(req, res) {
2801 + const domain = checkUserIpAddress(req, res);
2802 + if (domain == null) { return; }
2803 + if ((req.query.c == null) || (req.query.m == null) || (req.query.n == null) || (req.query.f == null)) { res.sendStatus(404); return; }
2804 +
2805 + // Check the inbound desktop sharing cookie
2806 + var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
2807 + if ((c == null) || (c.domainid !== domain.id)) { res.sendStatus(404); return; }
2808 +
2809 + // Check userid
2810 + const user = obj.users[c.userid];
2811 + if ((c == user)) { res.sendStatus(404); return; }
2812 +
2813 + // Check if this user has permission to manage this computer
2814 + const meshid = 'mesh/' + domain.id + '/' + req.query.m;
2815 + const nodeid = 'node/' + domain.id + '/' + req.query.n;
2816 + if ((obj.GetNodeRights(c.userid, meshid, nodeid) & MESHRIGHT_REMOTECONTROL) == 0) { res.sendStatus(404); return; }
2817 +
2818 + // All good, start the file transfer
2819 + req.query.id = getRandomLowerCase(12);
2820 + obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, null, res, req, domain, user, meshid, nodeid);
2821 + }
2822 +
2823 + // Handle download of a server file by an agent
2824 + function handleAgentDownloadFile(req, res) {
2825 + const domain = checkUserIpAddress(req, res);
2826 + if (domain == null) { return; }
2827 + if (req.query.c == null) { res.sendStatus(404); return; }
2828 +
2829 + // Check the inbound desktop sharing cookie
2830 + var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 5); // 5 minute timeout
2831 + if ((c == null) || (c.a != 'tmpdl') || (c.d != domain.id) || (c.nid == null) || (c.f == null) || (obj.common.IsFilenameValid(c.f) == false)) { res.sendStatus(404); return; }
2832 +
2833 + // Send the file back
2834 + try { res.sendFile(obj.path.join(obj.filespath, 'tmp', c.f)); return; } catch (ex) { res.sendStatus(404); }
2835 + }
2836 +
2837 + // Handle logo request
2838 + function handleLogoRequest(req, res) {
2839 + const domain = checkUserIpAddress(req, res);
2840 + if (domain == null) { return; }
2841 +
2842 + //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
2843 + if (domain.titlepicture) {
2844 + if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.titlepicture] != null)) {
2845 + // Use the logo in the database
2846 + res.set({ 'Content-Type': domain.titlepicture.toLowerCase().endsWith('.png')?'image/png':'image/jpeg' });
2847 + res.send(parent.configurationFiles[domain.titlepicture]);
2848 + return;
2849 + } else {
2850 + // Use the logo on file
2851 + try { res.sendFile(obj.path.join(obj.parent.datapath, domain.titlepicture)); return; } catch (ex) { }
2852 + }
2853 + }
2854 +
2855 + if ((domain.webpublicpath != null) && (obj.fs.existsSync(obj.path.join(domain.webpublicpath, 'images/logoback.png')))) {
2856 + // Use the domain logo picture
2857 + try { res.sendFile(obj.path.join(domain.webpublicpath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
2858 + } else if (parent.webPublicOverridePath && obj.fs.existsSync(obj.path.join(obj.parent.webPublicOverridePath, 'images/logoback.png'))) {
2859 + // Use the override logo picture
2860 + try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
2861 + } else {
2862 + // Use the default logo picture
2863 + try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
2864 + }
2865 + }
2866 +
2867 + // Handle login logo request
2868 + function handleLoginLogoRequest(req, res) {
2869 + const domain = checkUserIpAddress(req, res);
2870 + if (domain == null) { return; }
2871 +
2872 + //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
2873 + if (domain.loginpicture) {
2874 + if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.loginpicture] != null)) {
2875 + // Use the logo in the database
2876 + res.set({ 'Content-Type': domain.loginpicture.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
2877 + res.send(parent.configurationFiles[domain.loginpicture]);
2878 + return;
2879 + } else {
2880 + // Use the logo on file
2881 + try { res.sendFile(obj.path.join(obj.parent.datapath, domain.loginpicture)); return; } catch (ex) { res.sendStatus(404); }
2882 + }
2883 + } else {
2884 + res.sendStatus(404);
2885 + }
2886 + }
2887 +
2888 + // Handle translation request
2889 + function handleTranslationsRequest(req, res) {
2890 + const domain = checkUserIpAddress(req, res);
2891 + if (domain == null) { return; }
2892 + //if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2893 + if ((obj.userAllowedIp != null) && (checkIpAddressEx(req, res, obj.userAllowedIp, false) === false)) { return; } // Check server-wide IP filter only.
2894 +
2895 + var user = null;
2896 + if (obj.args.user != null) {
2897 + // A default user is active
2898 + user = obj.users['user/' + domain.id + '/' + obj.args.user];
2899 + if (!user) { parent.debug('web', 'handleTranslationsRequest: user not found.'); res.sendStatus(401); return; }
2900 + } else {
2901 + // Check if the user is logged and we have all required parameters
2902 + if (!req.session || !req.session.userid) { parent.debug('web', 'handleTranslationsRequest: failed checks (2).'); res.sendStatus(401); return; }
2903 +
2904 + // Get the current user
2905 + user = obj.users[req.session.userid];
2906 + if (!user) { parent.debug('web', 'handleTranslationsRequest: user not found.'); res.sendStatus(401); return; }
2907 + if (user.siteadmin != 0xFFFFFFFF) { parent.debug('web', 'handleTranslationsRequest: user not site administrator.'); res.sendStatus(401); return; }
2908 + }
2909 +
2910 + var data = '';
2911 + req.setEncoding('utf8');
2912 + req.on('data', function (chunk) { data += chunk; });
2913 + req.on('end', function () {
2914 + try { data = JSON.parse(data); } catch (ex) { data = null; }
2915 + if (data == null) { res.sendStatus(404); return; }
2916 + if (data.action == 'getTranslations') {
2917 + if (obj.fs.existsSync(obj.path.join(obj.parent.datapath, 'translate.json'))) {
2918 + // Return the translation file (JSON)
2919 + try { res.sendFile(obj.path.join(obj.parent.datapath, 'translate.json')); } catch (ex) { res.sendStatus(404); }
2920 + } else if (obj.fs.existsSync(obj.path.join(__dirname, 'translate', 'translate.json'))) {
2921 + // Return the default translation file (JSON)
2922 + try { res.sendFile(obj.path.join(__dirname, 'translate', 'translate.json')); } catch (ex) { res.sendStatus(404); }
2923 + } else { res.sendStatus(404); }
2924 + } else if (data.action == 'setTranslations') {
2925 + obj.fs.writeFile(obj.path.join(obj.parent.datapath, 'translate.json'), obj.common.translationsToJson({ strings: data.strings }), function (err) { if (err == null) { res.send(JSON.stringify({ response: 'ok' })); } else { res.send(JSON.stringify({ response: err })); } });
2926 + } else if (data.action == 'translateServer') {
2927 + if (obj.pendingTranslation === true) { res.send(JSON.stringify({ response: 'Server is already performing a translation.' })); return; }
2928 + const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
2929 + if (nodeVersion < 8) { res.send(JSON.stringify({ response: 'Server requires NodeJS 8.x or better.' })); return; }
2930 + var translateFile = obj.path.join(obj.parent.datapath, 'translate.json');
2931 + if (obj.fs.existsSync(translateFile) == false) { translateFile = obj.path.join(__dirname, 'translate', 'translate.json'); }
2932 + if (obj.fs.existsSync(translateFile) == false) { res.send(JSON.stringify({ response: 'Unable to find translate.js file on the server.' })); return; }
2933 + res.send(JSON.stringify({ response: 'ok' }));
2934 + console.log('Started server translation...');
2935 + obj.pendingTranslation = true;
2936 + require('child_process').exec('node translate.js translateall \"' + translateFile + '\"', { maxBuffer: 512000, timeout: 120000, cwd: obj.path.join(__dirname, 'translate') }, function (error, stdout, stderr) {
2937 + delete obj.pendingTranslation;
2938 + //console.log('error', error);
2939 + //console.log('stdout', stdout);
2940 + //console.log('stderr', stderr);
2941 + //console.log('Server restart...'); // Perform a server restart
2942 + //process.exit(0);
2943 + console.log('Server translation completed.');
2944 + });
2945 + } else {
2946 + // Unknown request
2947 + res.sendStatus(404);
2948 + }
2949 + });
2950 + }
2951 +
2952 + // Handle welcome image request
2953 + function handleWelcomeImageRequest(req, res) {
2954 + const domain = checkUserIpAddress(req, res);
2955 + if (domain == null) { return; }
2956 +
2957 + //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
2958 + if (domain.welcomepicture) {
2959 + if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.welcomepicture] != null)) {
2960 + // Use the welcome image in the database
2961 + res.set({ 'Content-Type': domain.welcomepicture.toLowerCase().endsWith('.png')?'image/png':'image/jpeg' });
2962 + res.send(parent.configurationFiles[domain.welcomepicture]);
2963 + return;
2964 + }
2965 +
2966 + // Use the configured logo picture
2967 + try { res.sendFile(obj.path.join(obj.parent.datapath, domain.welcomepicture)); return; } catch (ex) { }
2968 + }
2969 +
2970 + var imagefile = 'images/mainwelcome.jpg';
2971 + if (domain.sitestyle == 2) { imagefile = 'images/login/back.png'; }
2972 + if (domain.webpublicpath != null) {
2973 + obj.fs.exists(obj.path.join(domain.webpublicpath, imagefile), function (exists) {
2974 + if (exists) {
2975 + // Use the domain logo picture
2976 + try { res.sendFile(obj.path.join(domain.webpublicpath, imagefile)); } catch (ex) { res.sendStatus(404); }
2977 + } else {
2978 + // Use the default logo picture
2979 + try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
2980 + }
2981 + });
2982 + } else if (parent.webPublicOverridePath) {
2983 + obj.fs.exists(obj.path.join(obj.parent.webPublicOverridePath, imagefile), function (exists) {
2984 + if (exists) {
2985 + // Use the override logo picture
2986 + try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, imagefile)); } catch (ex) { res.sendStatus(404); }
2987 + } else {
2988 + // Use the default logo picture
2989 + try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
2990 + }
2991 + });
2992 + } else {
2993 + // Use the default logo picture
2994 + try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
2995 + }
2996 + }
2997 +
2998 + // Download a desktop recording
2999 + function handleGetRecordings(req, res) {
3000 + const domain = checkUserIpAddress(req, res);
3001 + if (domain == null) return;
3002 +
3003 + // Check the query
3004 + if ((domain.sessionrecording == null) || (req.query.file == null) || (obj.common.IsFilenameValid(req.query.file) !== true)) { res.sendStatus(401); return; }
3005 +
3006 + // Get the recording path
3007 + var recordingsPath = null;
3008 + if (domain.sessionrecording.filepath) { recordingsPath = domain.sessionrecording.filepath; } else { recordingsPath = parent.recordpath; }
3009 + if (recordingsPath == null) { res.sendStatus(401); return; }
3010 +
3011 + // Get the user and check user rights
3012 + var authUserid = null;
3013 + if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
3014 + if (authUserid == null) { res.sendStatus(401); return; }
3015 + const user = obj.users[authUserid];
3016 + if (user == null) { res.sendStatus(401); return; }
3017 + if ((user.siteadmin & 512) == 0) { res.sendStatus(401); return; } // Check if we have right to get recordings
3018 +
3019 + // Send the recorded file
3020 + setContentDispositionHeader(res, 'application/octet-stream', req.query.file, null, 'recording.mcrec');
3021 + try { res.sendFile(obj.path.join(recordingsPath, req.query.file)); } catch (ex) { res.sendStatus(404); }
3022 + }
3023 +
3024 + // Serve the player page
3025 + function handlePlayerRequest(req, res) {
3026 + const domain = checkUserIpAddress(req, res);
3027 + if (domain == null) { return; }
3028 +
3029 + parent.debug('web', 'handlePlayerRequest: sending player');
3030 + res.set({ 'Cache-Control': 'no-store' });
3031 + render(req, res, getRenderPage('player', req, domain), getRenderArgs({}, req, domain));
3032 + }
3033 +
3034 + // Serve the guest desktop page
3035 + function handleDesktopRequest(req, res) {
3036 + const domain = getDomain(req, res);
3037 + if (domain == null) { return; }
3038 + if (req.query.c == null) { res.sendStatus(404); return; }
3039 + if (domain.guestdevicesharing === false) { res.sendStatus(404); return; } // This feature is not allowed.
3040 +
3041 + // Check the inbound desktop sharing cookie
3042 + var c = obj.parent.decodeCookie(req.query.c, obj.parent.invitationLinkEncryptionKey, 60); // 60 minute timeout
3043 + if ((c == null) || (c.a !== 5) || ((c.p !== 2) && (c.p != null)) || (typeof c.uid != 'string') || (typeof c.nid != 'string') || (typeof c.gn != 'string') || (typeof c.cf != 'number') || (typeof c.start != 'number') || (typeof c.expire != 'number') || (typeof c.pid != 'string')) { res.sendStatus(404); return; }
3044 +
3045 + // Check the expired time, expire message.
3046 + if (c.expire <= Date.now()) { render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; }
3047 +
3048 + // Check the public id
3049 + obj.db.GetAllTypeNodeFiltered([c.nid], domain.id, 'deviceshare', null, function (err, docs) {
3050 + // Check if any desktop sharing links are present, expire message.
3051 + if ((err != null) || (docs.length == 0)) { render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; }
3052 +
3053 + // Search for the device share public identifier, expire message.
3054 + var found = false;
3055 + for (var i = 0; i < docs.length; i++) { if (docs[i].publicid == c.pid) { found = true; } }
3056 + if (found == false) { render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; }
3057 +
3058 + // Check the start time, not yet valid message.
3059 + if ((c.start > Date.now()) || (c.start > c.expire)) { render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 11, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; }
3060 +
3061 + // Looks good, let's create the outbound session cookies.
3062 + // Consent flags are 1 = Notify, 8 = Prompt, 64 = Privacy Bar.
3063 + const authCookie = obj.parent.encodeCookie({ userid: c.uid, domainid: domain.id, nid: c.nid, ip: req.clientIp, p: 2, gn: c.gn, cf: 65 | c.cf, r: 8, expire: c.expire, pid: c.pid, vo: c.vo }, obj.parent.loginCookieEncryptionKey);
3064 +
3065 + // Lets respond by sending out the desktop viewer.
3066 + var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
3067 + parent.debug('web', 'handleDesktopRequest: Sending guest desktop page for \"' + c.uid + '\", guest \"' + c.gn + '\".');
3068 + res.set({ 'Cache-Control': 'no-store' });
3069 + render(req, res, getRenderPage('desktop', req, domain), getRenderArgs({ authCookie: authCookie, authRelayCookie: '', domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), nodeid: c.nid, serverDnsName: obj.getWebServerName(domain), serverRedirPort: args.redirport, serverPublicPort: httpsPort, expire: c.expire, viewOnly: (c.vo == 1) ? 1 : 0 }, req, domain));
3070 + });
3071 + }
3072 +
3073 + // Serve the guest terminal page
3074 + function handleTerminalRequest(req, res) {
3075 + const domain = getDomain(req, res);
3076 + if (domain == null) { return; }
3077 + if (req.query.c == null) { res.sendStatus(404); return; }
3078 + if (domain.guestdevicesharing === false) { res.sendStatus(404); return; } // This feature is not allowed.
3079 +
3080 + // Check the inbound desktop sharing cookie
3081 + var c = obj.parent.decodeCookie(req.query.c, obj.parent.invitationLinkEncryptionKey, 60); // 60 minute timeout
3082 + if ((c == null) || (c.a !== 5) || (c.p !== 1) || (typeof c.uid != 'string') || (typeof c.nid != 'string') || (typeof c.gn != 'string') || (typeof c.cf != 'number') || (typeof c.start != 'number') || (typeof c.expire != 'number') || (typeof c.pid != 'string')) { res.sendStatus(404); return; }
3083 +
3084 + // Check the expired time, expire message.
3085 + if (c.expire <= Date.now()) { render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 4, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; }
3086 +
3087 + // Check the public id
3088 + obj.db.GetAllTypeNodeFiltered([c.nid], domain.id, 'deviceshare', null, function (err, docs) {
3089 + // Check if any desktop sharing links are present, expire message.
3090 + if ((err != null) || (docs.length == 0)) { render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 4, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; }
3091 +
3092 + // Search for the device share public identifier, expire message.
3093 + var found = false;
3094 + for (var i = 0; i < docs.length; i++) { if (docs[i].publicid == c.pid) { found = true; } }
3095 + if (found == false) { render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 4, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; }
3096 +
3097 + // Check the start time, not yet valid message.
3098 + if ((c.start > Date.now()) || (c.start > c.expire)) { render(req, res, getRenderPage((domain.sitestyle == 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 4, msgid: 11, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; }
3099 +
3100 + // Looks good, let's create the outbound session cookies.
3101 + // Consent flags are 2 = Notify, 16 = Prompt
3102 + const authCookie = obj.parent.encodeCookie({ userid: c.uid, domainid: domain.id, nid: c.nid, ip: req.clientIp, p: 1, gn: c.gn, cf: 2 | c.cf, r: 8, expire: c.expire, pid: c.pid }, obj.parent.loginCookieEncryptionKey);
3103 +
3104 + // Lets respond by sending out the desktop viewer.
3105 + var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
3106 + parent.debug('web', 'handleTerminalRequest: Sending guest terminal page for \"' + c.uid + '\", guest \"' + c.gn + '\".');
3107 + res.set({ 'Cache-Control': 'no-store' });
3108 + render(req, res, getRenderPage('terminal', req, domain), getRenderArgs({ authCookie: authCookie, authRelayCookie: '', domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27'), nodeid: c.nid, serverDnsName: obj.getWebServerName(domain), serverRedirPort: args.redirport, serverPublicPort: httpsPort, expire: c.expire }, req, domain));
3109 + });
3110 + }
3111 +
3112 + // Handle domain redirection
3113 + obj.handleDomainRedirect = function (req, res) {
3114 + const domain = checkUserIpAddress(req, res);
3115 + if (domain == null) { return; }
3116 + if (domain.redirects == null) { res.sendStatus(404); return; }
3117 + var urlArgs = '', urlName = null, splitUrl = req.originalUrl.split('?');
3118 + if (splitUrl.length > 1) { urlArgs = '?' + splitUrl[1]; }
3119 + if ((splitUrl.length > 0) && (splitUrl[0].length > 1)) { urlName = splitUrl[0].substring(1).toLowerCase(); }
3120 + if ((urlName == null) || (domain.redirects[urlName] == null) || (urlName[0] == '_')) { res.sendStatus(404); return; }
3121 + if (domain.redirects[urlName] == '~showversion') {
3122 + // Show the current version
3123 + res.end('MeshCentral v' + obj.parent.currentVer);
3124 + } else {
3125 + // Perform redirection
3126 + res.redirect(domain.redirects[urlName] + urlArgs + getQueryPortion(req));
3127 + }
3128 + }
3129 +
3130 + // Take a "user/domain/userid/path/file" format and return the actual server disk file path if access is allowed
3131 + obj.getServerFilePath = function (user, domain, path) {
3132 + var splitpath = path.split('/'), serverpath = obj.path.join(obj.filespath, 'domain'), filename = '';
3133 + if ((splitpath.length < 3) || (splitpath[0] != 'user' && splitpath[0] != 'mesh') || (splitpath[1] != domain.id)) return null; // Basic validation
3134 + var objid = splitpath[0] + '/' + splitpath[1] + '/' + splitpath[2];
3135 + if (splitpath[0] == 'user' && (objid != user._id)) return null; // User validation, only self allowed
3136 + if (splitpath[0] == 'mesh') { if ((obj.GetMeshRights(user, objid) & 32) == 0) { return null; } } // Check mesh server file rights
3137 + if (splitpath[1] != '') { serverpath += '-' + splitpath[1]; } // Add the domain if needed
3138 + serverpath += ('/' + splitpath[0] + '-' + splitpath[2]);
3139 + for (var i = 3; i < splitpath.length; i++) { if (obj.common.IsFilenameValid(splitpath[i]) == true) { serverpath += '/' + splitpath[i]; filename = splitpath[i]; } else { return null; } } // Check that each folder is correct
3140 + return { fullpath: obj.path.resolve(obj.filespath, serverpath), path: serverpath, name: filename, quota: obj.getQuota(objid, domain) };
3141 + };
3142 +
3143 + // Return the maximum number of bytes allowed in the user account "My Files".
3144 + obj.getQuota = function (objid, domain) {
3145 + if (objid == null) return 0;
3146 + if (objid.startsWith('user/')) {
3147 + var user = obj.users[objid];
3148 + if (user == null) return 0;
3149 + if (user.siteadmin == 0xFFFFFFFF) return null; // Administrators have no user limit
3150 + if ((user.quota != null) && (typeof user.quota == 'number')) { return user.quota; }
3151 + if ((domain != null) && (domain.userquota != null) && (typeof domain.userquota == 'number')) { return domain.userquota; }
3152 + return null; // By default, the user will have no limit
3153 + } else if (objid.startsWith('mesh/')) {
3154 + var mesh = obj.meshes[objid];
3155 + if (mesh == null) return 0;
3156 + if ((mesh.quota != null) && (typeof mesh.quota == 'number')) { return mesh.quota; }
3157 + if ((domain != null) && (domain.meshquota != null) && (typeof domain.meshquota == 'number')) { return domain.meshquota; }
3158 + return null; // By default, the mesh will have no limit
3159 + }
3160 + return 0;
3161 + };
3162 +
3163 + // Download a file from the server
3164 + function handleDownloadFile(req, res) {
3165 + const domain = checkUserIpAddress(req, res);
3166 + if (domain == null) { return; }
3167 + if ((req.query.link == null) || (req.session == null) || (req.session.userid == null) || (domain == null) || (domain.userQuota == -1)) { res.sendStatus(404); return; }
3168 + const user = obj.users[req.session.userid];
3169 + if (user == null) { res.sendStatus(404); return; }
3170 + const file = obj.getServerFilePath(user, domain, req.query.link);
3171 + if (file == null) { res.sendStatus(404); return; }
3172 + setContentDispositionHeader(res, 'application/octet-stream', file.name, null, 'file.bin');
3173 + obj.fs.exists(file.fullpath, function (exists) { if (exists == true) { res.sendFile(file.fullpath); } else { res.sendStatus(404); } });
3174 + }
3175 +
3176 + // Upload a MeshCore.js file to the server
3177 + function handleUploadMeshCoreFile(req, res) {
3178 + const domain = checkUserIpAddress(req, res);
3179 + if (domain == null) { return; }
3180 + if (domain.id !== '') { res.sendStatus(401); return; }
3181 +
3182 + var authUserid = null;
3183 + if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
3184 +
3185 + const multiparty = require('multiparty');
3186 + const form = new multiparty.Form();
3187 + form.parse(req, function (err, fields, files) {
3188 + // If an authentication cookie is embedded in the form, use that.
3189 + if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
3190 + var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
3191 + if ((loginCookie != null) && (obj.args.cookieipcheck !== false) && (loginCookie.ip != null) && (loginCookie.ip != req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
3192 + if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
3193 + }
3194 + if (authUserid == null) { res.sendStatus(401); return; }
3195 +
3196 + // Get the user
3197 + const user = obj.users[authUserid];
3198 + if (user.siteadmin != 0xFFFFFFFF) { res.sendStatus(401); return; } // Check if we have mesh core upload rights (Full admin only)
3199 +
3200 + if ((fields == null) || (fields.attrib == null) || (fields.attrib.length != 1)) { res.sendStatus(404); return; }
3201 + for (var i in files.files) {
3202 + var file = files.files[i];
3203 + obj.fs.readFile(file.path, 'utf8', function (err, data) {
3204 + if (err != null) return;
3205 + data = obj.common.IntToStr(0) + data; // Add the 4 bytes encoding type & flags (Set to 0 for raw)
3206 + obj.sendMeshAgentCore(user, domain, fields.attrib[0], 'custom', data); // Upload the core
3207 + try { obj.fs.unlinkSync(file.path); } catch (e) { }
3208 + });
3209 + }
3210 + res.send('');
3211 + });
3212 + }
3213 +
3214 + // Upload a file to the server
3215 + function handleUploadFile(req, res) {
3216 + const domain = checkUserIpAddress(req, res);
3217 + if (domain == null) { return; }
3218 + if (domain.userQuota == -1) { res.sendStatus(401); return; }
3219 + var authUserid = null;
3220 + if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
3221 + const multiparty = require('multiparty');
3222 + const form = new multiparty.Form();
3223 + form.parse(req, function (err, fields, files) {
3224 + // If an authentication cookie is embedded in the form, use that.
3225 + if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
3226 + var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
3227 + if ((loginCookie != null) && (obj.args.cookieipcheck !== false) && (loginCookie.ip != null) && (loginCookie.ip != req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
3228 + if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
3229 + }
3230 + if (authUserid == null) { res.sendStatus(401); return; }
3231 +
3232 + // Get the user
3233 + const user = obj.users[authUserid];
3234 + if ((user == null) || (user.siteadmin & 8) == 0) { res.sendStatus(401); return; } // Check if we have file rights
3235 +
3236 + if ((fields == null) || (fields.link == null) || (fields.link.length != 1)) { /*console.log('UploadFile, Invalid Fields:', fields, files);*/ console.log('err4'); res.sendStatus(404); return; }
3237 + var xfile = null;
3238 + try { xfile = obj.getServerFilePath(user, domain, decodeURIComponent(fields.link[0])); } catch (ex) { }
3239 + if (xfile == null) { res.sendStatus(404); return; }
3240 + // Get total bytes in the path
3241 + var totalsize = readTotalFileSize(xfile.fullpath);
3242 + if ((xfile.quota == null) || (totalsize < xfile.quota)) { // Check if the quota is not already broken
3243 + if (fields.name != null) {
3244 +
3245 + // See if we need to create the folder
3246 + var domainx = 'domain';
3247 + if (domain.id.length > 0) { domainx = 'domain-' + usersplit[1]; }
3248 + try { obj.fs.mkdirSync(obj.parent.filespath); } catch (ex) { }
3249 + try { obj.fs.mkdirSync(obj.parent.path.join(obj.parent.filespath, domainx)); } catch (ex) { }
3250 + try { obj.fs.mkdirSync(xfile.fullpath); } catch (ex) { }
3251 +
3252 + // Upload method where all the file data is within the fields.
3253 + var names = fields.name[0].split('*'), sizes = fields.size[0].split('*'), types = fields.type[0].split('*'), datas = fields.data[0].split('*');
3254 + if ((names.length == sizes.length) && (types.length == datas.length) && (names.length == types.length)) {
3255 + for (var i = 0; i < names.length; i++) {
3256 + if (obj.common.IsFilenameValid(names[i]) == false) { res.sendStatus(404); return; }
3257 + var filedata = Buffer.from(datas[i].split(',')[1], 'base64');
3258 + if ((xfile.quota == null) || ((totalsize + filedata.length) < xfile.quota)) { // Check if quota would not be broken if we add this file
3259 + // Create the user folder if needed
3260 + (function (fullpath, filename, filedata) {
3261 + obj.fs.mkdir(xfile.fullpath, function () {
3262 + // Write the file
3263 + obj.fs.writeFile(obj.path.join(xfile.fullpath, filename), filedata, function () {
3264 + obj.parent.DispatchEvent([user._id], obj, 'updatefiles'); // Fire an event causing this user to update this files
3265 + });
3266 + });
3267 + })(xfile.fullpath, names[i], filedata);
3268 + } else {
3269 + // Send a notification
3270 + obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: "Disk quota exceed", value: names[i], nolog: 1, id: Math.random() });
3271 + }
3272 + }
3273 + }
3274 + } else {
3275 + // More typical upload method, the file data is in a multipart mime post.
3276 + for (var i in files.files) {
3277 + var file = files.files[i], fpath = obj.path.join(xfile.fullpath, file.originalFilename);
3278 + if (obj.common.IsFilenameValid(file.originalFilename) && ((xfile.quota == null) || ((totalsize + file.size) < xfile.quota))) { // Check if quota would not be broken if we add this file
3279 +
3280 + // See if we need to create the folder
3281 + var domainx = 'domain';
3282 + if (domain.id.length > 0) { domainx = 'domain-' + domain.id; }
3283 + try { obj.fs.mkdirSync(obj.parent.filespath); } catch (e) { }
3284 + try { obj.fs.mkdirSync(obj.parent.path.join(obj.parent.filespath, domainx)); } catch (e) { }
3285 + try { obj.fs.mkdirSync(xfile.fullpath); } catch (e) { }
3286 +
3287 + // Rename the file
3288 + obj.fs.rename(file.path, fpath, function (err) {
3289 + if (err && (err.code === 'EXDEV')) {
3290 + // On some Linux, the rename will fail with a "EXDEV" error, do a copy+unlink instead.
3291 + obj.common.copyFile(file.path, fpath, function (err) {
3292 + obj.fs.unlink(file.path, function (err) {
3293 + obj.parent.DispatchEvent([user._id], obj, 'updatefiles'); // Fire an event causing this user to update this files
3294 + });
3295 + });
3296 + } else {
3297 + obj.parent.DispatchEvent([user._id], obj, 'updatefiles'); // Fire an event causing this user to update this files
3298 + }
3299 + });
3300 + } else {
3301 + // Send a notification
3302 + obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: "Disk quota exceed", value: file.originalFilename, nolog: 1, id: Math.random() });
3303 + try { obj.fs.unlink(file.path, function (err) { }); } catch (e) { }
3304 + }
3305 + }
3306 + }
3307 + } else {
3308 + // Send a notification
3309 + obj.parent.DispatchEvent([user._id], obj, { action: 'notify', value: "Disk quota exceed", nolog: 1, id: Math.random() });
3310 + }
3311 + res.send('');
3312 + });
3313 + }
3314 +
3315 + // Upload a file to the server and then batch upload to many agents
3316 + function handleUploadFileBatch(req, res) {
3317 + const domain = checkUserIpAddress(req, res);
3318 + if (domain == null) { return; }
3319 + var authUserid = null;
3320 + if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
3321 + const multiparty = require('multiparty');
3322 + const form = new multiparty.Form();
3323 + form.parse(req, function (err, fields, files) {
3324 + // If an authentication cookie is embedded in the form, use that.
3325 + if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
3326 + var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
3327 + if ((loginCookie != null) && (obj.args.cookieipcheck !== false) && (loginCookie.ip != null) && (loginCookie.ip != req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
3328 + if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
3329 + }
3330 + if (authUserid == null) { res.sendStatus(401); return; }
3331 +
3332 + // Get the user
3333 + const user = obj.users[authUserid];
3334 + if (user == null) { parent.debug('web', 'Batch upload error, invalid user.'); res.sendStatus(401); return; } // Check if user exists
3335 +
3336 + // Get fields
3337 + if ((fields == null) || (fields.nodeIds == null) || (fields.nodeIds.length != 1)) { res.sendStatus(404); return; }
3338 + var cmd = { nodeids: fields.nodeIds[0].split(','), files: [], user: user, domain: domain };
3339 + if ((fields.winpath != null) && (fields.winpath.length == 1)) { cmd.windowsPath = fields.winpath[0]; }
3340 + if ((fields.linuxpath != null) && (fields.linuxpath.length == 1)) { cmd.linuxPath = fields.linuxpath[0]; }
3341 + if ((fields.overwriteFiles != null) && (fields.overwriteFiles.length == 1) && (fields.overwriteFiles[0] == 'on')) { cmd.overwrite = true; }
3342 + if ((fields.createFolder != null) && (fields.createFolder.length == 1) && (fields.createFolder[0] == 'on')) { cmd.createFolder = true; }
3343 +
3344 + // Check if we have at least one target path
3345 + if ((cmd.windowsPath == null) && (cmd.linuxPath == null)) {
3346 + parent.debug('web', 'Batch upload error, invalid fields: ' + JSON.stringify(fields));
3347 + res.send('');
3348 + return;
3349 + }
3350 +
3351 + // Get server temporary path
3352 + var serverpath = obj.path.join(obj.filespath, 'tmp')
3353 + try { obj.fs.mkdirSync(obj.parent.filespath); } catch (ex) { }
3354 + try { obj.fs.mkdirSync(serverpath); } catch (ex) { }
3355 +
3356 + // More typical upload method, the file data is in a multipart mime post.
3357 + for (var i in files.files) {
3358 + var file = files.files[i], ftarget = getRandomPassword() + '-' + file.originalFilename, fpath = obj.path.join(serverpath, ftarget);
3359 + cmd.files.push({ name: file.originalFilename, target: ftarget });
3360 + // Rename the file
3361 + obj.fs.rename(file.path, fpath, function (err) {
3362 + if (err && (err.code === 'EXDEV')) {
3363 + // On some Linux, the rename will fail with a "EXDEV" error, do a copy+unlink instead.
3364 + obj.common.copyFile(file.path, fpath, function (err) { obj.fs.unlink(file.path, function (err) { }); });
3365 + }
3366 + });
3367 + }
3368 +
3369 + // Instruct one of more agents to download a URL to a given local drive location.
3370 + var tlsCertHash = null;
3371 + if (parent.args.ignoreagenthashcheck !== true) {
3372 + tlsCertHash = obj.webCertificateFullHashs[cmd.domain.id];
3373 + if (tlsCertHash != null) { tlsCertHash = Buffer.from(tlsCertHash, 'binary').toString('hex'); }
3374 + }
3375 + for (var i in cmd.nodeids) {
3376 + obj.GetNodeWithRights(cmd.domain, cmd.user, cmd.nodeids[i], function (node, rights, visible) {
3377 + if ((node == null) || ((rights & 8) == 0) || (visible == false)) return; // We don't have remote control rights to this device
3378 + var agentPath = ((node.agent.id > 0) && (node.agent.id < 5)) ? cmd.windowsPath : cmd.linuxPath;
3379 + if (agentPath == null) return;
3380 +
3381 + // Event that this operation is being performed.
3382 + var targets = obj.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', cmd.user._id]);
3383 + var msgid = 103; // "Batch upload of {0} file(s) to folder {1}"
3384 + var event = { etype: 'node', userid: cmd.user._id, username: cmd.user.name, nodeid: node._id, action: 'batchupload', msg: 'Performing batch upload of ' + cmd.files.length + ' file(s) to ' + agentPath, msgid: msgid, msgArgs: [cmd.files.length, agentPath], domain: cmd.domain.id };
3385 + parent.DispatchEvent(targets, obj, event);
3386 +
3387 + // Send the agent commands to perform the batch upload operation
3388 + for (var f in cmd.files) {
3389 + if (cmd.files[f].name != null) {
3390 + const acmd = { action: 'wget', overwrite: cmd.overwrite, createFolder: cmd.createFolder, urlpath: '/agentdownload.ashx?c=' + obj.parent.encodeCookie({ a: 'tmpdl', d: cmd.domain.id, nid: node._id, f: cmd.files[f].target }, obj.parent.loginCookieEncryptionKey), path: obj.path.join(agentPath, cmd.files[f].name), folder: agentPath, servertlshash: tlsCertHash };
3391 + var agent = obj.wsagents[node._id];
3392 + if (agent != null) { try { agent.send(JSON.stringify(acmd)); } catch (ex) { } }
3393 + // TODO: Add support for peer servers.
3394 + }
3395 + }
3396 + });
3397 + }
3398 +
3399 + res.send('');
3400 + });
3401 + }
3402 +
3403 + // Subscribe to all events we are allowed to receive
3404 + obj.subscribe = function (userid, target) {
3405 + const user = obj.users[userid];
3406 + const subscriptions = [userid, 'server-global'];
3407 + if (user.siteadmin != null) {
3408 + // Allow full site administrators of users with all events rights to see all events.
3409 + if ((user.siteadmin == 0xFFFFFFFF) || ((user.siteadmin & 2048) != 0)) { subscriptions.push('*'); }
3410 + else if ((user.siteadmin & 2) != 0) {
3411 + if ((user.groups == null) || (user.groups.length == 0)) {
3412 + // Subscribe to all user changes
3413 + subscriptions.push('server-users');
3414 + } else {
3415 + // Subscribe to user changes for some groups
3416 + for (var i in user.groups) { subscriptions.push('server-users:' + i); }
3417 + }
3418 + }
3419 + }
3420 + if (user.links != null) { for (var i in user.links) { subscriptions.push(i); } }
3421 + obj.parent.RemoveAllEventDispatch(target);
3422 + obj.parent.AddEventDispatch(subscriptions, target);
3423 + return subscriptions;
3424 + };
3425 +
3426 + // Handle a web socket relay request
3427 + function handleRelayWebSocket(ws, req, domain, user, cookie) {
3428 + if (!(req.query.host)) { console.log('ERR: No host target specified'); try { ws.close(); } catch (e) { } return; } // Disconnect websocket
3429 + parent.debug('web', 'Websocket relay connected from ' + user.name + ' for ' + req.query.host + '.');
3430 +
3431 + try { ws._socket.setKeepAlive(true, 240000); } catch (ex) { } // Set TCP keep alive
3432 +
3433 + // Fetch information about the target
3434 + obj.db.Get(req.query.host, function (err, docs) {
3435 + if (docs.length == 0) { console.log('ERR: Node not found'); try { ws.close(); } catch (e) { } return; } // Disconnect websocket
3436 + var node = docs[0];
3437 + if (!node.intelamt) { console.log('ERR: Not AMT node'); try { ws.close(); } catch (e) { } return; } // Disconnect websocket
3438 +
3439 + // Check if this user has permission to manage this computer
3440 + if ((obj.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0) { console.log('ERR: Access denied (3)'); try { ws.close(); } catch (e) { } return; }
3441 +
3442 + // Check what connectivity is available for this node
3443 + var state = parent.GetConnectivityState(req.query.host);
3444 + var conn = 0;
3445 + if (!state || state.connectivity == 0) { parent.debug('web', 'ERR: No routing possible (1)'); try { ws.close(); } catch (e) { } return; } else { conn = state.connectivity; }
3446 +
3447 + // Check what server needs to handle this connection
3448 + if ((obj.parent.multiServer != null) && ((cookie == null) || (cookie.ps != 1))) { // If a cookie is provided and is from a peer server, don't allow the connection to jump again to a different server
3449 + var server = obj.parent.GetRoutingServerId(req.query.host, 2); // Check for Intel CIRA connection
3450 + if (server != null) {
3451 + if (server.serverid != obj.parent.serverId) {
3452 + // Do local Intel CIRA routing using a different server
3453 + parent.debug('web', 'Route Intel AMT CIRA connection to peer server: ' + server.serverid);
3454 + obj.parent.multiServer.createPeerRelay(ws, req, server.serverid, user);
3455 + return;
3456 + }
3457 + } else {
3458 + server = obj.parent.GetRoutingServerId(req.query.host, 4); // Check for local Intel AMT connection
3459 + if ((server != null) && (server.serverid != obj.parent.serverId)) {
3460 + // Do local Intel AMT routing using a different server
3461 + parent.debug('web', 'Route Intel AMT direct connection to peer server: ' + server.serverid);
3462 + obj.parent.multiServer.createPeerRelay(ws, req, server.serverid, user);
3463 + return;
3464 + }
3465 + }
3466 + }
3467 +
3468 + // Setup session recording if needed
3469 + if (domain.sessionrecording == true || ((typeof domain.sessionrecording == 'object') && ((domain.sessionrecording.protocols == null) || (domain.sessionrecording.protocols.indexOf((req.query.p == 2) ? 101 : 100) >= 0)))) { // TODO 100
3470 + // Check again if we need to do recording
3471 + var record = true;
3472 + if (domain.sessionrecording.onlyselecteddevicegroups === true) {
3473 + var mesh = obj.meshes[node.meshid];
3474 + if ((mesh.flags == null) || ((mesh.flags & 4) == 0)) { record = false; } // Do not record the session
3475 + }
3476 +
3477 + if (record == true) {
3478 + var now = new Date(Date.now());
3479 + var recFilename = 'relaysession' + ((domain.id == '') ? '' : '-') + domain.id + '-' + now.getUTCFullYear() + '-' + obj.common.zeroPad(now.getUTCMonth(), 2) + '-' + obj.common.zeroPad(now.getUTCDate(), 2) + '-' + obj.common.zeroPad(now.getUTCHours(), 2) + '-' + obj.common.zeroPad(now.getUTCMinutes(), 2) + '-' + obj.common.zeroPad(now.getUTCSeconds(), 2) + '-' + getRandomPassword() + '.mcrec'
3480 + var recFullFilename = null;
3481 + if (domain.sessionrecording.filepath) {
3482 + try { obj.fs.mkdirSync(domain.sessionrecording.filepath); } catch (e) { }
3483 + recFullFilename = obj.path.join(domain.sessionrecording.filepath, recFilename);
3484 + } else {
3485 + try { obj.fs.mkdirSync(parent.recordpath); } catch (e) { }
3486 + recFullFilename = obj.path.join(parent.recordpath, recFilename);
3487 + }
3488 + var fd = obj.fs.openSync(recFullFilename, 'w');
3489 + if (fd != null) {
3490 + // Write the recording file header
3491 + var firstBlock = JSON.stringify({ magic: 'MeshCentralRelaySession', ver: 1, userid: user._id, username: user.name, ipaddr: req.clientIp, nodeid: node._id, intelamt: true, protocol: (req.query.p == 2) ? 101 : 100, time: new Date().toLocaleString() })
3492 + recordingEntry(fd, 1, 0, firstBlock, function () { });
3493 + ws.logfile = { fd: fd, lock: false };
3494 + if (req.query.p == 2) { ws.send(Buffer.from(String.fromCharCode(0xF0), 'binary')); } // Intel AMT Redirection: Indicate the session is being recorded
3495 + }
3496 + }
3497 + }
3498 +
3499 + // If Intel AMT CIRA connection is available, use it
3500 + var ciraconn = parent.mpsserver.GetConnectionToNode(req.query.host, null, false);
3501 + if (ciraconn != null) {
3502 + parent.debug('web', 'Opening relay CIRA channel connection to ' + req.query.host + '.');
3503 +
3504 + // TODO: If the CIRA connection is a relay or LMS connection, we can't detect the TLS state like this.
3505 + // Compute target port, look at the CIRA port mappings, if non-TLS is allowed, use that, if not use TLS
3506 + var port = 16993;
3507 + //if (node.intelamt.tls == 0) port = 16992; // DEBUG: Allow TLS flag to set TLS mode within CIRA
3508 + if (ciraconn.tag.boundPorts.indexOf(16992) >= 0) port = 16992; // RELEASE: Always use non-TLS mode if available within CIRA
3509 + if (req.query.p == 2) port += 2;
3510 +
3511 + // Setup a new CIRA channel
3512 + if ((port == 16993) || (port == 16995)) {
3513 + // Perform TLS
3514 + var ser = new SerialTunnel();
3515 + var chnl = parent.mpsserver.SetupChannel(ciraconn, port);
3516 +
3517 + // Let's chain up the TLSSocket <-> SerialTunnel <-> CIRA APF (chnl)
3518 + // Anything that needs to be forwarded by SerialTunnel will be encapsulated by chnl write
3519 + ser.forwardwrite = function (data) { if (data.length > 0) { chnl.write(data); } }; // TLS ---> CIRA
3520 +
3521 + // When APF tunnel return something, update SerialTunnel buffer
3522 + chnl.onData = function (ciraconn, data) { if (data.length > 0) { try { ser.updateBuffer(data); } catch (ex) { console.log(ex); } } }; // CIRA ---> TLS
3523 +
3524 + // Handle CIRA tunnel state change
3525 + chnl.onStateChange = function (ciraconn, state) {
3526 + parent.debug('webrelay', 'Relay TLS CIRA state change', state);
3527 + if (state == 0) { try { ws.close(); } catch (e) { } }
3528 + if (state == 2) {
3529 + // TLSSocket to encapsulate TLS communication, which then tunneled via SerialTunnel an then wrapped through CIRA APF
3530 + const tlsoptions = { socket: ser, ciphers: 'RSA+AES:!aNULL:!MD5:!DSS', secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE, rejectUnauthorized: false };
3531 + if (req.query.tls1only == 1) { tlsoptions.secureProtocol = 'TLSv1_method'; }
3532 + var tlsock = obj.tls.connect(tlsoptions, function () { parent.debug('webrelay', "CIRA Secure TLS Connection"); ws._socket.resume(); });
3533 + tlsock.chnl = chnl;
3534 + tlsock.setEncoding('binary');
3535 + tlsock.on('error', function (err) { parent.debug('webrelay', "CIRA TLS Connection Error", err); });
3536 +
3537 + // Decrypted tunnel from TLS communcation to be forwarded to websocket
3538 + tlsock.on('data', function (data) {
3539 + // AMT/TLS ---> WS
3540 + if (ws.interceptor) { data = ws.interceptor.processAmtData(data); } // Run data thru interceptor
3541 + try { ws.send(data); } catch (ex) { }
3542 + });
3543 +
3544 + // If TLS is on, forward it through TLSSocket
3545 + ws.forwardclient = tlsock;
3546 + ws.forwardclient.xtls = 1;
3547 +
3548 + ws.forwardclient.onStateChange = function (ciraconn, state) {
3549 + parent.debug('webrelay', 'Relay CIRA state change', state);
3550 + if (state == 0) { try { ws.close(); } catch (e) { } }
3551 + };
3552 +
3553 + ws.forwardclient.onData = function (ciraconn, data) {
3554 + // Run data thru interceptor
3555 + if (ws.interceptor) { data = ws.interceptor.processAmtData(data); }
3556 +
3557 + if (data.length > 0) {
3558 + if (ws.logfile == null) {
3559 + try { ws.send(data); } catch (e) { }
3560 + } else {
3561 + // Log to recording file
3562 + recordingEntry(ws.logfile.fd, 2, 0, data, function () { try { ws.send(data); } catch (ex) { console.log(ex); } }); // TODO: Add TLS support
3563 + }
3564 + }
3565 + };
3566 +
3567 + // TODO: Flow control? (Dont' really need it with AMT, but would be nice)
3568 + ws.forwardclient.onSendOk = function (ciraconn) { };
3569 + }
3570 + };
3571 + } else {
3572 + // Without TLS
3573 + ws.forwardclient = parent.mpsserver.SetupChannel(ciraconn, port);
3574 + ws.forwardclient.xtls = 0;
3575 + ws._socket.resume();
3576 +
3577 + ws.forwardclient.onStateChange = function (ciraconn, state) {
3578 + parent.debug('webrelay', 'Relay CIRA state change', state);
3579 + if (state == 0) { try { ws.close(); } catch (e) { } }
3580 + };
3581 +
3582 + ws.forwardclient.onData = function (ciraconn, data) {
3583 + //parent.debug('webrelaydata', 'Relay CIRA data to WS', data.length);
3584 +
3585 + // Run data thru interceptorp
3586 + if (ws.interceptor) { data = ws.interceptor.processAmtData(data); }
3587 +
3588 + //console.log('AMT --> WS', Buffer.from(data, 'binary').toString('hex'));
3589 + if (data.length > 0) {
3590 + if (ws.logfile == null) {
3591 + try { ws.send(data); } catch (e) { }
3592 + } else {
3593 + // Log to recording file
3594 + recordingEntry(ws.logfile.fd, 2, 0, data, function () { try { ws.send(data); } catch (ex) { console.log(ex); } });
3595 + }
3596 + }
3597 + };
3598 +
3599 + // TODO: Flow control? (Dont' really need it with AMT, but would be nice)
3600 + ws.forwardclient.onSendOk = function (ciraconn) { };
3601 + }
3602 +
3603 + // When data is received from the web socket, forward the data into the associated CIRA cahnnel.
3604 + // If the CIRA connection is pending, the CIRA channel has built-in buffering, so we are ok sending anyway.
3605 + ws.on('message', function (data) {
3606 + //parent.debug('webrelaydata', 'Relay WS data to CIRA', data.length);
3607 + if (typeof data == 'string') { data = Buffer.from(data, 'binary'); }
3608 +
3609 + // WS ---> AMT/TLS
3610 + if (ws.interceptor) { data = ws.interceptor.processBrowserData(data); } // Run data thru interceptor
3611 +
3612 + // Log to recording file
3613 + if (ws.logfile == null) {
3614 + // Forward data to the associated TCP connection.
3615 + ws.forwardclient.write(data);
3616 + } else {
3617 + // Log to recording file
3618 + recordingEntry(ws.logfile.fd, 2, 2, data, function () { try { ws.forwardclient.write(data); } catch (ex) { } });
3619 + }
3620 + });
3621 +
3622 + // If error, close the associated TCP connection.
3623 + ws.on('error', function (err) {
3624 + console.log('CIRA server websocket error from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.');
3625 + parent.debug('webrelay', 'Websocket relay closed on error.');
3626 +
3627 + // Websocket closed, close the CIRA channel and TLS session.
3628 + if (ws.forwardclient) {
3629 + if (ws.forwardclient.close) { ws.forwardclient.close(); } // NonTLS, close the CIRA channel
3630 + if (ws.forwardclient.end) { ws.forwardclient.end(); } // TLS, close the TLS session
3631 + if (ws.forwardclient.chnl) { ws.forwardclient.chnl.close(); } // TLS, close the CIRA channel
3632 + delete ws.forwardclient;
3633 + }
3634 +
3635 + // Close the recording file
3636 + if (ws.logfile != null) { recordingEntry(ws.logfile.fd, 3, 0, 'MeshCentralMCREC', function (fd, ws) { obj.fs.close(fd); delete ws.logfile; }, ws); }
3637 + });
3638 +
3639 + // If the web socket is closed, close the associated TCP connection.
3640 + ws.on('close', function (req) {
3641 + parent.debug('webrelay', 'Websocket relay closed.');
3642 +
3643 + // Websocket closed, close the CIRA channel and TLS session.
3644 + if (ws.forwardclient) {
3645 + if (ws.forwardclient.close) { ws.forwardclient.close(); } // NonTLS, close the CIRA channel
3646 + if (ws.forwardclient.end) { ws.forwardclient.end(); } // TLS, close the TLS session
3647 + if (ws.forwardclient.chnl) { ws.forwardclient.chnl.close(); } // TLS, close the CIRA channel
3648 + delete ws.forwardclient;
3649 + }
3650 +
3651 + // Close the recording file
3652 + if (ws.logfile != null) { recordingEntry(ws.logfile.fd, 3, 0, 'MeshCentralMCREC', function (fd, ws) { obj.fs.close(fd); delete ws.logfile; }, ws); }
3653 + });
3654 +
3655 + // Note that here, req.query.p: 1 = WSMAN with server auth, 2 = REDIR with server auth, 3 = WSMAN without server auth, 4 = REDIR with server auth
3656 +
3657 + // Fetch Intel AMT credentials & Setup interceptor
3658 + if (req.query.p == 1) {
3659 + parent.debug('webrelaydata', 'INTERCEPTOR1', { host: node.host, port: port, user: node.intelamt.user, pass: node.intelamt.pass });
3660 + ws.interceptor = obj.interceptor.CreateHttpInterceptor({ host: node.host, port: port, user: node.intelamt.user, pass: node.intelamt.pass });
3661 + ws.interceptor.blockAmtStorage = true;
3662 + } else if (req.query.p == 2) {
3663 + parent.debug('webrelaydata', 'INTERCEPTOR2', { user: node.intelamt.user, pass: node.intelamt.pass });
3664 + ws.interceptor = obj.interceptor.CreateRedirInterceptor({ user: node.intelamt.user, pass: node.intelamt.pass });
3665 + ws.interceptor.blockAmtStorage = true;
3666 + }
3667 +
3668 + return;
3669 + }
3670 +
3671 + // If Intel AMT direct connection is possible, option a direct socket
3672 + if ((conn & 4) != 0) { // We got a new web socket connection, initiate a TCP connection to the target Intel AMT host/port.
3673 + parent.debug('webrelay', 'Opening relay TCP socket connection to ' + req.query.host + '.');
3674 +
3675 + // When data is received from the web socket, forward the data into the associated TCP connection.
3676 + ws.on('message', function (msg) {
3677 + //parent.debug('webrelaydata', 'TCP relay data to ' + node.host + ', ' + msg.length + ' bytes');
3678 +
3679 + if (typeof msg == 'string') { msg = Buffer.from(msg, 'binary'); }
3680 + if (ws.interceptor) { msg = ws.interceptor.processBrowserData(msg); } // Run data thru interceptor
3681 +
3682 + // Log to recording file
3683 + if (ws.logfile == null) {
3684 + // Forward data to the associated TCP connection.
3685 + try { ws.forwardclient.write(msg); } catch (ex) { }
3686 + } else {
3687 + // Log to recording file
3688 + recordingEntry(ws.logfile.fd, 2, 2, msg, function () { try { ws.forwardclient.write(msg); } catch (ex) { } });
3689 + }
3690 + });
3691 +
3692 + // If error, close the associated TCP connection.
3693 + ws.on('error', function (err) {
3694 + console.log('Error with relay web socket connection from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.');
3695 + parent.debug('webrelay', 'Error with relay web socket connection from ' + req.clientIp + '.');
3696 + if (ws.forwardclient) { try { ws.forwardclient.destroy(); } catch (e) { } }
3697 +
3698 + // Close the recording file
3699 + if (ws.logfile != null) {
3700 + recordingEntry(ws.logfile.fd, 3, 0, 'MeshCentralMCREC', function (fd) {
3701 + obj.fs.close(fd);
3702 + ws.logfile = null;
3703 + });
3704 + }
3705 + });
3706 +
3707 + // If the web socket is closed, close the associated TCP connection.
3708 + ws.on('close', function () {
3709 + parent.debug('webrelay', 'Closing relay web socket connection to ' + req.query.host + '.');
3710 + if (ws.forwardclient) { try { ws.forwardclient.destroy(); } catch (e) { } }
3711 +
3712 + // Close the recording file
3713 + if (ws.logfile != null) {
3714 + recordingEntry(ws.logfile.fd, 3, 0, 'MeshCentralMCREC', function (fd) {
3715 + obj.fs.close(fd);
3716 + ws.logfile = null;
3717 + });
3718 + }
3719 + });
3720 +
3721 + // Compute target port
3722 + var port = 16992;
3723 + if (node.intelamt.tls > 0) port = 16993; // This is a direct connection, use TLS when possible
3724 + if ((req.query.p == 2) || (req.query.p == 4)) port += 2;
3725 +
3726 + if (node.intelamt.tls == 0) {
3727 + // If this is TCP (without TLS) set a normal TCP socket
3728 + ws.forwardclient = new obj.net.Socket();
3729 + ws.forwardclient.setEncoding('binary');
3730 + ws.forwardclient.xstate = 0;
3731 + ws.forwardclient.forwardwsocket = ws;
3732 + ws._socket.resume();
3733 + } else {
3734 + // If TLS is going to be used, setup a TLS socket
3735 + var tlsoptions = { ciphers: 'RSA+AES:!aNULL:!MD5:!DSS', secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE, rejectUnauthorized: false };
3736 + if (req.query.tls1only == 1) { tlsoptions.secureProtocol = 'TLSv1_method'; }
3737 + ws.forwardclient = obj.tls.connect(port, node.host, tlsoptions, function () {
3738 + // The TLS connection method is the same as TCP, but located a bit differently.
3739 + parent.debug('webrelay', 'TLS connected to ' + node.host + ':' + port + '.');
3740 + ws.forwardclient.xstate = 1;
3741 + ws._socket.resume();
3742 + });
3743 + ws.forwardclient.setEncoding('binary');
3744 + ws.forwardclient.xstate = 0;
3745 + ws.forwardclient.forwardwsocket = ws;
3746 + }
3747 +
3748 + // When we receive data on the TCP connection, forward it back into the web socket connection.
3749 + ws.forwardclient.on('data', function (data) {
3750 + if (typeof data == 'string') { data = Buffer.from(data, 'binary'); }
3751 + if (obj.parent.debugLevel >= 1) { // DEBUG
3752 + parent.debug('webrelaydata', 'TCP relay data from ' + node.host + ', ' + data.length + ' bytes.');
3753 + //if (obj.parent.debugLevel >= 4) { Debug(4, ' ' + Buffer.from(data, 'binary').toString('hex')); }
3754 + }
3755 + if (ws.interceptor) { data = ws.interceptor.processAmtData(data); } // Run data thru interceptor
3756 + if (ws.logfile == null) {
3757 + // No logging
3758 + try { ws.send(data); } catch (e) { }
3759 + } else {
3760 + // Log to recording file
3761 + recordingEntry(ws.logfile.fd, 2, 0, data, function () { try { ws.send(data); } catch (e) { } });
3762 + }
3763 + });
3764 +
3765 + // If the TCP connection closes, disconnect the associated web socket.
3766 + ws.forwardclient.on('close', function () {
3767 + parent.debug('webrelay', 'TCP relay disconnected from ' + node.host + ':' + port + '.');
3768 + try { ws.close(); } catch (e) { }
3769 + });
3770 +
3771 + // If the TCP connection causes an error, disconnect the associated web socket.
3772 + ws.forwardclient.on('error', function (err) {
3773 + parent.debug('webrelay', 'TCP relay error from ' + node.host + ':' + port + ': ' + err);
3774 + try { ws.close(); } catch (e) { }
3775 + });
3776 +
3777 + // Fetch Intel AMT credentials & Setup interceptor
3778 + if (req.query.p == 1) { ws.interceptor = obj.interceptor.CreateHttpInterceptor({ host: node.host, port: port, user: node.intelamt.user, pass: node.intelamt.pass }); }
3779 + else if (req.query.p == 2) { ws.interceptor = obj.interceptor.CreateRedirInterceptor({ user: node.intelamt.user, pass: node.intelamt.pass }); }
3780 +
3781 + if (node.intelamt.tls == 0) {
3782 + // A TCP connection to Intel AMT just connected, start forwarding.
3783 + ws.forwardclient.connect(port, node.host, function () {
3784 + parent.debug('webrelay', 'TCP relay connected to ' + node.host + ':' + port + '.');
3785 + ws.forwardclient.xstate = 1;
3786 + ws._socket.resume();
3787 + });
3788 + }
3789 + return;
3790 + }
3791 +
3792 + });
3793 + }
3794 +
3795 + // Setup agent to/from server file transfer handler
3796 + function handleAgentFileTransfer(ws, req) {
3797 + var domain = checkAgentIpAddress(ws, req);
3798 + if (domain == null) { parent.debug('web', 'Got agent file transfer connection with bad domain or blocked IP address ' + req.clientIp + ', dropping.'); ws.close(); return; }
3799 + if (req.query.c == null) { parent.debug('web', 'Got agent file transfer connection without a cookie from ' + req.clientIp + ', dropping.'); ws.close(); return; }
3800 + var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 10); // 10 minute timeout
3801 + if ((c == null) || (c.a != 'aft')) { parent.debug('web', 'Got agent file transfer connection with invalid cookie from ' + req.clientIp + ', dropping.'); ws.close(); return; }
3802 + ws.xcmd = c.b; ws.xarg = c.c, ws.xfilelen = 0;
3803 + ws.send('c'); // Indicate connection of the tunnel. In this case, we are the termination point.
3804 + ws.send('5'); // Indicate we want to perform file transfers (5 = Files).
3805 + if (ws.xcmd == 'coredump') {
3806 + // Check the agent core dump folder if not already present.
3807 + var coreDumpPath = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps');
3808 + if (obj.fs.existsSync(coreDumpPath) == false) { try { obj.fs.mkdirSync(coreDumpPath); } catch (ex) { } }
3809 + ws.xfilepath = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps', ws.xarg);
3810 + ws.xid = 'coredump';
3811 + ws.send(JSON.stringify({ action: 'download', sub: 'start', ask: 'coredump', id: 'coredump' })); // Ask for a core dump file
3812 + }
3813 +
3814 + // When data is received from the web socket, echo it back
3815 + ws.on('message', function (data) {
3816 + if (typeof data == 'string') {
3817 + // Control message
3818 + var cmd = null;
3819 + try { cmd = JSON.parse(data); } catch (ex) { }
3820 + if ((cmd == null) || (cmd.action != 'download') || (cmd.sub == null)) return;
3821 + switch (cmd.sub) {
3822 + case 'start': {
3823 + // Perform an async file open
3824 + var callback = function onFileOpen(err, fd) {
3825 + onFileOpen.xws.xfile = fd;
3826 + onFileOpen.xws.send(JSON.stringify({ action: 'download', sub: 'startack', id: onFileOpen.xws.xid, ack: 1 })); // Ask for a directory (test)
3827 + };
3828 + callback.xws = this;
3829 + obj.fs.open(this.xfilepath + '.part', 'w', callback);
3830 + break;
3831 + }
3832 + }
3833 + } else {
3834 + // Binary message
3835 + if (data.length < 4) return;
3836 + var flags = data.readInt32BE(0);
3837 + if ((data.length > 4)) {
3838 + // Write the file
3839 + this.xfilelen += (data.length - 4);
3840 + try {
3841 + var callback = function onFileDataWritten(err, bytesWritten, buffer) {
3842 + if (onFileDataWritten.xflags & 1) {
3843 + // End of file
3844 + parent.debug('web', "Completed downloads of agent dumpfile, " + onFileDataWritten.xws.xfilelen + " bytes.");
3845 + if (onFileDataWritten.xws.xfile) {
3846 + obj.fs.close(onFileDataWritten.xws.xfile, function (err) { });
3847 + obj.fs.rename(onFileDataWritten.xws.xfilepath + '.part', onFileDataWritten.xws.xfilepath, function (err) { });
3848 + onFileDataWritten.xws.xfile = null;
3849 + }
3850 + onFileDataWritten.xws.send(JSON.stringify({ action: 'markcoredump' })); // Ask to delete the core dump file
3851 + try { onFileDataWritten.xws.close(); } catch (ex) { }
3852 + } else {
3853 + // Send ack
3854 + onFileDataWritten.xws.send(JSON.stringify({ action: 'download', sub: 'ack', id: onFileDataWritten.xws.xid })); // Ask for a directory (test)
3855 + }
3856 + };
3857 + callback.xws = this;
3858 + callback.xflags = flags;
3859 + obj.fs.write(this.xfile, data, 4, data.length - 4, callback);
3860 + } catch (ex) { }
3861 + } else {
3862 + if (flags & 1) {
3863 + // End of file
3864 + parent.debug('web', "Completed downloads of agent dumpfile, " + this.xfilelen + " bytes.");
3865 + if (this.xfile) {
3866 + obj.fs.close(this.xfile, function (err) { });
3867 + obj.fs.rename(this.xfilepath + '.part', this.xfilepath, function (err) { });
3868 + this.xfile = null;
3869 + }
3870 + this.send(JSON.stringify({ action: 'markcoredump' })); // Ask to delete the core dump file
3871 + try { this.close(); } catch (ex) { }
3872 + } else {
3873 + // Send ack
3874 + this.send(JSON.stringify({ action: 'download', sub: 'ack', id: this.xid })); // Ask for a directory (test)
3875 + }
3876 + }
3877 + }
3878 + });
3879 +
3880 + // If error, do nothing.
3881 + ws.on('error', function (err) { console.log('Agent file transfer server error from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.'); });
3882 +
3883 + // If closed, do nothing
3884 + ws.on('close', function (req) {
3885 + if (this.xfile) {
3886 + obj.fs.close(this.xfile, function (err) { });
3887 + obj.fs.unlink(this.xfilepath + '.part', function (err) { }); // Remove a partial file
3888 + }
3889 + });
3890 + }
3891 +
3892 + // Handle the web socket echo request, just echo back the data sent
3893 + function handleEchoWebSocket(ws, req) {
3894 + const domain = checkUserIpAddress(ws, req);
3895 + if (domain == null) { return; }
3896 + ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive
3897 +
3898 + // When data is received from the web socket, echo it back
3899 + ws.on('message', function (data) {
3900 + if (data.toString('utf8') == 'close') {
3901 + try { ws.close(); } catch (e) { console.log(e); }
3902 + } else {
3903 + try { ws.send(data); } catch (e) { console.log(e); }
3904 + }
3905 + });
3906 +
3907 + // If error, do nothing.
3908 + ws.on('error', function (err) { console.log('Echo server error from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.'); });
3909 +
3910 + // If closed, do nothing
3911 + ws.on('close', function (req) { });
3912 + }
3913 +
3914 + // Get the total size of all files in a folder and all sub-folders. (TODO: try to make all async version)
3915 + function readTotalFileSize(path) {
3916 + var r = 0, dir;
3917 + try { dir = obj.fs.readdirSync(path); } catch (e) { return 0; }
3918 + for (var i in dir) {
3919 + var stat = obj.fs.statSync(path + '/' + dir[i]);
3920 + if ((stat.mode & 0x004000) == 0) { r += stat.size; } else { r += readTotalFileSize(path + '/' + dir[i]); }
3921 + }
3922 + return r;
3923 + }
3924 +
3925 + // Delete a folder and all sub items. (TODO: try to make all async version)
3926 + function deleteFolderRec(path) {
3927 + if (obj.fs.existsSync(path) == false) return;
3928 + try {
3929 + obj.fs.readdirSync(path).forEach(function (file, index) {
3930 + var pathx = path + '/' + file;
3931 + if (obj.fs.lstatSync(pathx).isDirectory()) { deleteFolderRec(pathx); } else { obj.fs.unlinkSync(pathx); }
3932 + });
3933 + obj.fs.rmdirSync(path);
3934 + } catch (ex) { }
3935 + }
3936 +
3937 + // Handle Intel AMT events
3938 + // To subscribe, add "http://server:port/amtevents.ashx" to Intel AMT subscriptions.
3939 + obj.handleAmtEventRequest = function (req, res) {
3940 + const domain = getDomain(req);
3941 + try {
3942 + if (req.headers.authorization) {
3943 + var authstr = req.headers.authorization;
3944 + if (authstr.substring(0, 7) == 'Digest ') {
3945 + var auth = obj.common.parseNameValueList(obj.common.quoteSplit(authstr.substring(7)));
3946 + if ((req.url === auth.uri) && (obj.httpAuthRealm === auth.realm) && (auth.opaque === obj.crypto.createHmac('SHA384', obj.httpAuthRandom).update(auth.nonce).digest('hex'))) {
3947 +
3948 + // Read the data, we need to get the arg field
3949 + var eventData = '';
3950 + req.on('data', function (chunk) { eventData += chunk; });
3951 + req.on('end', function () {
3952 +
3953 + // Completed event read, let get the argument that must contain the nodeid
3954 + var i = eventData.indexOf('<m:arg xmlns:m="http://x.com">');
3955 + if (i > 0) {
3956 + var nodeid = eventData.substring(i + 30, i + 30 + 64);
3957 + if (nodeid.length == 64) {
3958 + var nodekey = 'node/' + domain.id + '/' + nodeid;
3959 +
3960 + // See if this node exists in the database
3961 + obj.db.Get(nodekey, function (err, nodes) {
3962 + if (nodes.length == 1) {
3963 + // Yes, the node exists, compute Intel AMT digest password
3964 + var node = nodes[0];
3965 + var amtpass = obj.crypto.createHash('sha384').update(auth.username.toLowerCase() + ':' + nodeid + ":" + obj.parent.dbconfig.amtWsEventSecret).digest('base64').substring(0, 12).split('/').join('x').split('\\').join('x');
3966 +
3967 + // Check the MD5 hash
3968 + if (auth.response === obj.common.ComputeDigesthash(auth.username, amtpass, auth.realm, 'POST', auth.uri, auth.qop, auth.nonce, auth.nc, auth.cnonce)) {
3969 +
3970 + // This is an authenticated Intel AMT event, update the host address
3971 + var amthost = req.clientIp;
3972 + if (amthost.substring(0, 7) === '::ffff:') { amthost = amthost.substring(7); }
3973 + if (node.host != amthost) {
3974 + // Get the mesh for this device
3975 + var mesh = obj.meshes[node.meshid];
3976 + if (mesh) {
3977 + // Update the database
3978 + var oldname = node.host;
3979 + node.host = amthost;
3980 + obj.db.Set(obj.cleanDevice(node));
3981 +
3982 + // Event the node change
3983 + var event = { etype: 'node', action: 'changenode', nodeid: node._id, domain: domain.id, msg: 'Intel(R) AMT host change ' + node.name + ' from group ' + mesh.name + ': ' + oldname + ' to ' + amthost };
3984 +
3985 + // Remove the Intel AMT password before eventing this.
3986 + event.node = node;
3987 + if (event.node.intelamt && event.node.intelamt.pass) {
3988 + event.node = Object.assign({}, event.node); // Shallow clone
3989 + event.node.intelamt = Object.assign({}, event.node.intelamt); // Shallow clone
3990 + delete event.node.intelamt.pass;
3991 + }
3992 +
3993 + if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
3994 + obj.parent.DispatchEvent(['*', node.meshid], obj, event);
3995 + }
3996 + }
3997 +
3998 + parent.amtEventHandler.handleAmtEvent(eventData, nodeid, amthost);
3999 + //res.send('OK');
4000 +
4001 + return;
4002 + }
4003 + }
4004 + });
4005 + }
4006 + }
4007 + });
4008 + }
4009 + }
4010 + }
4011 + } catch (e) { console.log(e); }
4012 +
4013 + // Send authentication response
4014 + obj.crypto.randomBytes(48, function (err, buf) {
4015 + var nonce = buf.toString('hex'), opaque = obj.crypto.createHmac('SHA384', obj.httpAuthRandom).update(nonce).digest('hex');
4016 + res.set({ 'WWW-Authenticate': 'Digest realm="' + obj.httpAuthRealm + '", qop="auth,auth-int", nonce="' + nonce + '", opaque="' + opaque + '"' });
4017 + res.sendStatus(401);
4018 + });
4019 + };
4020 +
4021 + // Handle a server backup request
4022 + function handleBackupRequest(req, res) {
4023 + const domain = checkUserIpAddress(req, res);
4024 + if (domain == null) { return; }
4025 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
4026 + if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
4027 + if ((domain.myserver === false) || ((domain.myserver != null) && (domain.myserver.backup !== true))) { res.sendStatus(401); return; }
4028 +
4029 + var user = obj.users[req.session.userid];
4030 + if ((user == null) || ((user.siteadmin & 1) == 0)) { res.sendStatus(401); return; } // Check if we have server backup rights
4031 +
4032 + // Require modules
4033 + const archive = require('archiver')('zip', { level: 9 }); // Sets the compression method to maximum.
4034 +
4035 + // Good practice to catch this error explicitly
4036 + archive.on('error', function (err) { throw err; });
4037 +
4038 + // Set the archive name
4039 + res.attachment((domain.title ? domain.title : 'MeshCentral') + '-Backup-' + new Date().toLocaleDateString().replace('/', '-').replace('/', '-') + '.zip');
4040 +
4041 + // Pipe archive data to the file
4042 + archive.pipe(res);
4043 +
4044 + // Append files from a glob pattern
4045 + archive.directory(obj.parent.datapath, false);
4046 +
4047 + // Finalize the archive (ie we are done appending files but streams have to finish yet)
4048 + archive.finalize();
4049 + }
4050 +
4051 + // Handle a server restore request
4052 + function handleRestoreRequest(req, res) {
4053 + const domain = checkUserIpAddress(req, res);
4054 + if (domain == null) { return; }
4055 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
4056 + if ((domain.myserver === false) || ((domain.myserver != null) && (domain.myserver.restore !== true))) { res.sendStatus(401); return; }
4057 +
4058 + var authUserid = null;
4059 + if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
4060 + const multiparty = require('multiparty');
4061 + const form = new multiparty.Form();
4062 + form.parse(req, function (err, fields, files) {
4063 + // If an authentication cookie is embedded in the form, use that.
4064 + if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
4065 + var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
4066 + if ((loginCookie != null) && (obj.args.cookieipcheck !== false) && (loginCookie.ip != null) && (loginCookie.ip != req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
4067 + if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
4068 + }
4069 + if (authUserid == null) { res.sendStatus(401); return; }
4070 +
4071 + // Get the user
4072 + const user = obj.users[req.session.userid];
4073 + if ((user == null) || ((user.siteadmin & 4) == 0)) { res.sendStatus(401); return; } // Check if we have server restore rights
4074 +
4075 + res.set('Content-Type', 'text/html');
4076 + res.end('<html><body>Server must be restarted, <a href="' + domain.url + '">click here to login</a>.</body></html>');
4077 + parent.Stop(files.datafile[0].path);
4078 + });
4079 + }
4080 +
4081 + // Handle a request to download a mesh agent
4082 + obj.handleMeshAgentRequest = function (req, res) {
4083 + var domain = getDomain(req, res);
4084 + if (domain == null) { parent.debug('web', 'handleRootRequest: invalid domain.'); try { res.sendStatus(404); } catch (ex) { } return; }
4085 +
4086 + // If required, check if this user has rights to do this
4087 + if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true)) && (req.session.userid == null)) { res.sendStatus(401); return; }
4088 +
4089 + if ((req.query.meshinstall != null) && (req.query.id != null)) {
4090 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
4091 +
4092 + // Send meshagent with included self installer for a specific platform back
4093 + // Start by getting the .msh for this request
4094 + var meshsettings = getMshFromRequest(req, res, domain);
4095 + if (meshsettings == null) { res.sendStatus(401); return; }
4096 +
4097 + // Get the interactive install script, this only works for non-Windows agents
4098 + var agentid = parseInt(req.query.meshinstall);
4099 + var argentInfo = obj.parent.meshAgentBinaries[agentid];
4100 + var scriptInfo = obj.parent.meshAgentInstallScripts[6];
4101 + if ((argentInfo == null) || (scriptInfo == null) || (argentInfo.platform == 'win32')) { res.sendStatus(404); return; }
4102 +
4103 + // Change the .msh file into JSON format and merge it into the install script
4104 + var tokens, msh = {}, meshsettingslines = meshsettings.split('\r').join('').split('\n');
4105 + for (var i in meshsettingslines) { tokens = meshsettingslines[i].split('='); if (tokens.length == 2) { msh[tokens[0]] = tokens[1]; } }
4106 + var js = scriptInfo.data.replace('var msh = {};', 'var msh = ' + JSON.stringify(msh) + ';');
4107 +
4108 + // Get the agent filename
4109 + var meshagentFilename = 'meshagent';
4110 + if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) { meshagentFilename = domain.agentcustomization.filename; }
4111 +
4112 + setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename, null, 'meshagent');
4113 + res.statusCode = 200;
4114 + obj.parent.exeHandler.streamExeWithJavaScript({ platform: argentInfo.platform, sourceFileName: argentInfo.path, destinationStream: res, js: Buffer.from(js, 'utf8'), peinfo: argentInfo.pe });
4115 + } else if (req.query.id != null) {
4116 + // Send a specific mesh agent back
4117 + var argentInfo = obj.parent.meshAgentBinaries[req.query.id];
4118 + if (argentInfo == null) { res.sendStatus(404); return; }
4119 +
4120 + // Download PDB debug files, only allowed for administrator or accounts with agent dump access
4121 + if (req.query.pdb == 1) {
4122 + if ((req.session == null) || (req.session.userid == null)) { res.sendStatus(404); return; }
4123 + var user = obj.users[req.session.userid];
4124 + if (user == null) { res.sendStatus(404); return; }
4125 + if ((user != null) && ((user.siteadmin == 0xFFFFFFFF) || ((Array.isArray(obj.parent.config.settings.agentcoredumpusers)) && (obj.parent.config.settings.agentcoredumpusers.indexOf(user._id) >= 0)))) {
4126 + if (argentInfo.id == 3) { setContentDispositionHeader(res, 'application/octet-stream', 'MeshService.pdb', null, 'MeshService.pdb'); res.sendFile(argentInfo.path.split('MeshService-signed.exe').join('MeshService.pdb')); return; }
4127 + if (argentInfo.id == 4) { setContentDispositionHeader(res, 'application/octet-stream', 'MeshService64.pdb', null, 'MeshService64.pdb'); res.sendFile(argentInfo.path.split('MeshService64-signed.exe').join('MeshService64.pdb')); return; }
4128 + }
4129 + res.sendStatus(404); return;
4130 + }
4131 +
4132 + if ((req.query.meshid == null) || (argentInfo.platform != 'win32')) {
4133 + // Get the agent filename
4134 + var meshagentFilename = argentInfo.rname;
4135 + if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) { meshagentFilename = domain.agentcustomization.filename; }
4136 + setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename, null, 'meshagent');
4137 + if (argentInfo.data == null) { res.sendFile(argentInfo.path); } else { res.end(argentInfo.data); }
4138 + } else {
4139 + // Check if the meshid is a time limited, encrypted cookie
4140 + var meshcookie = obj.parent.decodeCookie(req.query.meshid, obj.parent.invitationLinkEncryptionKey);
4141 + if ((meshcookie != null) && (meshcookie.m != null)) { req.query.meshid = meshcookie.m; }
4142 +
4143 + // We are going to embed the .msh file into the Windows executable (signed or not).
4144 + // First, fetch the mesh object to build the .msh file
4145 + var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.meshid];
4146 + if (mesh == null) { res.sendStatus(401); return; }
4147 +
4148 + // If required, check if this user has rights to do this
4149 + if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true))) {
4150 + if ((domain.id != mesh.domain) || ((obj.GetMeshRights(req.session.userid, mesh) & 1) == 0)) { res.sendStatus(401); return; }
4151 + }
4152 +
4153 + var meshidhex = Buffer.from(req.query.meshid.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
4154 + var serveridhex = Buffer.from(obj.agentCertificateHashBase64.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
4155 + var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port if specified
4156 + if (obj.args.agentport != null) { httpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
4157 + if (obj.args.agentaliasport != null) { httpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
4158 +
4159 + // Prepare a mesh agent file name using the device group name.
4160 + var meshfilename = mesh.name
4161 + meshfilename = meshfilename.split('\\').join('').split('/').join('').split(':').join('').split('*').join('').split('?').join('').split('"').join('').split('<').join('').split('>').join('').split('|').join('').split(' ').join('').split('\'').join('');
4162 + if (argentInfo.rname.endsWith('.exe')) { meshfilename = argentInfo.rname.substring(0, argentInfo.rname.length - 4) + '-' + meshfilename + '.exe'; } else { meshfilename = argentInfo.rname + '-' + meshfilename; }
4163 +
4164 + // Customize the mesh agent file name
4165 + if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) {
4166 + meshfilename = meshfilename.split('meshagent').join(domain.agentcustomization.filename);
4167 + meshfilename = meshfilename.split('MeshAgent').join(domain.agentcustomization.filename);
4168 + }
4169 +
4170 + // Get the agent connection server name
4171 + var serverName = obj.getWebServerName(domain);
4172 + if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
4173 +
4174 + // Build the agent connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
4175 + var xdomain = (domain.dns == null) ? domain.id : '';
4176 + if (xdomain != '') xdomain += '/';
4177 + var meshsettings = '\r\nMeshName=' + mesh.name + '\r\nMeshType=' + mesh.mtype + '\r\nMeshID=0x' + meshidhex + '\r\nServerID=' + serveridhex + '\r\n';
4178 + if (obj.args.lanonly != true) { meshsettings += 'MeshServer=wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'agent.ashx\r\n'; } else {
4179 + meshsettings += 'MeshServer=local\r\n';
4180 + if ((obj.args.localdiscovery != null) && (typeof obj.args.localdiscovery.key == 'string') && (obj.args.localdiscovery.key.length > 0)) { meshsettings += 'DiscoveryKey=' + obj.args.localdiscovery.key + '\r\n'; }
4181 + }
4182 + if ((req.query.tag != null) && (typeof req.query.tag == 'string') && (obj.common.isAlphaNumeric(req.query.tag) == true)) { meshsettings += 'Tag=' + req.query.tag + '\r\n'; }
4183 + if ((req.query.installflags != null) && (req.query.installflags != 0) && (parseInt(req.query.installflags) == req.query.installflags)) { meshsettings += 'InstallFlags=' + parseInt(req.query.installflags) + '\r\n'; }
4184 + if ((domain.agentnoproxy === true) || (obj.args.lanonly == true)) { meshsettings += 'ignoreProxyFile=1\r\n'; }
4185 + if (obj.args.agentconfig) { for (var i in obj.args.agentconfig) { meshsettings += obj.args.agentconfig[i] + '\r\n'; } }
4186 + if (domain.agentconfig) { for (var i in domain.agentconfig) { meshsettings += domain.agentconfig[i] + '\r\n'; } }
4187 + if (domain.agentcustomization != null) { // Add agent customization
4188 + if (domain.agentcustomization.displayname != null) { meshsettings += 'displayName=' + domain.agentcustomization.displayname + '\r\n'; }
4189 + if (domain.agentcustomization.description != null) { meshsettings += 'description=' + domain.agentcustomization.description + '\r\n'; }
4190 + if (domain.agentcustomization.companyname != null) { meshsettings += 'companyName=' + domain.agentcustomization.companyname + '\r\n'; }
4191 + if (domain.agentcustomization.servicename != null) { meshsettings += 'meshServiceName=' + domain.agentcustomization.servicename + '\r\n'; }
4192 + if (domain.agentcustomization.filename != null) { meshsettings += 'fileName=' + domain.agentcustomization.filename + '\r\n'; }
4193 + }
4194 + if (parent.agentTranslations != null) { meshsettings += 'translation=' + parent.agentTranslations + '\r\n'; }
4195 + setContentDispositionHeader(res, 'application/octet-stream', meshfilename, null, argentInfo.rname);
4196 + obj.parent.exeHandler.streamExeWithMeshPolicy({ platform: 'win32', sourceFileName: obj.parent.meshAgentBinaries[req.query.id].path, destinationStream: res, msh: meshsettings, peinfo: obj.parent.meshAgentBinaries[req.query.id].pe });
4197 + }
4198 + } else if (req.query.script != null) {
4199 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
4200 +
4201 + // Send a specific mesh install script back
4202 + var scriptInfo = obj.parent.meshAgentInstallScripts[req.query.script];
4203 + if (scriptInfo == null) { res.sendStatus(404); return; }
4204 + setContentDispositionHeader(res, 'application/octet-stream', scriptInfo.rname, null, 'script');
4205 + var data = scriptInfo.data;
4206 + var cmdoptions = { wgetoptionshttp: '', wgetoptionshttps: '', curloptionshttp: '-L ', curloptionshttps: '-L ' }
4207 + if (obj.isTrustedCert(domain) != true) {
4208 + cmdoptions.wgetoptionshttps += '--no-check-certificate ';
4209 + cmdoptions.curloptionshttps += '-k ';
4210 + }
4211 + if (domain.agentnoproxy === true) {
4212 + cmdoptions.wgetoptionshttp += '--no-proxy ';
4213 + cmdoptions.wgetoptionshttps += '--no-proxy ';
4214 + cmdoptions.curloptionshttp += '--noproxy \'*\' ';
4215 + cmdoptions.curloptionshttps += '--noproxy \'*\' ';
4216 + }
4217 + for (var i in cmdoptions) { data = data.split('{{{' + i + '}}}').join(cmdoptions[i]); }
4218 + res.send(data);
4219 + } else if (req.query.meshcmd != null) {
4220 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
4221 +
4222 + // Send meshcmd for a specific platform back
4223 + var agentid = parseInt(req.query.meshcmd);
4224 + // If the agentid is 3 or 4, check if we have a signed MeshCmd.exe
4225 + if ((agentid == 3)) { // Signed Windows MeshCmd.exe x86
4226 + var stats = null, meshCmdPath = obj.path.join(__dirname, 'agents', 'MeshCmd-signed.exe');
4227 + try { stats = obj.fs.statSync(meshCmdPath); } catch (e) { }
4228 + if ((stats != null)) {
4229 + setContentDispositionHeader(res, 'application/octet-stream', 'meshcmd' + ((req.query.meshcmd <= 3) ? '.exe' : ''), null, 'meshcmd');
4230 + res.sendFile(meshCmdPath); return;
4231 + }
4232 + } else if ((agentid == 4)) { // Signed Windows MeshCmd64.exe x64
4233 + var stats = null, meshCmd64Path = obj.path.join(__dirname, 'agents', 'MeshCmd64-signed.exe');
4234 + try { stats = obj.fs.statSync(meshCmd64Path); } catch (e) { }
4235 + if ((stats != null)) {
4236 + setContentDispositionHeader(res, 'application/octet-stream', 'meshcmd' + ((req.query.meshcmd <= 4) ? '.exe' : ''), null, 'meshcmd');
4237 + res.sendFile(meshCmd64Path); return;
4238 + }
4239 + }
4240 + // No signed agents, we are going to merge a new MeshCmd.
4241 + if ((agentid < 10000) && (obj.parent.meshAgentBinaries[agentid + 10000] != null)) { agentid += 10000; } // Avoid merging javascript to a signed mesh agent.
4242 + var argentInfo = obj.parent.meshAgentBinaries[agentid];
4243 + if ((argentInfo == null) || (obj.parent.defaultMeshCmd == null)) { res.sendStatus(404); return; }
4244 + setContentDispositionHeader(res, 'application/octet-stream', 'meshcmd' + ((req.query.meshcmd <= 4) ? '.exe' : ''), null, 'meshcmd');
4245 + res.statusCode = 200;
4246 + if (argentInfo.signedMeshCmdPath != null) {
4247 + // If we have a pre-signed MeshCmd, send that.
4248 + res.sendFile(argentInfo.signedMeshCmdPath);
4249 + } else {
4250 + // Merge JavaScript to a unsigned agent and send that.
4251 + obj.parent.exeHandler.streamExeWithJavaScript({ platform: argentInfo.platform, sourceFileName: argentInfo.path, destinationStream: res, js: Buffer.from(obj.parent.defaultMeshCmd, 'utf8'), peinfo: argentInfo.pe });
4252 + }
4253 + } else if (req.query.meshaction != null) {
4254 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
4255 + var user = obj.users[req.session.userid];
4256 + if (user == null) {
4257 + // Check if we have an authentication cookie
4258 + var c = obj.parent.decodeCookie(req.query.auth, obj.parent.loginCookieEncryptionKey);
4259 + if (c == null) { res.sendStatus(404); return; }
4260 +
4261 + // Download tools using a cookie
4262 + if (c.download == req.query.meshaction) {
4263 + if (req.query.meshaction == 'winrouter') {
4264 + var p = obj.path.join(__dirname, 'agents', 'MeshCentralRouter.exe');
4265 + if (obj.fs.existsSync(p)) {
4266 + setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralRouter.exe', null, 'MeshCentralRouter.exe');
4267 + try { res.sendFile(p); } catch (e) { res.sendStatus(404); }
4268 + } else { res.sendStatus(404); }
4269 + } else if (req.query.meshaction == 'winassistant') {
4270 + var p = obj.path.join(__dirname, 'agents', 'MeshCentralAssistant.exe');
4271 + if (obj.fs.existsSync(p)) {
4272 + setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralAssistant.exe', null, 'MeshCentralAssistant.exe');
4273 + try { res.sendFile(p); } catch (e) { res.sendStatus(404); }
4274 + } else { res.sendStatus(404); }
4275 + } else if (req.query.meshaction == 'macrouter') {
4276 + var p = obj.path.join(__dirname, 'agents', 'MeshCentralRouter.dmg');
4277 + if (obj.fs.existsSync(p)) {
4278 + setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralRouter.dmg', null, 'MeshCentralRouter.dmg');
4279 + try { res.sendFile(p); } catch (e) { res.sendStatus(404); }
4280 + } else { res.sendStatus(404); }
4281 + }
4282 + return;
4283 + }
4284 +
4285 + // Check if the cookie authenticates a user
4286 + if (c.userid == null) { res.sendStatus(404); return; }
4287 + user = obj.users[c.userid];
4288 + if (user == null) { res.sendStatus(404); return; }
4289 + }
4290 + if ((req.query.meshaction == 'route') && (req.query.nodeid != null)) {
4291 + obj.db.Get(req.query.nodeid, function (err, nodes) {
4292 + if (nodes.length != 1) { res.sendStatus(401); return; }
4293 + var node = nodes[0];
4294 +
4295 + // Create the meshaction.txt file for meshcmd.exe
4296 + var meshaction = {
4297 + action: req.query.meshaction,
4298 + localPort: 1234,
4299 + remoteName: node.name,
4300 + remoteNodeId: node._id,
4301 + remoteTarget: null,
4302 + remotePort: 3389,
4303 + username: '',
4304 + password: '',
4305 + serverId: obj.agentCertificateHashHex.toUpperCase(), // SHA384 of server HTTPS public key
4306 + serverHttpsHash: Buffer.from(obj.webCertificateHashs[domain.id], 'binary').toString('hex').toUpperCase(), // SHA384 of server HTTPS certificate
4307 + debugLevel: 0
4308 + };
4309 + if (user != null) { meshaction.username = user.name; }
4310 + if (req.query.key != null) { meshaction.loginKey = req.query.key; }
4311 + var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
4312 + if (obj.args.lanonly != true) { meshaction.serverUrl = 'wss://' + obj.getWebServerName(domain) + ':' + httpsPort + '/' + ((domain.id == '') ? '' : ('/' + domain.id)) + 'meshrelay.ashx'; }
4313 +
4314 + setContentDispositionHeader(res, 'application/octet-stream', 'meshaction.txt', null, 'meshaction.txt');
4315 + res.send(JSON.stringify(meshaction, null, ' '));
4316 + });
4317 + } else if (req.query.meshaction == 'generic') {
4318 + var meshaction = {
4319 + username: user.name,
4320 + password: '',
4321 + serverId: obj.agentCertificateHashHex.toUpperCase(), // SHA384 of server HTTPS public key
4322 + serverHttpsHash: Buffer.from(obj.webCertificateHashs[domain.id], 'binary').toString('hex').toUpperCase(), // SHA384 of server HTTPS certificate
4323 + debugLevel: 0
4324 + };
4325 + if (user != null) { meshaction.username = user.name; }
4326 + if (req.query.key != null) { meshaction.loginKey = req.query.key; }
4327 + var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
4328 + if (obj.args.lanonly != true) { meshaction.serverUrl = 'wss://' + obj.getWebServerName(domain) + ':' + httpsPort + '/' + ((domain.id == '') ? '' : ('/' + domain.id)) + 'meshrelay.ashx'; }
4329 + setContentDispositionHeader(res, 'application/octet-stream', 'meshaction.txt', null, 'meshaction.txt');
4330 + res.send(JSON.stringify(meshaction, null, ' '));
4331 + } else if (req.query.meshaction == 'winrouter') {
4332 + console.log('t2');
4333 + var p = obj.path.join(__dirname, 'agents', 'MeshCentralRouter.exe');
4334 + if (obj.fs.existsSync(p)) {
4335 + setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralRouter.exe', null, 'MeshCentralRouter.exe');
4336 + try { res.sendFile(p); } catch (e) { res.sendStatus(404); }
4337 + } else { res.sendStatus(404); }
4338 + } else if (req.query.meshaction == 'winassistant') {
4339 + var p = obj.path.join(__dirname, 'agents', 'MeshCentralAssistant.exe');
4340 + if (obj.fs.existsSync(p)) {
4341 + setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralAssistant.exe', null, 'MeshCentralAssistant.exe');
4342 + try { res.sendFile(p); } catch (e) { res.sendStatus(404); }
4343 + } else { res.sendStatus(404); }
4344 + } else if (req.query.meshaction == 'macrouter') {
4345 + var p = obj.path.join(__dirname, 'agents', 'MeshCentralRouter.dmg');
4346 + if (obj.fs.existsSync(p)) {
4347 + setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralRouter.dmg', null, 'MeshCentralRouter.dmg');
4348 + try { res.sendFile(p); } catch (e) { res.sendStatus(404); }
4349 + } else { res.sendStatus(404); }
4350 + } else {
4351 + res.sendStatus(401);
4352 + }
4353 + } else {
4354 + domain = checkUserIpAddress(req, res); // Recheck the domain to apply user IP filtering.
4355 + if (domain == null) return;
4356 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
4357 + if ((req.session == null) || (req.session.userid == null)) { res.sendStatus(404); return; }
4358 + var user = null, coreDumpsAllowed = false;
4359 + if (typeof req.session.userid == 'string') { user = obj.users[req.session.userid]; }
4360 + if (user == null) { res.sendStatus(404); return; }
4361 +
4362 + // Check if this user has access to agent core dumps
4363 + if ((obj.parent.config.settings.agentcoredump === true) && ((user.siteadmin == 0xFFFFFFFF) || ((Array.isArray(obj.parent.config.settings.agentcoredumpusers)) && (obj.parent.config.settings.agentcoredumpusers.indexOf(user._id) >= 0)))) {
4364 + coreDumpsAllowed = true;
4365 +
4366 + if ((req.query.dldump != null) && obj.common.IsFilenameValid(req.query.dldump)) {
4367 + // Download a dump file
4368 + var dumpFile = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps', req.query.dldump);
4369 + if (obj.fs.existsSync(dumpFile)) {
4370 + setContentDispositionHeader(res, 'application/octet-stream', req.query.dldump, null, 'file.bin');
4371 + res.sendFile(dumpFile); return;
4372 + } else {
4373 + res.sendStatus(404); return;
4374 + }
4375 + }
4376 +
4377 + if ((req.query.deldump != null) && obj.common.IsFilenameValid(req.query.deldump)) {
4378 + // Delete a dump file
4379 + try { obj.fs.unlinkSync(obj.path.join(parent.datapath, '..', 'meshcentral-coredumps', req.query.deldump)); } catch (ex) { console.log(ex); }
4380 + }
4381 +
4382 + if ((req.query.dumps != null) || (req.query.deldump != null)) {
4383 + // Send list of agent core dumps
4384 + var response = '<html><head><title>Mesh Agents Core Dumps</title><style>table,th,td { border:1px solid black;border-collapse:collapse;padding:3px; }</style></head><body style=overflow:auto><table>';
4385 + response += '<tr style="background-color:lightgray"><th>ID</th><th>Upload Date</th><th>Description</th><th>Current</th><th>Dump</th><th>Size</th><th>Agent</th><th>Agent SHA384</th><th>NodeID</th><th></th></tr>';
4386 +
4387 + var coreDumpPath = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps');
4388 + if (obj.fs.existsSync(coreDumpPath)) {
4389 + var files = obj.fs.readdirSync(coreDumpPath);
4390 + var coredumps = [];
4391 + for (var i in files) {
4392 + var file = files[i];
4393 + if (file.endsWith('.dmp')) {
4394 + var fileSplit = file.substring(0, file.length - 4).split('-');
4395 + if (fileSplit.length == 3) {
4396 + var agentid = parseInt(fileSplit[0]);
4397 + if ((isNaN(agentid) == false) && (obj.parent.meshAgentBinaries[agentid] != null)) {
4398 + var agentinfo = obj.parent.meshAgentBinaries[agentid];
4399 + var filestats = obj.fs.statSync(obj.path.join(parent.datapath, '..', 'meshcentral-coredumps', file));
4400 + coredumps.push({
4401 + fileSplit: fileSplit,
4402 + agentinfo: agentinfo,
4403 + filestats: filestats,
4404 + currentAgent: agentinfo.hashhex.startsWith(fileSplit[1].toLowerCase()),
4405 + downloadUrl: req.originalUrl.split('?')[0] + '?dldump=' + file + (req.query.key ? ('&key=' + req.query.key) : ''),
4406 + deleteUrl: req.originalUrl.split('?')[0] + '?deldump=' + file + (req.query.key ? ('&key=' + req.query.key) : ''),
4407 + agentUrl: req.originalUrl.split('?')[0] + '?id=' + agentinfo.id + (req.query.key ? ('&key=' + req.query.key) : ''),
4408 + time: new Date(filestats.ctime)
4409 + });
4410 + }
4411 + }
4412 + }
4413 + }
4414 + coredumps.sort(function (a, b) { if (a.time > b.time) return -1; if (a.time < b.time) return 1; return 0; });
4415 + for (var i in coredumps) {
4416 + var d = coredumps[i];
4417 + response += '<tr><td>' + d.agentinfo.id + '</td><td>' + d.time.toDateString().split(' ').join('&nbsp;') + '</td><td>' + d.agentinfo.desc.split(' ').join('&nbsp;') + '</td>';
4418 + response += '<td style=text-align:center>' + d.currentAgent + '</td><td><a download href="' + d.downloadUrl + '">Download</a></td><td style=text-align:right>' + d.filestats.size + '</td>';
4419 + if (d.currentAgent) { response += '<td><a download href="' + d.agentUrl + '">Download</a></td>'; } else { response += '<td></td>'; }
4420 + response += '<td>' + d.fileSplit[1].toLowerCase() + '</td><td>' + d.fileSplit[2] + '</td><td><a href="' + d.deleteUrl + '">Delete</a></td></tr>';
4421 + }
4422 + }
4423 + response += '</table><a href="' + req.originalUrl.split('?')[0] + (req.query.key ? ('?key=' + req.query.key) : '') + '">Mesh Agents</a></body></html>';
4424 + res.send(response);
4425 + return;
4426 + }
4427 + }
4428 +
4429 + if (req.query.cores != null) {
4430 + // Send list of agent cores
4431 + var response = '<html><head><title>Mesh Agents Cores</title><style>table,th,td { border:1px solid black;border-collapse:collapse;padding:3px; }</style></head><body style=overflow:auto><table>';
4432 + response += '<tr style="background-color:lightgray"><th>Name</th><th>Size</th><th>Comp</th><th>Decompressed Hash SHA384</th></tr>';
4433 + for (var i in parent.defaultMeshCores) {
4434 + response += '<tr><td>' + i.split(' ').join('&nbsp;') + '</td><td style="text-align:right"><a download href="/meshagents?dlcore=' + i + '">' + parent.defaultMeshCores[i].length + (req.query.key ? ('?key=' + req.query.key) : '') + '</a></td><td style="text-align:right"><a download href="/meshagents?dlccore=' + i + (req.query.key ? ('?key=' + req.query.key) : '') + '">' + parent.defaultMeshCoresDeflate[i].length + '</a></td><td>' + Buffer.from(parent.defaultMeshCoresHash[i], 'binary').toString('hex') + '</td></tr>';
4435 + }
4436 + response += '</table><a href="' + req.originalUrl.split('?')[0] + (req.query.key ? ('?key=' + req.query.key) : '') + '">Mesh Agents</a></body></html>';
4437 + res.send(response);
4438 + return;
4439 + }
4440 +
4441 + if (req.query.dlcore != null) {
4442 + // Download mesh core
4443 + var bin = parent.defaultMeshCores[req.query.dlcore];
4444 + if (bin == null) { res.sendStatus(404); return; }
4445 + setContentDispositionHeader(res, 'application/octet-stream', req.query.dlcore + '.js', null, 'meshcore.js');
4446 + res.send(bin);
4447 + return;
4448 + }
4449 +
4450 + if (req.query.dlccore != null) {
4451 + // Download compressed mesh core
4452 + var bin = parent.defaultMeshCoresDeflate[req.query.dlccore];
4453 + if (bin == null) { res.sendStatus(404); return; }
4454 + setContentDispositionHeader(res, 'application/octet-stream', req.query.dlccore + '.js.deflate', null, 'meshcore.js.deflate');
4455 + res.send(bin);
4456 + return;
4457 + }
4458 +
4459 + // Send a list of available mesh agents
4460 + var response = '<html><head><title>Mesh Agents</title><style>table,th,td { border:1px solid black;border-collapse:collapse;padding:3px; }</style></head><body style=overflow:auto><table>';
4461 + response += '<tr style="background-color:lightgray"><th>ID</th><th>Description</th><th>Link</th><th>Size</th><th>SHA384</th><th>MeshCmd</th></tr>';
4462 + var originalUrl = req.originalUrl.split('?')[0];
4463 + for (var agentid in obj.parent.meshAgentBinaries) {
4464 + if ((agentid >= 10000) && (agentid != 10005)) continue;
4465 + var agentinfo = obj.parent.meshAgentBinaries[agentid];
4466 + response += '<tr><td>' + agentinfo.id + '</td><td>' + agentinfo.desc.split(' ').join('&nbsp;') + '</td>';
4467 + response += '<td><a download href="' + originalUrl + '?id=' + agentinfo.id + (req.query.key ? ('&key=' + req.query.key) : '') + '">' + agentinfo.rname + '</a>';
4468 + if ((user.siteadmin == 0xFFFFFFFF) || ((Array.isArray(obj.parent.config.settings.agentcoredumpusers)) && (obj.parent.config.settings.agentcoredumpusers.indexOf(user._id) >= 0))) {
4469 + if ((agentid == 3) || (agentid == 4)) { response += ', <a download href="' + originalUrl + '?id=' + agentinfo.id + '&pdb=1' + (req.query.key ? ('&key=' + req.query.key) : '') + '">PDB</a>'; }
4470 + }
4471 + response += '</td>';
4472 + response += '<td>' + agentinfo.size + '</td><td>' + agentinfo.hashhex + '</td>';
4473 + response += '<td><a download href="' + originalUrl + '?meshcmd=' + agentinfo.id + (req.query.key ? ('&key=' + req.query.key) : '') + '">' + agentinfo.rname.replace('agent', 'cmd') + '</a></td></tr>';
4474 + }
4475 + response += '</table>';
4476 + response += '<a href="' + originalUrl + '?cores=1' + (req.query.key ? ('&key=' + req.query.key) : '') + '">MeshCores</a> ';
4477 + if (coreDumpsAllowed) { response += '<a href="' + originalUrl + '?dumps=1' + (req.query.key ? ('&key=' + req.query.key) : '') + '">MeshAgent Crash Dumps</a>'; }
4478 + response += '</body></html>';
4479 + res.send(response);
4480 + }
4481 + };
4482 +
4483 + // Get the web server hostname. This may change if using a domain with a DNS name.
4484 + obj.getWebServerName = function (domain) {
4485 + if (domain.dns != null) return domain.dns;
4486 + return obj.certificates.CommonName;
4487 + }
4488 +
4489 + // Create a OSX mesh agent installer
4490 + obj.handleMeshOsxAgentRequest = function (req, res) {
4491 + const domain = getDomain(req, res);
4492 + if (domain == null) { parent.debug('web', 'handleRootRequest: invalid domain.'); try { res.sendStatus(404); } catch (ex) { } return; }
4493 + if (req.query.id == null) { res.sendStatus(404); return; }
4494 +
4495 + // If required, check if this user has rights to do this
4496 + if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true)) && (req.session.userid == null)) { res.sendStatus(401); return; }
4497 +
4498 + // Send a specific mesh agent back
4499 + var argentInfo = obj.parent.meshAgentBinaries[req.query.id];
4500 + if ((argentInfo == null) || (req.query.meshid == null)) { res.sendStatus(404); return; }
4501 +
4502 + // Check if the meshid is a time limited, encrypted cookie
4503 + var meshcookie = obj.parent.decodeCookie(req.query.meshid, obj.parent.invitationLinkEncryptionKey);
4504 + if ((meshcookie != null) && (meshcookie.m != null)) { req.query.meshid = meshcookie.m; }
4505 +
4506 + // We are going to embed the .msh file into the Windows executable (signed or not).
4507 + // First, fetch the mesh object to build the .msh file
4508 + var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.meshid];
4509 + if (mesh == null) { res.sendStatus(401); return; }
4510 +
4511 + // If required, check if this user has rights to do this
4512 + if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true))) {
4513 + if ((domain.id != mesh.domain) || ((obj.GetMeshRights(req.session.userid, mesh) & 1) == 0)) { res.sendStatus(401); return; }
4514 + }
4515 +
4516 + var meshidhex = Buffer.from(req.query.meshid.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
4517 + var serveridhex = Buffer.from(obj.agentCertificateHashBase64.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
4518 +
4519 + // Get the agent connection server name
4520 + var serverName = obj.getWebServerName(domain);
4521 + if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
4522 +
4523 + // Build the agent connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
4524 + var xdomain = (domain.dns == null) ? domain.id : '';
4525 + if (xdomain != '') xdomain += '/';
4526 + var meshsettings = '\r\nMeshName=' + mesh.name + '\r\nMeshType=' + mesh.mtype + '\r\nMeshID=0x' + meshidhex + '\r\nServerID=' + serveridhex + '\r\n';
4527 + var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
4528 + if (obj.args.agentport != null) { httpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
4529 + if (obj.args.agentaliasport != null) { httpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
4530 + if (obj.args.lanonly != true) { meshsettings += 'MeshServer=wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'agent.ashx\r\n'; } else {
4531 + meshsettings += 'MeshServer=local\r\n';
4532 + if ((obj.args.localdiscovery != null) && (typeof obj.args.localdiscovery.key == 'string') && (obj.args.localdiscovery.key.length > 0)) { meshsettings += 'DiscoveryKey=' + obj.args.localdiscovery.key + '\r\n'; }
4533 + }
4534 + if ((req.query.tag != null) && (typeof req.query.tag == 'string') && (obj.common.isAlphaNumeric(req.query.tag) == true)) { meshsettings += 'Tag=' + req.query.tag + '\r\n'; }
4535 + if ((req.query.installflags != null) && (req.query.installflags != 0) && (parseInt(req.query.installflags) == req.query.installflags)) { meshsettings += 'InstallFlags=' + parseInt(req.query.installflags) + '\r\n'; }
4536 + if ((domain.agentnoproxy === true) || (obj.args.lanonly == true)) { meshsettings += 'ignoreProxyFile=1\r\n'; }
4537 + if (obj.args.agentconfig) { for (var i in obj.args.agentconfig) { meshsettings += obj.args.agentconfig[i] + '\r\n'; } }
4538 + if (domain.agentconfig) { for (var i in domain.agentconfig) { meshsettings += domain.agentconfig[i] + '\r\n'; } }
4539 + if (domain.agentcustomization != null) { // Add agent customization
4540 + if (domain.agentcustomization.displayname != null) { meshsettings += 'displayName=' + domain.agentcustomization.displayname + '\r\n'; }
4541 + if (domain.agentcustomization.description != null) { meshsettings += 'description=' + domain.agentcustomization.description + '\r\n'; }
4542 + if (domain.agentcustomization.companyname != null) { meshsettings += 'companyName=' + domain.agentcustomization.companyname + '\r\n'; }
4543 + if (domain.agentcustomization.servicename != null) { meshsettings += 'meshServiceName=' + domain.agentcustomization.servicename + '\r\n'; }
4544 + if (domain.agentcustomization.filename != null) { meshsettings += 'fileName=' + domain.agentcustomization.filename + '\r\n'; }
4545 + }
4546 + if (parent.agentTranslations != null) { meshsettings += 'translation=' + parent.agentTranslations + '\r\n'; }
4547 +
4548 + // Setup the response output
4549 + var archive = require('archiver')('zip', { level: 5 }); // Sets the compression method.
4550 + archive.on('error', function (err) { throw err; });
4551 +
4552 + // Set the agent download including the mesh name.
4553 + setContentDispositionHeader(res, 'application/octet-stream', 'MeshAgent-' + mesh.name + '.zip', null, 'MeshAgent.zip');
4554 + archive.pipe(res);
4555 +
4556 + // Opens the "MeshAgentOSXPackager.zip"
4557 + var yauzl = require('yauzl');
4558 + yauzl.open(obj.path.join(__dirname, 'agents', 'MeshAgentOSXPackager.zip'), { lazyEntries: true }, function (err, zipfile) {
4559 + if (err) { res.sendStatus(500); return; }
4560 + zipfile.readEntry();
4561 + zipfile.on('entry', function (entry) {
4562 + if (/\/$/.test(entry.fileName)) {
4563 + // Skip all folder entries
4564 + zipfile.readEntry();
4565 + } else {
4566 + if (entry.fileName == 'MeshAgent.mpkg/Contents/distribution.dist') {
4567 + // This is a special file entry, we need to fix it.
4568 + zipfile.openReadStream(entry, function (err, readStream) {
4569 + readStream.on('data', function (data) { if (readStream.xxdata) { readStream.xxdata += data; } else { readStream.xxdata = data; } });
4570 + readStream.on('end', function () {
4571 + var meshname = mesh.name.split(']').join('').split('[').join(''); // We can't have ']]' in the string since it will terminate the CDATA.
4572 + var welcomemsg = 'Welcome to the MeshCentral agent for MacOS\n\nThis installer will install the mesh agent for "' + meshname + '" and allow the administrator to remotely monitor and control this computer over the internet. For more information, go to https://www.meshcommander.com/meshcentral2.\n\nThis software is provided under Apache 2.0 license.\n';
4573 + var installsize = Math.floor((argentInfo.size + meshsettings.length) / 1024);
4574 + archive.append(readStream.xxdata.toString().split('###WELCOMEMSG###').join(welcomemsg).split('###INSTALLSIZE###').join(installsize), { name: entry.fileName });
4575 + zipfile.readEntry();
4576 + });
4577 + });
4578 + } else {
4579 + // Normal file entry
4580 + zipfile.openReadStream(entry, function (err, readStream) {
4581 + if (err) { throw err; }
4582 + var options = { name: entry.fileName };
4583 + if (entry.fileName.endsWith('postflight') || entry.fileName.endsWith('Uninstall.command')) { options.mode = 493; }
4584 + archive.append(readStream, options);
4585 + readStream.on('end', function () { zipfile.readEntry(); });
4586 + });
4587 + }
4588 + }
4589 + });
4590 + zipfile.on('end', function () {
4591 + archive.file(argentInfo.path, { name: 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/meshagent_osx64.bin' });
4592 + archive.append(meshsettings, { name: 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/meshagent_osx64.msh' });
4593 + archive.finalize();
4594 + });
4595 + });
4596 + }
4597 +
4598 + // Return a .msh file from a given request, id is the device group identifier or encrypted cookie with the identifier.
4599 + function getMshFromRequest(req, res, domain) {
4600 + // If required, check if this user has rights to do this
4601 + if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true)) && (req.session.userid == null)) { return null; }
4602 +
4603 + // Check if the meshid is a time limited, encrypted cookie
4604 + var meshcookie = obj.parent.decodeCookie(req.query.id, obj.parent.invitationLinkEncryptionKey);
4605 + if ((meshcookie != null) && (meshcookie.m != null)) { req.query.id = meshcookie.m; }
4606 +
4607 + // Fetch the mesh object
4608 + var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.id];
4609 + if (mesh == null) { return null; }
4610 +
4611 + // If needed, check if this user has rights to do this
4612 + if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true))) {
4613 + if ((domain.id != mesh.domain) || ((obj.GetMeshRights(req.session.userid, mesh) & 1) == 0)) { return null; }
4614 + }
4615 +
4616 + var meshidhex = Buffer.from(req.query.id.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
4617 + var serveridhex = Buffer.from(obj.agentCertificateHashBase64.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
4618 +
4619 + // Get the agent connection server name
4620 + var serverName = obj.getWebServerName(domain);
4621 + if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
4622 +
4623 + // Build the agent connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
4624 + var xdomain = (domain.dns == null) ? domain.id : '';
4625 + if (xdomain != '') xdomain += '/';
4626 + var meshsettings = '\r\nMeshName=' + mesh.name + '\r\nMeshType=' + mesh.mtype + '\r\nMeshID=0x' + meshidhex + '\r\nServerID=' + serveridhex + '\r\n';
4627 + var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
4628 + if (obj.args.agentport != null) { httpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
4629 + if (obj.args.agentaliasport != null) { httpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
4630 + if (obj.args.lanonly != true) { meshsettings += 'MeshServer=wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'agent.ashx\r\n'; } else {
4631 + meshsettings += 'MeshServer=local\r\n';
4632 + if ((obj.args.localdiscovery != null) && (typeof obj.args.localdiscovery.key == 'string') && (obj.args.localdiscovery.key.length > 0)) { meshsettings += 'DiscoveryKey=' + obj.args.localdiscovery.key + '\r\n'; }
4633 + }
4634 + if ((req.query.tag != null) && (typeof req.query.tag == 'string') && (obj.common.isAlphaNumeric(req.query.tag) == true)) { meshsettings += 'Tag=' + req.query.tag + '\r\n'; }
4635 + if ((req.query.installflags != null) && (req.query.installflags != 0) && (parseInt(req.query.installflags) == req.query.installflags)) { meshsettings += 'InstallFlags=' + parseInt(req.query.installflags) + '\r\n'; }
4636 + if ((domain.agentnoproxy === true) || (obj.args.lanonly == true)) { meshsettings += 'ignoreProxyFile=1\r\n'; }
4637 + if (obj.args.agentconfig) { for (var i in obj.args.agentconfig) { meshsettings += obj.args.agentconfig[i] + '\r\n'; } }
4638 + if (domain.agentconfig) { for (var i in domain.agentconfig) { meshsettings += domain.agentconfig[i] + '\r\n'; } }
4639 + if (domain.agentcustomization != null) { // Add agent customization
4640 + if (domain.agentcustomization.displayname != null) { meshsettings += 'displayName=' + domain.agentcustomization.displayname + '\r\n'; }
4641 + if (domain.agentcustomization.description != null) { meshsettings += 'description=' + domain.agentcustomization.description + '\r\n'; }
4642 + if (domain.agentcustomization.companyname != null) { meshsettings += 'companyName=' + domain.agentcustomization.companyname + '\r\n'; }
4643 + if (domain.agentcustomization.servicename != null) { meshsettings += 'meshServiceName=' + domain.agentcustomization.servicename + '\r\n'; }
4644 + if (domain.agentcustomization.filename != null) { meshsettings += 'fileName=' + domain.agentcustomization.filename + '\r\n'; }
4645 + }
4646 + if (parent.agentTranslations != null) { meshsettings += 'translation=' + parent.agentTranslations + '\r\n'; }
4647 + return meshsettings;
4648 + }
4649 +
4650 + // Handle a request to download a mesh settings
4651 + obj.handleMeshSettingsRequest = function (req, res) {
4652 + const domain = getDomain(req);
4653 + if (domain == null) { return; }
4654 + //if ((domain.id !== '') || (!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
4655 +
4656 + var meshsettings = getMshFromRequest(req, res, domain);
4657 + if (meshsettings == null) { res.sendStatus(401); return; }
4658 +
4659 + // Get the agent filename
4660 + var meshagentFilename = 'meshagent';
4661 + if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) { meshagentFilename = domain.agentcustomization.filename; }
4662 +
4663 + setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename + '.msh', null, 'meshagent.msh');
4664 + res.send(meshsettings);
4665 + };
4666 +
4667 + // Handle a request for power events
4668 + obj.handleDevicePowerEvents = function (req, res) {
4669 + const domain = checkUserIpAddress(req, res);
4670 + if (domain == null) { return; }
4671 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
4672 + if ((domain.id !== '') || (!req.session) || (req.session == null) || (!req.session.userid) || (req.query.id == null) || (typeof req.query.id != 'string')) { res.sendStatus(401); return; }
4673 + var x = req.query.id.split('/');
4674 + var user = obj.users[req.session.userid];
4675 + if ((x.length != 3) || (x[0] != 'node') || (x[1] != domain.id) || (user == null) || (user.links == null)) { res.sendStatus(401); return; }
4676 +
4677 + obj.db.Get(req.query.id, function (err, docs) {
4678 + if (docs.length != 1) {
4679 + res.sendStatus(401);
4680 + } else {
4681 + var node = docs[0];
4682 +
4683 + // Check if we have right to this node
4684 + if (obj.GetNodeRights(user, node.meshid, node._id) == 0) { res.sendStatus(401); return; }
4685 +
4686 + // Get the list of power events and send them
4687 + setContentDispositionHeader(res, 'application/octet-stream', 'powerevents.csv', null, 'powerevents.csv');
4688 + obj.db.getPowerTimeline(node._id, function (err, docs) {
4689 + var xevents = ['Time, State, Previous State'], prevState = 0;
4690 + for (var i in docs) {
4691 + if (docs[i].power != prevState) {
4692 + prevState = docs[i].power;
4693 + if (docs[i].oldPower != null) {
4694 + xevents.push(docs[i].time.toString() + ',' + docs[i].power + ',' + docs[i].oldPower);
4695 + } else {
4696 + xevents.push(docs[i].time.toString() + ',' + docs[i].power);
4697 + }
4698 + }
4699 + }
4700 + res.send(xevents.join('\r\n'));
4701 + });
4702 + }
4703 + });
4704 + }
4705 +
4706 + if (parent.pluginHandler != null) {
4707 + // Handle a plugin admin request
4708 + obj.handlePluginAdminReq = function (req, res) {
4709 + const domain = checkUserIpAddress(req, res);
4710 + if (domain == null) { return; }
4711 + if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
4712 + var user = obj.users[req.session.userid];
4713 + if (user == null) { res.sendStatus(401); return; }
4714 +
4715 + parent.pluginHandler.handleAdminReq(req, res, user, obj);
4716 + }
4717 +
4718 + obj.handlePluginAdminPostReq = function (req, res) {
4719 + const domain = checkUserIpAddress(req, res);
4720 + if (domain == null) { return; }
4721 + if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
4722 + var user = obj.users[req.session.userid];
4723 + if (user == null) { res.sendStatus(401); return; }
4724 +
4725 + parent.pluginHandler.handleAdminPostReq(req, res, user, obj);
4726 + }
4727 +
4728 + obj.handlePluginJS = function (req, res) {
4729 + const domain = checkUserIpAddress(req, res);
4730 + if (domain == null) { return; }
4731 + if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
4732 + var user = obj.users[req.session.userid];
4733 + if (user == null) { res.sendStatus(401); return; }
4734 +
4735 + parent.pluginHandler.refreshJS(req, res);
4736 + }
4737 + }
4738 +
4739 + // Starts the HTTPS server, this should be called after the user/mesh tables are loaded
4740 + function serverStart() {
4741 + // Start the server, only after users and meshes are loaded from the database.
4742 + if (obj.args.tlsoffload) {
4743 + // Setup the HTTP server without TLS
4744 + obj.expressWs = require('express-ws')(obj.app, null, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
4745 + } else {
4746 + // Setup the HTTP server with TLS, use only TLS 1.2 and higher with perfect forward secrecy (PFS).
4747 + //const tlsOptions = { cert: obj.certificates.web.cert, key: obj.certificates.web.key, ca: obj.certificates.web.ca, rejectUnauthorized: true, ciphers: "HIGH:!aNULL:!eNULL:!EXPORT:!RSA:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA", secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE | constants.SSL_OP_NO_TLSv1 | constants.SSL_OP_NO_TLSv1_1 }; // This does not work with TLS 1.3
4748 + const tlsOptions = { cert: obj.certificates.web.cert, key: obj.certificates.web.key, ca: obj.certificates.web.ca, rejectUnauthorized: true, ciphers: "HIGH:TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_AES_128_CCM_8_SHA256:TLS_AES_128_CCM_SHA256:TLS_CHACHA20_POLY1305_SHA256", secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE | constants.SSL_OP_NO_TLSv1 | constants.SSL_OP_NO_TLSv1_1 };
4749 + if (obj.tlsSniCredentials != null) { tlsOptions.SNICallback = TlsSniCallback; } // We have multiple web server certificate used depending on the domain name
4750 + obj.tlsServer = require('https').createServer(tlsOptions, obj.app);
4751 + obj.tlsServer.on('secureConnection', function () { /*console.log('tlsServer secureConnection');*/ });
4752 + obj.tlsServer.on('error', function (err) { console.log('tlsServer error', err); });
4753 + //obj.tlsServer.on('tlsClientError', function (err) { console.log('tlsClientError', err); });
4754 + obj.tlsServer.on('newSession', function (id, data, cb) { if (tlsSessionStoreCount > 1000) { tlsSessionStoreCount = 0; tlsSessionStore = {}; } tlsSessionStore[id.toString('hex')] = data; tlsSessionStoreCount++; cb(); });
4755 + obj.tlsServer.on('resumeSession', function (id, cb) { cb(null, tlsSessionStore[id.toString('hex')] || null); });
4756 + obj.expressWs = require('express-ws')(obj.app, obj.tlsServer, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
4757 + }
4758 +
4759 + // Start a second agent-only server if needed
4760 + if (obj.args.agentport) {
4761 + var agentPortTls = true;
4762 + if (obj.args.tlsoffload != null) { agentPortTls = false; }
4763 + if (typeof obj.args.agentporttls == 'boolean') { agentPortTls = obj.args.agentporttls; }
4764 + if (obj.certificates.webdefault == null) { agentPortTls = false; }
4765 +
4766 + if (agentPortTls == false) {
4767 + // Setup the HTTP server without TLS
4768 + obj.expressWsAlt = require('express-ws')(obj.agentapp, null, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
4769 + } else {
4770 + // Setup the agent HTTP server with TLS, use only TLS 1.2 and higher with perfect forward secrecy (PFS).
4771 + // If TLS is used on the agent port, we always use the default TLS certificate.
4772 + const tlsOptions = { cert: obj.certificates.webdefault.cert, key: obj.certificates.webdefault.key, ca: obj.certificates.webdefault.ca, rejectUnauthorized: true, ciphers: "HIGH:TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_AES_128_CCM_8_SHA256:TLS_AES_128_CCM_SHA256:TLS_CHACHA20_POLY1305_SHA256", secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE | constants.SSL_OP_NO_TLSv1 | constants.SSL_OP_NO_TLSv1_1 };
4773 + obj.tlsAltServer = require('https').createServer(tlsOptions, obj.agentapp);
4774 + obj.tlsAltServer.on('secureConnection', function () { /*console.log('tlsAltServer secureConnection');*/ });
4775 + obj.tlsAltServer.on('error', function (err) { console.log('tlsAltServer error', err); });
4776 + //obj.tlsAltServer.on('tlsClientError', function (err) { console.log('tlsClientError', err); });
4777 + obj.tlsAltServer.on('newSession', function (id, data, cb) { if (tlsSessionStoreCount > 1000) { tlsSessionStoreCount = 0; tlsSessionStore = {}; } tlsSessionStore[id.toString('hex')] = data; tlsSessionStoreCount++; cb(); });
4778 + obj.tlsAltServer.on('resumeSession', function (id, cb) { cb(null, tlsSessionStore[id.toString('hex')] || null); });
4779 + obj.expressWsAlt = require('express-ws')(obj.agentapp, obj.tlsAltServer, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
4780 + }
4781 + }
4782 +
4783 + // Setup middleware
4784 + obj.app.engine('handlebars', obj.exphbs({ defaultLayout: null })); // defaultLayout: 'main'
4785 + obj.app.set('view engine', 'handlebars');
4786 + if (obj.args.trustedproxy) {
4787 + // Reverse proxy should add the "X-Forwarded-*" headers
4788 + try {
4789 + obj.app.set('trust proxy', obj.args.trustedproxy);
4790 + } catch (ex) {
4791 + // If there is an error, try to resolve the string
4792 + if ((obj.args.trustedproxy.length == 1) && (typeof obj.args.trustedproxy[0] == 'string')) {
4793 + require('dns').lookup(obj.args.trustedproxy[0], function (err, address, family) { if (err == null) { obj.app.set('trust proxy', address); obj.args.trustedproxy = [address]; } });
4794 + }
4795 + }
4796 + }
4797 + else if (typeof obj.args.tlsoffload == 'object') {
4798 + // Reverse proxy should add the "X-Forwarded-*" headers
4799 + try {
4800 + obj.app.set('trust proxy', obj.args.tlsoffload);
4801 + } catch (ex) {
4802 + // If there is an error, try to resolve the string
4803 + if ((Array.isArray(obj.args.tlsoffload)) && (obj.args.tlsoffload.length == 1) && (typeof obj.args.tlsoffload[0] == 'string')) {
4804 + require('dns').lookup(obj.args.tlsoffload[0], function (err, address, family) { if (err == null) { obj.app.set('trust proxy', address); obj.args.tlsoffload = [address]; } });
4805 + }
4806 + }
4807 + }
4808 + obj.app.use(obj.bodyParser.urlencoded({ extended: false }));
4809 + var sessionOptions = {
4810 + name: 'xid', // Recommended security practice to not use the default cookie name
4811 + httpOnly: true,
4812 + keys: [obj.args.sessionkey], // If multiple instances of this server are behind a load-balancer, this secret must be the same for all instances
4813 + secure: (obj.args.tlsoffload == null) // Use this cookie only over TLS (Check this: https://expressjs.com/en/guide/behind-proxies.html)
4814 + }
4815 + if (obj.args.sessionsamesite != null) { sessionOptions.sameSite = obj.args.sessionsamesite; } else { sessionOptions.sameSite = 'strict'; }
4816 + if (obj.args.sessiontime != null) { sessionOptions.maxAge = (obj.args.sessiontime * 60 * 1000); }
4817 + obj.app.use(obj.session(sessionOptions));
4818 +
4819 + // Add HTTP security headers to all responses
4820 + obj.app.use(function (req, res, next) {
4821 + // Useful for debugging reverse proxy issues
4822 + parent.debug('httpheaders', req.method, req.url, req.headers);
4823 +
4824 + // Set the real IP address of the request
4825 + // If a trusted reverse-proxy is sending us the remote IP address, use it.
4826 + var ipex = '0.0.0.0', xforwardedhost = req.headers.host;
4827 + if (typeof req.connection.remoteAddress == 'string') { ipex = (req.connection.remoteAddress.startsWith('::ffff:')) ? req.connection.remoteAddress.substring(7) : req.connection.remoteAddress; }
4828 + if (
4829 + (obj.args.trustedproxy === true) || (obj.args.tlsoffload === true) ||
4830 + ((typeof obj.args.trustedproxy == 'object') && (isIPMatch(ipex, obj.args.trustedproxy))) ||
4831 + ((typeof obj.args.tlsoffload == 'object') && (isIPMatch(ipex, obj.args.tlsoffload)))
4832 + ) {
4833 + // Get client IP
4834 + if (req.headers['cf-connecting-ip']) { // Use CloudFlare IP address if present
4835 + req.clientIp = req.headers['cf-connecting-ip'].split(',')[0].trim();
4836 + } else if (req.headers['x-forwarded-for']) {
4837 + req.clientIp = req.headers['x-forwarded-for'].split(',')[0].trim();
4838 + } else if (req.headers['x-real-ip']) {
4839 + req.clientIp = req.headers['x-real-ip'].split(',')[0].trim();
4840 + } else {
4841 + req.clientIp = ipex;
4842 + }
4843 +
4844 + // If there is a port number, remove it. This will only work for IPv4, but nice for people that have a bad reverse proxy config.
4845 + const clientIpSplit = req.clientIp.split(':');
4846 + if (clientIpSplit.length == 2) { req.clientIp = clientIpSplit[0]; }
4847 +
4848 + // Get server host
4849 + if (req.headers['x-forwarded-host']) { xforwardedhost = req.headers['x-forwarded-host']; }
4850 + } else {
4851 + req.clientIp = ipex;
4852 + }
4853 +
4854 + // Get the domain for this request
4855 + const domain = req.xdomain = getDomain(req);
4856 + parent.debug('webrequest', '(' + req.clientIp + ') ' + req.url);
4857 +
4858 + // Skip the rest is this is an agent connection
4859 + if ((req.url.indexOf('/meshrelay.ashx/.websocket') >= 0) || (req.url.indexOf('/agent.ashx/.websocket') >= 0)) { next(); return; }
4860 +
4861 + // If this domain has configured headers, use them.
4862 + // Example headers: { 'Strict-Transport-Security': 'max-age=360000;includeSubDomains' };
4863 + // { 'Referrer-Policy': 'no-referrer', 'x-frame-options': 'SAMEORIGIN', 'X-XSS-Protection': '1; mode=block', 'X-Content-Type-Options': 'nosniff', 'Content-Security-Policy': "default-src http: ws: data: 'self';script-src http: 'unsafe-inline';style-src http: 'unsafe-inline'" };
4864 + if ((domain != null) && (domain.httpheaders != null) && (typeof domain.httpheaders == 'object')) {
4865 + res.set(domain.httpheaders);
4866 + } else {
4867 + // Use default security headers
4868 + const geourl = (domain.geolocation ? ' *.openstreetmap.org' : '');
4869 + var selfurl = ' wss://' + req.headers.host;
4870 + if ((xforwardedhost != null) && (xforwardedhost != req.headers.host)) { selfurl += ' wss://' + xforwardedhost; }
4871 + const extraScriptSrc = (parent.config.settings.extrascriptsrc != null) ? (' ' + parent.config.settings.extrascriptsrc) : '';
4872 + const headers = {
4873 + 'Referrer-Policy': 'no-referrer',
4874 + 'X-XSS-Protection': '1; mode=block',
4875 + 'X-Content-Type-Options': 'nosniff',
4876 + 'Content-Security-Policy': "default-src 'none'; font-src 'self'; script-src 'self' 'unsafe-inline'" + extraScriptSrc + "; connect-src 'self'" + geourl + selfurl + "; img-src 'self'" + geourl + " data:; style-src 'self' 'unsafe-inline'; frame-src 'self' https://*.youtube.com mcrouter:; media-src 'self'; form-action 'self'"
4877 + };
4878 + if ((parent.config.settings.allowframing !== true) && (typeof parent.config.settings.allowframing !== 'string')) { headers['X-Frame-Options'] = 'sameorigin'; }
4879 + res.set(headers);
4880 + }
4881 +
4882 + // Check the session if bound to the external IP address
4883 + if ((req.session.ip != null) && (req.clientIp != null) && (req.session.ip != req.clientIp)) { req.session = {}; }
4884 +
4885 + // Extend the session time by forcing a change to the session every minute.
4886 + if (req.session.userid != null) { req.session.nowInMinutes = Math.floor(Date.now() / 60e3); } else { delete req.session.nowInMinutes; }
4887 +
4888 + // Continue processing the request
4889 + return next();
4890 + });
4891 +
4892 + if (obj.agentapp) {
4893 + // Add HTTP security headers to all responses
4894 + obj.agentapp.use(function (req, res, next) {
4895 + // Set the real IP address of the request
4896 + // If a trusted reverse-proxy is sending us the remote IP address, use it.
4897 + var ipex = '0.0.0.0';
4898 + if (typeof req.connection.remoteAddress == 'string') { ipex = (req.connection.remoteAddress.startsWith('::ffff:')) ? req.connection.remoteAddress.substring(7) : req.connection.remoteAddress; }
4899 + if (
4900 + (obj.args.trustedproxy === true) ||
4901 + ((typeof obj.args.trustedproxy == 'object') && (obj.args.trustedproxy.indexOf(ipex) >= 0)) ||
4902 + ((typeof obj.args.tlsoffload == 'object') && (obj.args.tlsoffload.indexOf(ipex) >= 0))
4903 + ) {
4904 + if (req.headers['cf-connecting-ip']) { // Use CloudFlare IP address if present
4905 + req.clientIp = req.headers['cf-connecting-ip'].split(',')[0].trim();
4906 + } else if (req.headers['x-forwarded-for']) {
4907 + req.clientIp = req.headers['x-forwarded-for'].split(',')[0].trim();
4908 + } else if (req.headers['x-real-ip']) {
4909 + req.clientIp = req.headers['x-real-ip'].split(',')[0].trim();
4910 + } else {
4911 + req.clientIp = ipex;
4912 + }
4913 + } else {
4914 + req.clientIp = ipex;
4915 + }
4916 +
4917 + // Get the domain for this request
4918 + const domain = req.xdomain = getDomain(req);
4919 + parent.debug('webrequest', '(' + req.clientIp + ') AgentPort: ' + req.url);
4920 + res.removeHeader('X-Powered-By');
4921 + return next();
4922 + });
4923 + }
4924 +
4925 + // Setup all sharing domains
4926 + for (var i in parent.config.domains) {
4927 + if ((parent.config.domains[i].dns == null) && (parent.config.domains[i].share != null)) { obj.app.use(parent.config.domains[i].url, obj.express.static(parent.config.domains[i].share)); }
4928 + }
4929 +
4930 + // Setup all HTTP handlers
4931 + if (parent.multiServer != null) { obj.app.ws('/meshserver.ashx', function (ws, req) { parent.multiServer.CreatePeerInServer(parent.multiServer, ws, req, obj.args.tlsoffload == null); }); }
4932 + for (var i in parent.config.domains) {
4933 + if ((parent.config.domains[i].dns != null) || (parent.config.domains[i].share != null)) { continue; } // This is a subdomain with a DNS name, no added HTTP bindings needed.
4934 + var domain = parent.config.domains[i];
4935 + var url = domain.url;
4936 + if (domain.rootredirect == null) {
4937 + // Present the login page as the root page
4938 + obj.app.get(url, handleRootRequest);
4939 + obj.app.post(url, handleRootPostRequest);
4940 + } else {
4941 + // Root page redirects the user to a different URL
4942 + obj.app.get(url, handleRootRedirect);
4943 + }
4944 + obj.app.get(url + 'refresh.ashx', function (req, res) { res.sendStatus(200); });
4945 + if ((domain.myserver !== false) && ((domain.myserver == null) || (domain.myserver.backup === true))) { obj.app.get(url + 'backup.zip', handleBackupRequest); }
4946 + if ((domain.myserver !== false) && ((domain.myserver == null) || (domain.myserver.restore === true))) { obj.app.post(url + 'restoreserver.ashx', handleRestoreRequest); }
4947 + obj.app.get(url + 'terms', handleTermsRequest);
4948 + obj.app.get(url + 'xterm', handleXTermRequest);
4949 + obj.app.get(url + 'login', handleRootRequest);
4950 + obj.app.post(url + 'login', handleRootPostRequest);
4951 + obj.app.post(url + 'tokenlogin', handleLoginRequest);
4952 + obj.app.get(url + 'logout', handleLogoutRequest);
4953 + obj.app.get(url + 'MeshServerRootCert.cer', handleRootCertRequest);
4954 + obj.app.post(url + 'changepassword', handlePasswordChangeRequest);
4955 + obj.app.post(url + 'deleteaccount', handleDeleteAccountRequest);
4956 + obj.app.post(url + 'createaccount', handleCreateAccountRequest);
4957 + obj.app.post(url + 'resetpassword', handleResetPasswordRequest);
4958 + obj.app.post(url + 'resetaccount', handleResetAccountRequest);
4959 + obj.app.get(url + 'checkmail', handleCheckMailRequest);
4960 + obj.app.get(url + 'agentinvite', handleAgentInviteRequest);
4961 + obj.app.post(url + 'amtevents.ashx', obj.handleAmtEventRequest);
4962 + obj.app.get(url + 'meshagents', obj.handleMeshAgentRequest);
4963 + obj.app.get(url + 'messenger', handleMessengerRequest);
4964 + obj.app.get(url + 'meshosxagent', obj.handleMeshOsxAgentRequest);
4965 + obj.app.get(url + 'meshsettings', obj.handleMeshSettingsRequest);
4966 + obj.app.get(url + 'devicepowerevents.ashx', obj.handleDevicePowerEvents);
4967 + obj.app.get(url + 'downloadfile.ashx', handleDownloadFile);
4968 + obj.app.post(url + 'uploadfile.ashx', handleUploadFile);
4969 + obj.app.post(url + 'uploadfilebatch.ashx', handleUploadFileBatch);
4970 + obj.app.post(url + 'uploadmeshcorefile.ashx', handleUploadMeshCoreFile);
4971 + obj.app.get(url + 'userfiles/*', handleDownloadUserFiles);
4972 + obj.app.ws(url + 'echo.ashx', handleEchoWebSocket);
4973 + obj.app.ws(url + 'apf.ashx', function (ws, req) { obj.parent.mpsserver.onWebSocketConnection(ws, req); })
4974 + obj.app.get(url + 'webrelay.ashx', function (req, res) { res.send('Websocket connection expected'); });
4975 + obj.app.get(url + 'health.ashx', function (req, res) { res.send('ok'); }); // TODO: Perform more server checking.
4976 + obj.app.ws(url + 'webrelay.ashx', function (ws, req) { PerformWSSessionAuth(ws, req, false, handleRelayWebSocket); });
4977 + obj.app.ws(url + 'webider.ashx', function (ws, req) { PerformWSSessionAuth(ws, req, false, function (ws1, req1, domain, user, cookie) { obj.meshIderHandler.CreateAmtIderSession(obj, obj.db, ws1, req1, obj.args, domain, user); }); });
4978 + obj.app.ws(url + 'control.ashx', function (ws, req) {
4979 + const domain = getDomain(req);
4980 + if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { ws.close(); return; } // Check 3FA URL key
4981 + PerformWSSessionAuth(ws, req, false, function (ws1, req1, domain, user, cookie) { obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws1, req1, obj.args, domain, user); });
4982 + });
4983 + obj.app.ws(url + 'devicefile.ashx', function (ws, req) { obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, ws, null, req, domain); });
4984 + obj.app.get(url + 'devicefile.ashx', handleDeviceFile);
4985 + obj.app.get(url + 'agentdownload.ashx', handleAgentDownloadFile);
4986 + obj.app.get(url + 'logo.png', handleLogoRequest);
4987 + obj.app.get(url + 'loginlogo.png', handleLoginLogoRequest);
4988 + obj.app.post(url + 'translations', handleTranslationsRequest);
4989 + obj.app.get(url + 'welcome.jpg', handleWelcomeImageRequest);
4990 + obj.app.get(url + 'welcome.png', handleWelcomeImageRequest);
4991 + obj.app.get(url + 'recordings.ashx', handleGetRecordings);
4992 + obj.app.get(url + 'player.htm', handlePlayerRequest);
4993 + obj.app.get(url + 'player', handlePlayerRequest);
4994 + obj.app.get(url + 'desktop', handleDesktopRequest);
4995 + obj.app.get(url + 'terminal', handleTerminalRequest);
4996 + obj.app.ws(url + 'agenttransfer.ashx', handleAgentFileTransfer); // Setup agent to/from server file transfer handler
4997 + obj.app.ws(url + 'meshrelay.ashx', function (ws, req) {
4998 + PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie) {
4999 + if (((parent.config.settings.desktopmultiplex === true) || (domain.desktopmultiplex === true)) && (req.query.p == 2)) {

This file is too large to show in full.