Web socket connection error, #
Ylian Saint-Hilaire committed
Apr 12, 2022 at 14:03 UTC
4c3a82a552d12e000e2445f4aa97dd2aba77337b
4 files changed
+16430
-12
meshcentral-config-schema.json
+1
-1
@@ -63,7 +63,7 @@
63
"certificatePrivateKeyPassword": { "type": "array", "default": null, "description": "List of passwords used to decrypt PKCK#8 .key files that are in the meshcentral-data folder." },
64
"sessionTime": { "type": "integer", "default": 60, "description": "Duration of a session cookie in minutes. Changing this affects how often the session needs to be automatically refreshed." },
65
"sessionKey": { "type": "string", "default": null, "description": "Password used to encrypt the MeshCentral web session cookies. If null, a random one is generated each time the server starts." },
66
- "cookieSameSite": { "type": "string", "default": "lax", "enum": ["strict", "lax", "none"] },
66
+ "sessionSameSite": { "type": "string", "default": "lax", "enum": ["strict", "lax", "none"] },
67
"dbEncryptKey": { "type": "string" },
68
"dbRecordsEncryptKey": { "type": "string", "default": null },
69
"dbRecordsDecryptKey": { "type": "string", "default": null },
webserver-broken.js
new
+8212
@@ -0,0 +1,8212 @@
1
+/**
2
+* @description MeshCentral web server
3
+* @author Ylian Saint-Hilaire
4
+* @copyright Intel Corporation 2018-2022
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, doneFunc) {
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
+ obj.uaparser = require('./ua-parser');
56
+ const constants = (obj.crypto.constants ? obj.crypto.constants : require('constants')); // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead.
57
+
58
+ // Setup WebAuthn / FIDO2
59
+ obj.webauthn = require('./webauthn.js').CreateWebAuthnModule();
60
+
61
+ // Variables
62
+ obj.args = args;
63
+ obj.parent = parent;
64
+ obj.filespath = parent.filespath;
65
+ obj.db = db;
66
+ obj.app = obj.express();
67
+ if (obj.args.agentport) { obj.agentapp = obj.express(); }
68
+ if (args.compression !== false) { obj.app.use(require('compression')()); }
69
+ obj.app.disable('x-powered-by');
70
+ obj.tlsServer = null;
71
+ obj.tcpServer = null;
72
+ obj.certificates = certificates;
73
+ obj.users = {}; // UserID --> User
74
+ obj.meshes = {}; // MeshID --> Mesh (also called device group)
75
+ obj.userGroups = {}; // UGrpID --> User Group
76
+ obj.userAllowedIp = args.userallowedip; // List of allowed IP addresses for users
77
+ obj.agentAllowedIp = args.agentallowedip; // List of allowed IP addresses for agents
78
+ obj.agentBlockedIp = args.agentblockedip; // List of blocked IP addresses for agents
79
+ obj.tlsSniCredentials = null;
80
+ obj.dnsDomains = {};
81
+ obj.relaySessionCount = 0;
82
+ obj.relaySessionErrorCount = 0;
83
+ obj.blockedUsers = 0;
84
+ obj.blockedAgents = 0;
85
+ obj.renderPages = null;
86
+ obj.renderLanguages = [];
87
+ obj.destroyedSessions = {};
88
+
89
+ // Mesh Rights
90
+ const MESHRIGHT_EDITMESH = 0x00000001;
91
+ const MESHRIGHT_MANAGEUSERS = 0x00000002;
92
+ const MESHRIGHT_MANAGECOMPUTERS = 0x00000004;
93
+ const MESHRIGHT_REMOTECONTROL = 0x00000008;
94
+ const MESHRIGHT_AGENTCONSOLE = 0x00000010;
95
+ const MESHRIGHT_SERVERFILES = 0x00000020;
96
+ const MESHRIGHT_WAKEDEVICE = 0x00000040;
97
+ const MESHRIGHT_SETNOTES = 0x00000080;
98
+ const MESHRIGHT_REMOTEVIEWONLY = 0x00000100;
99
+ const MESHRIGHT_NOTERMINAL = 0x00000200;
100
+ const MESHRIGHT_NOFILES = 0x00000400;
101
+ const MESHRIGHT_NOAMT = 0x00000800;
102
+ const MESHRIGHT_DESKLIMITEDINPUT = 0x00001000;
103
+ const MESHRIGHT_LIMITEVENTS = 0x00002000;
104
+ const MESHRIGHT_CHATNOTIFY = 0x00004000;
105
+ const MESHRIGHT_UNINSTALL = 0x00008000;
106
+ const MESHRIGHT_NODESKTOP = 0x00010000;
107
+ const MESHRIGHT_REMOTECOMMAND = 0x00020000;
108
+ const MESHRIGHT_RESETOFF = 0x00040000;
109
+ const MESHRIGHT_GUESTSHARING = 0x00080000;
110
+ const MESHRIGHT_ADMIN = 0xFFFFFFFF;
111
+
112
+ // Site rights
113
+ const SITERIGHT_SERVERBACKUP = 0x00000001;
114
+ const SITERIGHT_MANAGEUSERS = 0x00000002;
115
+ const SITERIGHT_SERVERRESTORE = 0x00000004;
116
+ const SITERIGHT_FILEACCESS = 0x00000008;
117
+ const SITERIGHT_SERVERUPDATE = 0x00000010;
118
+ const SITERIGHT_LOCKED = 0x00000020;
119
+ const SITERIGHT_NONEWGROUPS = 0x00000040;
120
+ const SITERIGHT_NOMESHCMD = 0x00000080;
121
+ const SITERIGHT_USERGROUPS = 0x00000100;
122
+ const SITERIGHT_RECORDINGS = 0x00000200;
123
+ const SITERIGHT_LOCKSETTINGS = 0x00000400;
124
+ const SITERIGHT_ALLEVENTS = 0x00000800;
125
+ const SITERIGHT_NONEWDEVICES = 0x00001000;
126
+ const SITERIGHT_ADMIN = 0xFFFFFFFF;
127
+
128
+ // Setup SSPI authentication if needed
129
+ if ((obj.parent.platform == 'win32') && (obj.args.nousers != true) && (obj.parent.config != null) && (obj.parent.config.domains != null)) {
130
+ 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: false, offerBasic: false }); } }
131
+ }
132
+
133
+ // Perform hash on web certificate and agent certificate
134
+ obj.webCertificateHash = parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.web.cert);
135
+ obj.webCertificateHashs = { '': obj.webCertificateHash };
136
+ obj.webCertificateHashBase64 = Buffer.from(obj.webCertificateHash, 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
137
+ obj.webCertificateFullHash = parent.certificateOperations.getCertHashBinary(obj.certificates.web.cert);
138
+ obj.webCertificateFullHashs = { '': obj.webCertificateFullHash };
139
+ obj.webCertificateExpire = { '': Date.parse(parent.certificateOperations.forge.pki.certificateFromPem(parent.certificates.web.cert).validity.notAfter) };
140
+ obj.agentCertificateHashHex = parent.certificateOperations.getPublicKeyHash(obj.certificates.agent.cert);
141
+ obj.agentCertificateHashBase64 = Buffer.from(obj.agentCertificateHashHex, 'hex').toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
142
+ obj.agentCertificateAsn1 = parent.certificateOperations.forge.asn1.toDer(parent.certificateOperations.forge.pki.certificateToAsn1(parent.certificateOperations.forge.pki.certificateFromPem(parent.certificates.agent.cert))).getBytes();
143
+ obj.defaultWebCertificateHash = parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.webdefault.cert);
144
+ obj.defaultWebCertificateFullHash = parent.certificateOperations.getCertHashBinary(obj.certificates.webdefault.cert);
145
+
146
+ // Compute the hash of all of the web certificates for each domain
147
+ for (var i in obj.parent.config.domains) {
148
+ if (obj.parent.config.domains[i].certhash != null) {
149
+ // If the web certificate hash is provided, use it.
150
+ obj.webCertificateHashs[i] = obj.webCertificateFullHashs[i] = Buffer.from(obj.parent.config.domains[i].certhash, 'hex').toString('binary');
151
+ if (obj.parent.config.domains[i].certkeyhash != null) { obj.webCertificateHashs[i] = Buffer.from(obj.parent.config.domains[i].certkeyhash, 'hex').toString('binary'); }
152
+ delete obj.webCertificateExpire[i]; // Expire time is not provided
153
+ } else if ((obj.parent.config.domains[i].dns != null) && (obj.parent.config.domains[i].certs != null)) {
154
+ // If the domain has a different DNS name, use a different certificate hash.
155
+ // Hash the full certificate
156
+ obj.webCertificateFullHashs[i] = parent.certificateOperations.getCertHashBinary(obj.parent.config.domains[i].certs.cert);
157
+ obj.webCertificateExpire[i] = Date.parse(parent.certificateOperations.forge.pki.certificateFromPem(obj.parent.config.domains[i].certs.cert).validity.notAfter);
158
+ try {
159
+ // Decode a RSA certificate and hash the public key.
160
+ obj.webCertificateHashs[i] = parent.certificateOperations.getPublicKeyHashBinary(obj.parent.config.domains[i].certs.cert);
161
+ } catch (ex) {
162
+ // This may be a ECDSA certificate, hash the entire cert.
163
+ obj.webCertificateHashs[i] = obj.webCertificateFullHashs[i];
164
+ }
165
+ } else if ((obj.parent.config.domains[i].dns != null) && (obj.certificates.dns[i] != null)) {
166
+ // If this domain has a DNS and a matching DNS cert, use it. This case works for wildcard certs.
167
+ obj.webCertificateFullHashs[i] = parent.certificateOperations.getCertHashBinary(obj.certificates.dns[i].cert);
168
+ obj.webCertificateHashs[i] = parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.dns[i].cert);
169
+ obj.webCertificateExpire[i] = Date.parse(parent.certificateOperations.forge.pki.certificateFromPem(obj.certificates.dns[i].cert).validity.notAfter);
170
+ } else if (i != '') {
171
+ // For any other domain, use the default cert.
172
+ obj.webCertificateFullHashs[i] = obj.webCertificateFullHashs[''];
173
+ obj.webCertificateHashs[i] = obj.webCertificateHashs[''];
174
+ obj.webCertificateExpire[i] = obj.webCertificateExpire[''];
175
+ }
176
+ }
177
+
178
+ // If we are running the legacy swarm server, compute the hash for that certificate
179
+ if (parent.certificates.swarmserver != null) {
180
+ obj.swarmCertificateAsn1 = parent.certificateOperations.forge.asn1.toDer(parent.certificateOperations.forge.pki.certificateToAsn1(parent.certificateOperations.forge.pki.certificateFromPem(parent.certificates.swarmserver.cert))).getBytes();
181
+ 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' });
182
+ 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' });
183
+ }
184
+
185
+ // Main lists
186
+ obj.wsagents = {}; // NodeId --> Agent
187
+ obj.wsagentsWithBadWebCerts = {}; // NodeId --> Agent
188
+ obj.wsagentsDisconnections = {};
189
+ obj.wsagentsDisconnectionsTimer = null;
190
+ obj.duplicateAgentsLog = {};
191
+ obj.wssessions = {}; // UserId --> Array Of Sessions
192
+ obj.wssessions2 = {}; // "UserId + SessionRnd" --> Session (Note that the SessionId is the UserId + / + SessionRnd)
193
+ obj.wsPeerSessions = {}; // ServerId --> Array Of "UserId + SessionRnd"
194
+ obj.wsPeerSessions2 = {}; // "UserId + SessionRnd" --> ServerId
195
+ obj.wsPeerSessions3 = {}; // ServerId --> UserId --> [ SessionId ]
196
+ obj.sessionsCount = {}; // Merged session counters, used when doing server peering. UserId --> SessionCount
197
+ obj.wsrelays = {}; // Id -> Relay
198
+ obj.desktoprelays = {}; // Id -> Desktop Multiplexor Relay
199
+ obj.wsPeerRelays = {}; // Id -> { ServerId, Time }
200
+ var tlsSessionStore = {}; // Store TLS session information for quick resume.
201
+ var tlsSessionStoreCount = 0; // Number of cached TLS session information in store.
202
+
203
+ // Setup randoms
204
+ obj.crypto.randomBytes(48, function (err, buf) { obj.httpAuthRandom = buf; });
205
+ obj.crypto.randomBytes(16, function (err, buf) { obj.httpAuthRealm = buf.toString('hex'); });
206
+ obj.crypto.randomBytes(48, function (err, buf) { obj.relayRandom = buf; });
207
+
208
+ // Get non-english web pages and emails
209
+ getRenderList();
210
+ getEmailLanguageList();
211
+
212
+ // Setup DNS domain TLS SNI credentials
213
+ {
214
+ var dnscount = 0;
215
+ obj.tlsSniCredentials = {};
216
+ 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++; } }
217
+ 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; }
218
+ }
219
+ function TlsSniCallback(name, cb) {
220
+ var c = obj.tlsSniCredentials[name];
221
+ if (c != null) {
222
+ cb(null, c);
223
+ } else {
224
+ cb(null, obj.tlsSniCredentials['']);
225
+ }
226
+ }
227
+
228
+ function EscapeHtml(x) { if (typeof x == 'string') return x.replace(/&/g, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/'/g, '''); if (typeof x == 'boolean') return x; if (typeof x == 'number') return x; }
229
+ //function EscapeHtmlBreaks(x) { if (typeof x == "string") return x.replace(/&/g, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/'/g, ''').replace(/\r/g, '<br />').replace(/\n/g, '').replace(/\t/g, ' '); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
230
+ // Fetch all users from the database, keep this in memory
231
+ obj.db.GetAllType('user', function (err, docs) {
232
+ obj.common.unEscapeAllLinksFieldName(docs);
233
+ var domainUserCount = {}, i = 0;
234
+ for (i in parent.config.domains) { domainUserCount[i] = 0; }
235
+ for (i in docs) { var u = obj.users[docs[i]._id] = docs[i]; domainUserCount[u.domain]++; }
236
+ for (i in parent.config.domains) {
237
+ if ((parent.config.domains[i].share == null) && (domainUserCount[i] == 0)) {
238
+ // If newaccounts is set to no new accounts, but no accounts exists, temporarly allow account creation.
239
+ //if ((parent.config.domains[i].newaccounts === 0) || (parent.config.domains[i].newaccounts === false)) { parent.config.domains[i].newaccounts = 2; }
240
+ console.log('Server ' + ((i == '') ? '' : (i + ' ')) + 'has no users, next new account will be site administrator.');
241
+ }
242
+ }
243
+
244
+ // Fetch all device groups (meshes) from the database, keep this in memory
245
+ // As we load things in memory, we will also be doing some cleaning up.
246
+ // We will not save any clean up in the database right now, instead it will be saved next time there is a change.
247
+ obj.db.GetAllType('mesh', function (err, docs) {
248
+ obj.common.unEscapeAllLinksFieldName(docs);
249
+ for (var i in docs) { obj.meshes[docs[i]._id] = docs[i]; } // Get all meshes, including deleted ones.
250
+
251
+ // Fetch all user groups from the database, keep this in memory
252
+ obj.db.GetAllType('ugrp', function (err, docs) {
253
+ obj.common.unEscapeAllLinksFieldName(docs);
254
+
255
+ // Perform user group link cleanup
256
+ for (var i in docs) {
257
+ const ugrp = docs[i];
258
+ if (ugrp.links != null) {
259
+ for (var j in ugrp.links) {
260
+ if (j.startsWith('user/') && (obj.users[j] == null)) { delete ugrp.links[j]; } // User group has a link to a user that does not exist
261
+ 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
262
+ }
263
+ }
264
+ obj.userGroups[docs[i]._id] = docs[i]; // Get all user groups
265
+ }
266
+
267
+ // Perform device group link cleanup
268
+ for (var i in obj.meshes) {
269
+ const mesh = obj.meshes[i];
270
+ if (mesh.links != null) {
271
+ for (var j in mesh.links) {
272
+ 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
273
+ 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
274
+ }
275
+ }
276
+ }
277
+
278
+ // Perform user link cleanup
279
+ for (var i in obj.users) {
280
+ const user = obj.users[i];
281
+ if (user.links != null) {
282
+ for (var j in user.links) {
283
+ if (j.startsWith('ugrp/') && (obj.userGroups[j] == null)) { delete user.links[j]; } // User has a link to a user group that does not exist
284
+ 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
285
+ //else if (j.startsWith('node/') && (obj.nodes[j] == null)) { delete user.links[j]; } // TODO
286
+ }
287
+ //if (Object.keys(user.links).length == 0) { delete user.links; }
288
+ }
289
+ }
290
+
291
+ // We loaded the users, device groups and user group state, start the server
292
+ serverStart();
293
+ });
294
+ });
295
+ });
296
+
297
+ // Clean up a device, used before saving it in the database
298
+ obj.cleanDevice = function (device) {
299
+ // Check device links, if a link points to an unknown user, remove it.
300
+ if (device.links != null) {
301
+ for (var j in device.links) {
302
+ if ((obj.users[j] == null) && (obj.userGroups[j] == null)) {
303
+ delete device.links[j];
304
+ if (Object.keys(device.links).length == 0) { delete device.links; }
305
+ }
306
+ }
307
+ }
308
+ return device;
309
+ }
310
+
311
+ // Return statistics about this web server
312
+ obj.getStats = function () {
313
+ return {
314
+ users: Object.keys(obj.users).length,
315
+ meshes: Object.keys(obj.meshes).length,
316
+ dnsDomains: Object.keys(obj.dnsDomains).length,
317
+ relaySessionCount: obj.relaySessionCount,
318
+ relaySessionErrorCount: obj.relaySessionErrorCount,
319
+ wsagents: Object.keys(obj.wsagents).length,
320
+ wsagentsDisconnections: Object.keys(obj.wsagentsDisconnections).length,
321
+ wsagentsDisconnectionsTimer: Object.keys(obj.wsagentsDisconnectionsTimer).length,
322
+ wssessions: Object.keys(obj.wssessions).length,
323
+ wssessions2: Object.keys(obj.wssessions2).length,
324
+ wsPeerSessions: Object.keys(obj.wsPeerSessions).length,
325
+ wsPeerSessions2: Object.keys(obj.wsPeerSessions2).length,
326
+ wsPeerSessions3: Object.keys(obj.wsPeerSessions3).length,
327
+ sessionsCount: Object.keys(obj.sessionsCount).length,
328
+ wsrelays: Object.keys(obj.wsrelays).length,
329
+ wsPeerRelays: Object.keys(obj.wsPeerRelays).length,
330
+ tlsSessionStore: Object.keys(tlsSessionStore).length,
331
+ blockedUsers: obj.blockedUsers,
332
+ blockedAgents: obj.blockedAgents
333
+ };
334
+ }
335
+
336
+ // Agent counters
337
+ obj.agentStats = {
338
+ createMeshAgentCount: 0,
339
+ agentClose: 0,
340
+ agentBinaryUpdate: 0,
341
+ agentMeshCoreBinaryUpdate: 0,
342
+ coreIsStableCount: 0,
343
+ verifiedAgentConnectionCount: 0,
344
+ clearingCoreCount: 0,
345
+ updatingCoreCount: 0,
346
+ recoveryCoreIsStableCount: 0,
347
+ meshDoesNotExistCount: 0,
348
+ invalidPkcsSignatureCount: 0,
349
+ invalidRsaSignatureCount: 0,
350
+ invalidJsonCount: 0,
351
+ unknownAgentActionCount: 0,
352
+ agentBadWebCertHashCount: 0,
353
+ agentBadSignature1Count: 0,
354
+ agentBadSignature2Count: 0,
355
+ agentMaxSessionHoldCount: 0,
356
+ invalidDomainMeshCount: 0,
357
+ invalidMeshTypeCount: 0,
358
+ invalidDomainMesh2Count: 0,
359
+ invalidMeshType2Count: 0,
360
+ duplicateAgentCount: 0,
361
+ maxDomainDevicesReached: 0,
362
+ agentInTrouble: 0,
363
+ agentInBigTrouble: 0
364
+ }
365
+ obj.getAgentStats = function () { return obj.agentStats; }
366
+
367
+ // Traffic counters
368
+ obj.trafficStats = {
369
+ httpRequestCount: 0,
370
+ httpWebSocketCount: 0,
371
+ httpIn: 0,
372
+ httpOut: 0,
373
+ relayCount: {},
374
+ relayIn: {},
375
+ relayOut: {},
376
+ localRelayCount: {},
377
+ localRelayIn: {},
378
+ localRelayOut: {},
379
+ AgentCtrlIn: 0,
380
+ AgentCtrlOut: 0,
381
+ LMSIn: 0,
382
+ LMSOut: 0,
383
+ CIRAIn: 0,
384
+ CIRAOut: 0
385
+ }
386
+ obj.trafficStats.time = Date.now();
387
+ obj.getTrafficStats = function () { return obj.trafficStats; }
388
+ obj.getTrafficDelta = function (oldTraffic) { // Return the difference between the old and new data along with the delta time.
389
+ const data = obj.common.Clone(obj.trafficStats);
390
+ data.time = Date.now();
391
+ const delta = calcDelta(oldTraffic ? oldTraffic : {}, data);
392
+ if (oldTraffic && oldTraffic.time) { delta.delta = (data.time - oldTraffic.time); }
393
+ delta.time = data.time;
394
+ return { current: data, delta: delta }
395
+ }
396
+ function calcDelta(oldData, newData) { // Recursive function that computes the difference of all numbers
397
+ const r = {};
398
+ for (var i in newData) {
399
+ if (typeof newData[i] == 'object') { r[i] = calcDelta(oldData[i] ? oldData[i] : {}, newData[i]); }
400
+ if (typeof newData[i] == 'number') { if (typeof oldData[i] == 'number') { r[i] = (newData[i] - oldData[i]); } else { r[i] = newData[i]; } }
401
+ }
402
+ return r;
403
+ }
404
+
405
+ // Keep a record of the last agent issues.
406
+ obj.getAgentIssues = function () { return obj.agentIssues; }
407
+ obj.setAgentIssue = function (agent, issue) { obj.agentIssues.push([new Date().toLocaleString(), agent.remoteaddrport, issue]); while (obj.setAgentIssue.length > 50) { obj.agentIssues.shift(); } }
408
+ obj.agentIssues = [];
409
+
410
+ // Authenticate the user
411
+ obj.authenticate = function (name, pass, domain, fn) {
412
+ if ((typeof (name) != 'string') || (typeof (pass) != 'string') || (typeof (domain) != 'object')) { fn(new Error('invalid fields')); return; }
413
+ if (name.startsWith('~t:')) {
414
+ // Login token, try to fetch the token from the database
415
+ obj.db.Get('logintoken-' + name, function (err, docs) {
416
+ if (err != null) { fn(err); return; }
417
+ if ((docs == null) || (docs.length != 1)) { fn(new Error('login token not found')); return; }
418
+ const loginToken = docs[0];
419
+ if ((loginToken.expire != 0) && (loginToken.expire < Date.now())) { fn(new Error('login token expired')); return; }
420
+
421
+ // Default strong password hashing (pbkdf2 SHA384)
422
+ require('./pass').hash(pass, loginToken.salt, function (err, hash, tag) {
423
+ if (err) return fn(err);
424
+ if (hash == loginToken.hash) {
425
+ // Login username and password are valid.
426
+ var user = obj.users[loginToken.userid];
427
+ if (!user) { fn(new Error('cannot find user')); return; }
428
+ if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
429
+
430
+ // Succesful login token authentication
431
+ var loginOptions = { tokenName: loginToken.name, tokenUser: loginToken.tokenUser };
432
+ if (loginToken.expire != 0) { loginOptions.expire = loginToken.expire; }
433
+ return fn(null, user._id, null, loginOptions);
434
+ }
435
+ fn(new Error('invalid password'));
436
+ }, 0);
437
+ });
438
+ } else if (domain.auth == 'ldap') {
439
+ if (domain.ldapoptions.url == 'test') {
440
+ // Fake LDAP login
441
+ var xxuser = domain.ldapoptions[name.toLowerCase()];
442
+ if (xxuser == null) {
443
+ fn(new Error('invalid password'));
444
+ return;
445
+ } else {
446
+ var username = xxuser['displayName'];
447
+ if (domain.ldapusername) { username = xxuser[domain.ldapusername]; }
448
+ var shortname = null;
449
+ if (domain.ldapuserbinarykey) {
450
+ // Use a binary key as the userid
451
+ if (xxuser[domain.ldapuserbinarykey]) { shortname = Buffer.from(xxuser[domain.ldapuserbinarykey], 'binary').toString('hex'); }
452
+ } else if (domain.ldapuserkey) {
453
+ // Use a string key as the userid
454
+ if (xxuser[domain.ldapuserkey]) { shortname = xxuser[domain.ldapuserkey]; }
455
+ } else {
456
+ // Use the default key as the userid
457
+ if (xxuser.objectSid) { shortname = Buffer.from(xxuser.objectSid, 'binary').toString('hex').toLowerCase(); }
458
+ else if (xxuser.objectGUID) { shortname = Buffer.from(xxuser.objectGUID, 'binary').toString('hex').toLowerCase(); }
459
+ else if (xxuser.name) { shortname = xxuser.name; }
460
+ else if (xxuser.cn) { shortname = xxuser.cn; }
461
+ }
462
+ if (shortname == null) { fn(new Error('no user identifier')); return; }
463
+ if (username == null) { username = shortname; }
464
+ var userid = 'user/' + domain.id + '/' + shortname;
465
+ var user = obj.users[userid];
466
+ var email = null;
467
+ if (domain.ldapuseremail) {
468
+ email = xxuser[domain.ldapuseremail];
469
+ } else if (xxuser.mail) { // use default
470
+ email = xxuser.mail;
471
+ }
472
+ if ('[object Array]' == Object.prototype.toString.call(email)) {
473
+ // mail may be multivalued in ldap in which case, answer is an array. Use the 1st value.
474
+ email = email[0];
475
+ }
476
+ if (email) { email = email.toLowerCase(); } // it seems some code otherwhere also lowercase the emailaddress. be compatible.
477
+
478
+ if (user == null) {
479
+ // Create a new user
480
+ var user = { type: 'user', _id: userid, name: username, creation: Math.floor(Date.now() / 1000), login: Math.floor(Date.now() / 1000), access: Math.floor(Date.now() / 1000), domain: domain.id };
481
+ if (email) { user['email'] = email; user['emailVerified'] = true; }
482
+ if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; }
483
+ if (obj.common.validateStrArray(domain.newaccountrealms)) { user.groups = domain.newaccountrealms; }
484
+ var usercount = 0;
485
+ for (var i in obj.users) { if (obj.users[i].domain == domain.id) { usercount++; } }
486
+ if (usercount == 0) { user.siteadmin = 4294967295; /*if (domain.newaccounts === 2) { delete domain.newaccounts; }*/ } // If this is the first user, give the account site admin.
487
+
488
+ // Auto-join any user groups
489
+ if (typeof domain.newaccountsusergroups == 'object') {
490
+ for (var i in domain.newaccountsusergroups) {
491
+ var ugrpid = domain.newaccountsusergroups[i];
492
+ if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
493
+ var ugroup = obj.userGroups[ugrpid];
494
+ if (ugroup != null) {
495
+ // Add group to the user
496
+ if (user.links == null) { user.links = {}; }
497
+ user.links[ugroup._id] = { rights: 1 };
498
+
499
+ // Add user to the group
500
+ ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
501
+ db.Set(ugroup);
502
+
503
+ // Notify user group change
504
+ var event = { etype: 'ugrp', ugrpid: ugroup._id, name: ugroup.name, desc: ugroup.desc, action: 'usergroupchange', links: ugroup.links, msgid: 71, msgArgs: [user.name, ugroup.name], msg: 'Added user ' + user.name + ' to user group ' + ugroup.name, addUserDomain: domain.id };
505
+ 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.
506
+ parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
507
+ }
508
+ }
509
+ }
510
+
511
+ obj.users[user._id] = user;
512
+ obj.db.SetUser(user);
513
+ var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountcreate', msgid: 128, msgArgs: [user.name], msg: 'Account created, name is ' + user.name, domain: domain.id };
514
+ 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.
515
+ obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
516
+ return fn(null, user._id);
517
+ } else {
518
+ // This is an existing user
519
+ // If the display username has changes, update it.
520
+ if (user.name != username) {
521
+ user.name = username;
522
+ obj.db.SetUser(user);
523
+ var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msgid: 127, msgArgs: [user.name], msg: 'Changed account display name to ' + user.name, domain: domain.id };
524
+ 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.
525
+ parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
526
+ }
527
+ // Check if user email has changed
528
+ var emailreason = null;
529
+ if (user.email && !email) { // email unset in ldap => unset
530
+ delete user.email;
531
+ delete user.emailVerified;
532
+ emailreason = 'Unset email (no more email in LDAP)'
533
+ } else if (user.email != email) { // update email
534
+ user['email'] = email;
535
+ user['emailVerified'] = true;
536
+ emailreason = 'Set account email to ' + email + '. Sync with LDAP.';
537
+ }
538
+ if (emailreason) {
539
+ obj.db.SetUser(user);
540
+ var event = { etype: 'user', userid: userid, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: emailreason, domain: domain.id };
541
+ 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.
542
+ parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
543
+ }
544
+ // If user is locker out, block here.
545
+ if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
546
+ return fn(null, user._id);
547
+ }
548
+ }
549
+ } else {
550
+ // LDAP login
551
+ var LdapAuth = require('ldapauth-fork');
552
+ var ldap = new LdapAuth(domain.ldapoptions);
553
+ ldap.on('error', function (err) { console.log('ldap error: ', err); });
554
+ ldap.authenticate(name, pass, function (err, xxuser) {
555
+ try { ldap.close(); } catch (ex) { console.log(ex); } // Close the LDAP object
556
+ if (err) { fn(new Error('invalid password')); return; }
557
+ var shortname = null;
558
+ var email = null;
559
+ if (domain.ldapuseremail) {
560
+ email = xxuser[domain.ldapuseremail];
561
+ } else if (xxuser.mail) {
562
+ email = xxuser.mail;
563
+ }
564
+ if ('[object Array]' == Object.prototype.toString.call(email)) {
565
+ // mail may be multivalued in ldap in which case, answer would be an array. Use the 1st one.
566
+ email = email[0];
567
+ }
568
+ if (email) { email = email.toLowerCase(); } // it seems some code otherwhere also lowercase the emailaddress. be compatible.
569
+ var username = xxuser['displayName'];
570
+ if (domain.ldapusername) { username = xxuser[domain.ldapusername]; }
571
+ if (domain.ldapuserbinarykey) {
572
+ // Use a binary key as the userid
573
+ if (xxuser[domain.ldapuserbinarykey]) { shortname = Buffer.from(xxuser[domain.ldapuserbinarykey], 'binary').toString('hex').toLowerCase(); }
574
+ } else if (domain.ldapuserkey) {
575
+ // Use a string key as the userid
576
+ if (xxuser[domain.ldapuserkey]) { shortname = xxuser[domain.ldapuserkey]; }
577
+ } else {
578
+ // Use the default key as the userid
579
+ if (xxuser.objectSid) { shortname = Buffer.from(xxuser.objectSid, 'binary').toString('hex').toLowerCase(); }
580
+ else if (xxuser.objectGUID) { shortname = Buffer.from(xxuser.objectGUID, 'binary').toString('hex').toLowerCase(); }
581
+ else if (xxuser.name) { shortname = xxuser.name; }
582
+ else if (xxuser.cn) { shortname = xxuser.cn; }
583
+ }
584
+ if (shortname == null) { fn(new Error('no user identifier')); return; }
585
+ if (username == null) { username = shortname; }
586
+ var userid = 'user/' + domain.id + '/' + shortname;
587
+ var user = obj.users[userid];
588
+
589
+ if (user == null) {
590
+ // This user does not exist, create a new account.
591
+ var user = { type: 'user', _id: userid, name: username, creation: Math.floor(Date.now() / 1000), login: Math.floor(Date.now() / 1000), access: Math.floor(Date.now() / 1000), domain: domain.id };
592
+ if (email) { user['email'] = email; user['emailVerified'] = true; }
593
+ if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; }
594
+ if (obj.common.validateStrArray(domain.newaccountrealms)) { user.groups = domain.newaccountrealms; }
595
+ var usercount = 0;
596
+ for (var i in obj.users) { if (obj.users[i].domain == domain.id) { usercount++; } }
597
+ if (usercount == 0) { user.siteadmin = 4294967295; /*if (domain.newaccounts === 2) { delete domain.newaccounts; }*/ } // If this is the first user, give the account site admin.
598
+
599
+ // Auto-join any user groups
600
+ if (typeof domain.newaccountsusergroups == 'object') {
601
+ for (var i in domain.newaccountsusergroups) {
602
+ var ugrpid = domain.newaccountsusergroups[i];
603
+ if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
604
+ var ugroup = obj.userGroups[ugrpid];
605
+ if (ugroup != null) {
606
+ // Add group to the user
607
+ if (user.links == null) { user.links = {}; }
608
+ user.links[ugroup._id] = { rights: 1 };
609
+
610
+ // Add user to the group
611
+ ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
612
+ db.Set(ugroup);
613
+
614
+ // Notify user group change
615
+ var event = { etype: 'ugrp', ugrpid: ugroup._id, name: ugroup.name, desc: ugroup.desc, action: 'usergroupchange', links: ugroup.links, msgid: 71, msgArgs: [user.name, ugroup.name], msg: 'Added user ' + user.name + ' to user group ' + ugroup.name, addUserDomain: domain.id };
616
+ 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.
617
+ parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
618
+ }
619
+ }
620
+ }
621
+
622
+ obj.users[user._id] = user;
623
+ obj.db.SetUser(user);
624
+ var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountcreate', msgid: 128, msgArgs: [user.name], msg: 'Account created, name is ' + user.name, domain: domain.id };
625
+ 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.
626
+ obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
627
+ return fn(null, user._id);
628
+ } else {
629
+ // This is an existing user
630
+ // If the display username has changes, update it.
631
+ if (user.name != username) {
632
+ user.name = username;
633
+ obj.db.SetUser(user);
634
+ var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msgid: 127, msgArgs: [user.name], msg: 'Changed account display name to ' + user.name, domain: domain.id };
635
+ 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.
636
+ parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
637
+ }
638
+ // Check if user email has changed
639
+ var emailreason = null;
640
+ if (user.email && !email) { // email unset in ldap => unset
641
+ delete user.email;
642
+ delete user.emailVerified;
643
+ emailreason = 'Unset email (no more email in LDAP)'
644
+ } else if (user.email != email) { // update email
645
+ user['email'] = email;
646
+ user['emailVerified'] = true;
647
+ emailreason = 'Set account email to ' + email + '. Sync with LDAP.';
648
+ }
649
+ if (emailreason) {
650
+ obj.db.SetUser(user);
651
+ var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: emailreason, domain: domain.id };
652
+ 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.
653
+ parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
654
+ }
655
+ // If user is locker out, block here.
656
+ if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
657
+ return fn(null, user._id);
658
+ }
659
+ });
660
+ }
661
+ } else {
662
+ // Regular login
663
+ var user = obj.users['user/' + domain.id + '/' + name.toLowerCase()];
664
+ // Query the db for the given username
665
+ if (!user) { fn(new Error('cannot find user')); return; }
666
+ // Apply the same algorithm to the POSTed password, applying the hash against the pass / salt, if there is a match we found the user
667
+ if (user.salt == null) {
668
+ fn(new Error('invalid password'));
669
+ } else {
670
+ if (user.passtype != null) {
671
+ // IIS default clear or weak password hashing (SHA-1)
672
+ require('./pass').iishash(user.passtype, pass, user.salt, function (err, hash) {
673
+ if (err) return fn(err);
674
+ if (hash == user.hash) {
675
+ // Update the password to the stronger format.
676
+ 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);
677
+ if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
678
+ return fn(null, user._id);
679
+ }
680
+ fn(new Error('invalid password'), null, user.passhint);
681
+ });
682
+ } else {
683
+ // Default strong password hashing (pbkdf2 SHA384)
684
+ require('./pass').hash(pass, user.salt, function (err, hash, tag) {
685
+ if (err) return fn(err);
686
+ if (hash == user.hash) {
687
+ if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
688
+ return fn(null, user._id);
689
+ }
690
+ fn(new Error('invalid password'), null, user.passhint);
691
+ }, 0);
692
+ }
693
+ }
694
+ }
695
+ };
696
+
697
+ /*
698
+ obj.restrict = function (req, res, next) {
699
+ console.log('restrict', req.url);
700
+ var domain = getDomain(req);
701
+ if (req.session.userid) {
702
+ next();
703
+ } else {
704
+ req.session.messageid = 111; // Access denied.
705
+ res.redirect(domain.url + 'login');
706
+ }
707
+ };
708
+ */
709
+
710
+ // Check if the source IP address is in the IP list, return false if not.
711
+ function checkIpAddressEx(req, res, ipList, closeIfThis, redirectUrl) {
712
+ try {
713
+ if (req.connection) {
714
+ // HTTP(S) request
715
+ if (req.clientIp) { for (var i = 0; i < ipList.length; i++) { if (require('ipcheck').match(req.clientIp, ipList[i])) { if (closeIfThis === true) { if (typeof redirectUrl == 'string') { res.redirect(redirectUrl); } else { res.sendStatus(401); } } return true; } } }
716
+ if (closeIfThis === false) { if (typeof redirectUrl == 'string') { res.redirect(redirectUrl); } else { res.sendStatus(401); } }
717
+ } else {
718
+ // WebSocket request
719
+ 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; } } }
720
+ if (closeIfThis === false) { try { req.close(); } catch (e) { } }
721
+ }
722
+ } catch (e) { console.log(e); } // Should never happen
723
+ return false;
724
+ }
725
+
726
+ // Check if the source IP address is allowed, return domain if allowed
727
+ // If there is a fail and null is returned, the request or connection is closed already.
728
+ function checkUserIpAddress(req, res) {
729
+ if ((parent.config.settings.userblockedip != null) && (checkIpAddressEx(req, res, parent.config.settings.userblockedip, true, parent.config.settings.ipblockeduserredirect) == true)) { obj.blockedUsers++; return null; }
730
+ if ((parent.config.settings.userallowedip != null) && (checkIpAddressEx(req, res, parent.config.settings.userallowedip, false, parent.config.settings.ipblockeduserredirect) == false)) { obj.blockedUsers++; return null; }
731
+ const domain = (req.url ? getDomain(req) : getDomain(res));
732
+ if (domain == null) { parent.debug('web', 'handleRootRequest: invalid domain.'); try { res.sendStatus(404); } catch (ex) { } return; }
733
+ if ((domain.userblockedip != null) && (checkIpAddressEx(req, res, domain.userblockedip, true, domain.ipblockeduserredirect) == true)) { obj.blockedUsers++; return null; }
734
+ if ((domain.userallowedip != null) && (checkIpAddressEx(req, res, domain.userallowedip, false, domain.ipblockeduserredirect) == false)) { obj.blockedUsers++; return null; }
735
+ return domain;
736
+ }
737
+
738
+ // Check if the source IP address is allowed, return domain if allowed
739
+ // If there is a fail and null is returned, the request or connection is closed already.
740
+ function checkAgentIpAddress(req, res) {
741
+ if ((parent.config.settings.agentblockedip != null) && (checkIpAddressEx(req, res, parent.config.settings.agentblockedip, null) == true)) { obj.blockedAgents++; return null; }
742
+ if ((parent.config.settings.agentallowedip != null) && (checkIpAddressEx(req, res, parent.config.settings.agentallowedip, null) == false)) { obj.blockedAgents++; return null; }
743
+ const domain = (req.url ? getDomain(req) : getDomain(res));
744
+ if ((domain.agentblockedip != null) && (checkIpAddressEx(req, res, domain.agentblockedip, null) == true)) { obj.blockedAgents++; return null; }
745
+ if ((domain.agentallowedip != null) && (checkIpAddressEx(req, res, domain.agentallowedip, null) == false)) { obj.blockedAgents++; return null; }
746
+ return domain;
747
+ }
748
+
749
+ // Return the current domain of the request
750
+ // Request or connection says open regardless of the response
751
+ function getDomain(req) {
752
+ if (req.xdomain != null) { return req.xdomain; } // Domain already set for this request, return it.
753
+ 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.
754
+ var x = req.url.split('/');
755
+ if (x.length < 2) return parent.config.domains[''];
756
+ var y = parent.config.domains[x[1].toLowerCase()];
757
+ if ((y != null) && (y.dns == null)) { return parent.config.domains[x[1].toLowerCase()]; }
758
+ return parent.config.domains[''];
759
+ }
760
+
761
+ function handleLogoutRequest(req, res) {
762
+ const domain = checkUserIpAddress(req, res);
763
+ if (domain == null) { return; }
764
+ if (domain.auth == 'sspi') { parent.debug('web', 'handleLogoutRequest: failed checks.'); res.sendStatus(404); return; }
765
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
766
+
767
+ res.set({ 'Cache-Control': 'no-store' });
768
+ // Destroy the user's session to log them out will be re-created next request
769
+ var userid = req.session.userid;
770
+ if (req.session.userid) {
771
+ var user = obj.users[req.session.userid];
772
+ 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 }); }
773
+ if (req.session.x) { clearDestroyedSessions(); obj.destroyedSessions[req.session.userid + '/' + req.session.x] = Date.now(); } // Destroy this session
774
+ }
775
+ req.session = null;
776
+ parent.debug('web', 'handleLogoutRequest: success.');
777
+
778
+ // If this user was logged in using an authentication strategy and there is a logout URL, use it.
779
+ if ((userid != null) && (domain.authstrategies != null)) {
780
+ const u = userid.split('/')[2];
781
+ if (u.startsWith('~twitter:') && (domain.authstrategies.twitter != null) && (typeof domain.authstrategies.twitter.logouturl == 'string')) { res.redirect(domain.authstrategies.twitter.logouturl); return; }
782
+ if (u.startsWith('~google:') && (domain.authstrategies.google != null) && (typeof domain.authstrategies.google.logouturl == 'string')) { res.redirect(domain.authstrategies.google.logouturl); return; }
783
+ if (u.startsWith('~github:') && (domain.authstrategies.github != null) && (typeof domain.authstrategies.github.logouturl == 'string')) { res.redirect(domain.authstrategies.github.logouturl); return; }
784
+ if (u.startsWith('~reddit:') && (domain.authstrategies.reddit != null) && (typeof domain.authstrategies.reddit.logouturl == 'string')) { res.redirect(domain.authstrategies.reddit.logouturl); return; }
785
+ if (u.startsWith('~azure:') && (domain.authstrategies.azure != null) && (typeof domain.authstrategies.azure.logouturl == 'string')) { res.redirect(domain.authstrategies.azure.logouturl); return; }
786
+ if (u.startsWith('~oidc:') && (domain.authstrategies.oidc != null) && (typeof domain.authstrategies.oidc.logouturl == 'string')) { res.redirect(domain.authstrategies.oidc.logouturl); return; }
787
+ if (u.startsWith('~jumpcloud:') && (domain.authstrategies.jumpcloud != null) && (typeof domain.authstrategies.jumpcloud.logouturl == 'string')) { res.redirect(domain.authstrategies.jumpcloud.logouturl); return; }
788
+ if (u.startsWith('~saml:') && (domain.authstrategies.saml != null) && (typeof domain.authstrategies.saml.logouturl == 'string')) { res.redirect(domain.authstrategies.saml.logouturl); return; }
789
+ if (u.startsWith('~intel:') && (domain.authstrategies.intel != null) && (typeof domain.authstrategies.intel.logouturl == 'string')) { res.redirect(domain.authstrategies.intel.logouturl); return; }
790
+ }
791
+
792
+ // This is the default logout redirect to the login page
793
+ if (req.query.key != null) { res.redirect(domain.url + '?key=' + req.query.key); } else { res.redirect(domain.url); }
794
+ }
795
+
796
+ // Return an object with 2FA type if 2-step auth can be skipped
797
+ function checkUserOneTimePasswordSkip(domain, user, req, loginOptions) {
798
+ if (parent.config.settings.no2factorauth == true) return null;
799
+
800
+ // If this login occured using a login token, no 2FA needed.
801
+ if ((loginOptions != null) && (typeof loginOptions.tokenName === 'string')) { return { twoFactorType: 'tokenlogin' }; }
802
+
803
+ // Check if we can skip 2nd factor auth because of the source IP address
804
+ if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
805
+ for (var i in domain.passwordrequirements.skip2factor) { if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) { return { twoFactorType: 'ipaddr' }; } }
806
+ }
807
+
808
+ // Check if a 2nd factor cookie is present
809
+ if (typeof req.headers.cookie == 'string') {
810
+ const cookies = req.headers.cookie.split('; ');
811
+ for (var i in cookies) {
812
+ if (cookies[i].startsWith('twofactor=')) {
813
+ 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.
814
+ if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { return { twoFactorType: 'cookie' }; }
815
+ }
816
+ }
817
+ }
818
+
819
+ return null;
820
+ }
821
+
822
+ // Return true if this user has 2-step auth active
823
+ function checkUserOneTimePasswordRequired(domain, user, req, loginOptions) {
824
+ // If this login occured using a login token, no 2FA needed.
825
+ if ((loginOptions != null) && (typeof loginOptions.tokenName === 'string')) { return false; }
826
+
827
+ // Check if we can skip 2nd factor auth because of the source IP address
828
+ if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
829
+ for (var i in domain.passwordrequirements.skip2factor) { if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) return false; }
830
+ }
831
+
832
+ // Check if a 2nd factor cookie is present
833
+ if (typeof req.headers.cookie == 'string') {
834
+ const cookies = req.headers.cookie.split('; ');
835
+ for (var i in cookies) {
836
+ if (cookies[i].startsWith('twofactor=')) {
837
+ 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.
838
+ if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { return false; }
839
+ }
840
+ }
841
+ }
842
+
843
+ // See if SMS 2FA is available
844
+ var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
845
+
846
+ // Check if a 2nd factor is present
847
+ return ((parent.config.settings.no2factorauth !== true) && (sms2fa || (user.otpsecret != null) || ((user.email != null) && (user.emailVerified == true) && (domain.mailserver != null) && (user.otpekey != null)) || ((user.otphkeys != null) && (user.otphkeys.length > 0))));
848
+ }
849
+
850
+ // Check the 2-step auth token
851
+ function checkUserOneTimePassword(req, domain, user, token, hwtoken, func) {
852
+ parent.debug('web', 'checkUserOneTimePassword()');
853
+ const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.nousers !== true) && (parent.config.settings.no2factorauth !== true));
854
+ if (twoStepLoginSupported == false) { parent.debug('web', 'checkUserOneTimePassword: not supported.'); func(true); return; };
855
+
856
+ // Check if we can use OTP tokens with email
857
+ var otpemail = (domain.mailserver != null);
858
+ if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; }
859
+ var otpsms = (parent.smsserver != null);
860
+ if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; }
861
+
862
+ // Check 2FA login cookie
863
+ if ((token != null) && (token.startsWith('cookie='))) {
864
+ 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.
865
+ if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { func(true, { twoFactorType: 'cookie' }); return; }
866
+ }
867
+
868
+ // Check email key
869
+ if ((otpemail) && (user.otpekey != null) && (user.otpekey.d != null) && (user.otpekey.k === token)) {
870
+ var deltaTime = (Date.now() - user.otpekey.d);
871
+ if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the email token (10000 * 60 * 5).
872
+ user.otpekey = {};
873
+ obj.db.SetUser(user);
874
+ parent.debug('web', 'checkUserOneTimePassword: success (email).');
875
+ func(true, { twoFactorType: 'email' });
876
+ return;
877
+ }
878
+ }
879
+
880
+ // Check sms key
881
+ if ((otpsms) && (user.phone != null) && (user.otpsms != null) && (user.otpsms.d != null) && (user.otpsms.k === token)) {
882
+ var deltaTime = (Date.now() - user.otpsms.d);
883
+ if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the SMS token (10000 * 60 * 5).
884
+ delete user.otpsms;
885
+ obj.db.SetUser(user);
886
+ parent.debug('web', 'checkUserOneTimePassword: success (SMS).');
887
+ func(true, { twoFactorType: 'sms' });
888
+ return;
889
+ }
890
+ }
891
+
892
+ // Check hardware key
893
+ if (user.otphkeys && (user.otphkeys.length > 0) && (typeof (hwtoken) == 'string') && (hwtoken.length > 0)) {
894
+ var authResponse = null;
895
+ try { authResponse = JSON.parse(hwtoken); } catch (ex) { }
896
+ if ((authResponse != null) && (authResponse.clientDataJSON)) {
897
+ // Get all WebAuthn keys
898
+ var webAuthnKeys = [];
899
+ for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 3) { webAuthnKeys.push(user.otphkeys[i]); } }
900
+ if (webAuthnKeys.length > 0) {
901
+ // Decode authentication response
902
+ var clientAssertionResponse = { response: {} };
903
+ clientAssertionResponse.id = authResponse.id;
904
+ clientAssertionResponse.rawId = Buffer.from(authResponse.id, 'base64');
905
+ clientAssertionResponse.response.authenticatorData = Buffer.from(authResponse.authenticatorData, 'base64');
906
+ clientAssertionResponse.response.clientDataJSON = Buffer.from(authResponse.clientDataJSON, 'base64');
907
+ clientAssertionResponse.response.signature = Buffer.from(authResponse.signature, 'base64');
908
+ clientAssertionResponse.response.userHandle = Buffer.from(authResponse.userHandle, 'base64');
909
+
910
+ // Look for the key with clientAssertionResponse.id
911
+ var webAuthnKey = null;
912
+ for (var i = 0; i < webAuthnKeys.length; i++) { if (webAuthnKeys[i].keyId == clientAssertionResponse.id) { webAuthnKey = webAuthnKeys[i]; } }
913
+
914
+ // If we found a valid key to use, let's validate the response
915
+ if (webAuthnKey != null) {
916
+ // Figure out the origin
917
+ var httpport = ((args.aliasport != null) ? args.aliasport : args.port);
918
+ var origin = 'https://' + (domain.dns ? domain.dns : parent.certificates.CommonName);
919
+ if (httpport != 443) { origin += ':' + httpport; }
920
+
921
+ var assertionExpectations = {
922
+ challenge: req.session.u2f,
923
+ origin: origin,
924
+ factor: 'either',
925
+ fmt: 'fido-u2f',
926
+ publicKey: webAuthnKey.publicKey,
927
+ prevCounter: webAuthnKey.counter,
928
+ userHandle: Buffer.from(user._id, 'binary').toString('base64')
929
+ };
930
+
931
+ var webauthnResponse = null;
932
+ try { webauthnResponse = obj.webauthn.verifyAuthenticatorAssertionResponse(clientAssertionResponse.response, assertionExpectations); } catch (ex) { parent.debug('web', 'checkUserOneTimePassword: exception ' + ex); console.log(ex); }
933
+ if ((webauthnResponse != null) && (webauthnResponse.verified === true)) {
934
+ // Update the hardware key counter and accept the 2nd factor
935
+ webAuthnKey.counter = webauthnResponse.counter;
936
+ obj.db.SetUser(user);
937
+ parent.debug('web', 'checkUserOneTimePassword: success (hardware).');
938
+ func(true, { twoFactorType: 'fido' });
939
+ } else {
940
+ parent.debug('web', 'checkUserOneTimePassword: fail (hardware).');
941
+ func(false);
942
+ }
943
+ return;
944
+ }
945
+ }
946
+ }
947
+ }
948
+
949
+ // Check Google Authenticator
950
+ const otplib = require('otplib')
951
+ otplib.authenticator.options = { window: 2 }; // Set +/- 1 minute window
952
+ if (user.otpsecret && (typeof (token) == 'string') && (token.length == 6) && (otplib.authenticator.check(token, user.otpsecret) == true)) {
953
+ parent.debug('web', 'checkUserOneTimePassword: success (authenticator).');
954
+ func(true, { twoFactorType: 'otp' });
955
+ return;
956
+ };
957
+
958
+ // Check written down keys
959
+ if ((user.otpkeys != null) && (user.otpkeys.keys != null) && (typeof (token) == 'string') && (token.length == 8)) {
960
+ var tokenNumber = parseInt(token);
961
+ for (var i = 0; i < user.otpkeys.keys.length; i++) {
962
+ if ((tokenNumber === user.otpkeys.keys[i].p) && (user.otpkeys.keys[i].u === true)) {
963
+ parent.debug('web', 'checkUserOneTimePassword: success (one-time).');
964
+ user.otpkeys.keys[i].u = false; func(true, { twoFactorType: 'backup' }); return;
965
+ }
966
+ }
967
+ }
968
+
969
+ // Check OTP hardware key (Yubikey OTP)
970
+ 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)) {
971
+ var keyId = token.substring(0, 12);
972
+
973
+ // Find a matching OTP key
974
+ var match = false;
975
+ for (var i = 0; i < user.otphkeys.length; i++) { if ((user.otphkeys[i].type === 2) && (user.otphkeys[i].keyid === keyId)) { match = true; } }
976
+
977
+ // If we have a match, check the OTP
978
+ if (match === true) {
979
+ var yubikeyotp = require('yubikeyotp');
980
+ var request = { otp: token, id: domain.yubikey.id, key: domain.yubikey.secret, timestamp: true }
981
+ if (domain.yubikey.proxy) { request.requestParams = { proxy: domain.yubikey.proxy }; }
982
+ yubikeyotp.verifyOTP(request, function (err, results) {
983
+ if ((results != null) && (results.status == 'OK')) {
984
+ parent.debug('web', 'checkUserOneTimePassword: success (Yubikey).');
985
+ func(true, { twoFactorType: 'hwotp' });
986
+ } else {
987
+ parent.debug('web', 'checkUserOneTimePassword: fail (Yubikey).');
988
+ func(false);
989
+ }
990
+ });
991
+ return;
992
+ }
993
+ }
994
+
995
+ parent.debug('web', 'checkUserOneTimePassword: fail (2).');
996
+ func(false);
997
+ }
998
+
999
+ // Return a U2F hardware key challenge
1000
+ function getHardwareKeyChallenge(req, domain, user, func) {
1001
+ delete req.session.u2f;
1002
+ if (user.otphkeys && (user.otphkeys.length > 0)) {
1003
+ // Get all WebAuthn keys
1004
+ var webAuthnKeys = [];
1005
+ for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 3) { webAuthnKeys.push(user.otphkeys[i]); } }
1006
+ if (webAuthnKeys.length > 0) {
1007
+ // Generate a Webauthn challenge, this is really easy, no need to call any modules to do this.
1008
+ var authnOptions = { type: 'webAuthn', keyIds: [], timeout: 60000, challenge: obj.crypto.randomBytes(64).toString('base64') };
1009
+ for (var i = 0; i < webAuthnKeys.length; i++) { authnOptions.keyIds.push(webAuthnKeys[i].keyId); }
1010
+ req.session.u2f = authnOptions.challenge;
1011
+ parent.debug('web', 'getHardwareKeyChallenge: success');
1012
+ func(JSON.stringify(authnOptions));
1013
+ return;
1014
+ }
1015
+ }
1016
+ parent.debug('web', 'getHardwareKeyChallenge: fail');
1017
+ func('');
1018
+ }
1019
+
1020
+ // Redirect a root request to a different page
1021
+ function handleRootRedirect(req, res, direct) {
1022
+ const domain = checkUserIpAddress(req, res);
1023
+ if (domain == null) { return; }
1024
+ res.redirect(domain.rootredirect + getQueryPortion(req));
1025
+ }
1026
+
1027
+ function handleLoginRequest(req, res, direct) {
1028
+ const domain = checkUserIpAddress(req, res);
1029
+ if (domain == null) { return; }
1030
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1031
+
1032
+ // Check if this is a banned ip address
1033
+ if (obj.checkAllowLogin(req) == false) {
1034
+ // Wait and redirect the user
1035
+ setTimeout(function () {
1036
+ req.session.messageid = 114; // IP address blocked, try again later.
1037
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1038
+ }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
1039
+ return;
1040
+ }
1041
+
1042
+ // Normally, use the body username/password. If this is a token, use the username/password in the session.
1043
+ var xusername = req.body.username, xpassword = req.body.password;
1044
+ if ((xusername == null) && (xpassword == null) && (req.body.token != null)) { xusername = req.session.tuser; xpassword = req.session.tpass; }
1045
+
1046
+ // Authenticate the user
1047
+ obj.authenticate(xusername, xpassword, domain, function (err, userid, passhint, loginOptions) {
1048
+ if (userid) {
1049
+ var user = obj.users[userid];
1050
+
1051
+ // Check if we are in maintenance mode
1052
+ if ((parent.config.settings.maintenancemode != null) && (user.siteadmin != 4294967295)) {
1053
+ req.session.messageid = 115; // Server under maintenance
1054
+ req.session.loginmode = 1;
1055
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1056
+ return;
1057
+ }
1058
+
1059
+ var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.email != null) && (user.emailVerified == true) && (user.otpekey != null));
1060
+ var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
1061
+ var push2fa = ((parent.firebase != null) && (user.otpdev != null));
1062
+
1063
+ // Check if two factor can be skipped
1064
+ const twoFactorSkip = checkUserOneTimePasswordSkip(domain, user, req, loginOptions);
1065
+
1066
+ // Check if this user has 2-step login active
1067
+ if ((twoFactorSkip == null) && (req.session.loginmode != 6) && checkUserOneTimePasswordRequired(domain, user, req, loginOptions)) {
1068
+ if ((req.body.hwtoken == '**timeout**')) {
1069
+ delete req.session; // Clear the session
1070
+ res.redirect(domain.url + getQueryPortion(req));
1071
+ return;
1072
+ }
1073
+
1074
+ if ((req.body.hwtoken == '**email**') && email2fa) {
1075
+ user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
1076
+ obj.db.SetUser(user);
1077
+ parent.debug('web', 'Sending 2FA email to: ' + user.email);
1078
+ domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key);
1079
+ req.session.messageid = 2; // "Email sent" message
1080
+ req.session.loginmode = 4;
1081
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1082
+ return;
1083
+ }
1084
+
1085
+ if ((req.body.hwtoken == '**sms**') && sms2fa) {
1086
+ // Cause a token to be sent to the user's phone number
1087
+ user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
1088
+ obj.db.SetUser(user);
1089
+ parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
1090
+ parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
1091
+ // Ask for a login token & confirm sms was sent
1092
+ req.session.messageid = 4; // "SMS sent" message
1093
+ req.session.loginmode = 4;
1094
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1095
+ return;
1096
+ }
1097
+
1098
+ // Handle device push notification 2FA request
1099
+ // We create a browser cookie, send it back and when the browser connects it's web socket, it will trigger the push notification.
1100
+ if ((req.body.hwtoken == '**push**') && push2fa && ((domain.passwordrequirements == null) || (domain.passwordrequirements.push2factor != false))) {
1101
+ const logincodeb64 = Buffer.from(obj.common.zeroPad(getRandomSixDigitInteger(), 6)).toString('base64');
1102
+ const sessioncode = obj.crypto.randomBytes(24).toString('base64');
1103
+
1104
+ // Create a browser cookie so the browser can connect using websocket and wait for device accept/reject.
1105
+ const browserCookie = parent.encodeCookie({ a: 'waitAuth', c: logincodeb64, u: user._id, n: user.otpdev, s: sessioncode, d: domain.id });
1106
+
1107
+ // Get the HTTPS port
1108
+ var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port if specified
1109
+
1110
+ // Get the agent connection server name
1111
+ var serverName = obj.getWebServerName(domain);
1112
+ if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
1113
+
1114
+ // Build the connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
1115
+ var xdomain = (domain.dns == null) ? domain.id : '';
1116
+ if (xdomain != '') xdomain += '/';
1117
+ var url = 'wss://' + serverName + ':' + httpsPort + '/' + xdomain + '2fahold.ashx?c=' + browserCookie;
1118
+
1119
+ // Request that the login page wait for device auth
1120
+ req.session.messageid = 5; // "Sending notification..." message
1121
+ req.session.passhint = url;
1122
+ req.session.loginmode = 8;
1123
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1124
+ return;
1125
+ }
1126
+
1127
+ checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result, authData) {
1128
+ if (result == false) {
1129
+ var randomWaitTime = 0;
1130
+
1131
+ // Check if 2FA is allowed for this IP address
1132
+ if (obj.checkAllow2Fa(req) == false) {
1133
+ // Wait and redirect the user
1134
+ setTimeout(function () {
1135
+ req.session.messageid = 114; // IP address blocked, try again later.
1136
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1137
+ }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
1138
+ return;
1139
+ }
1140
+
1141
+ // 2-step auth is required, but the token is not present or not valid.
1142
+ if ((req.body.token != null) || (req.body.hwtoken != null)) {
1143
+ randomWaitTime = 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095); // This is a fail, wait a random time. 2 to 6 seconds.
1144
+ req.session.messageid = 108; // Invalid token, try again.
1145
+ if (obj.parent.authlog) { obj.parent.authLog('https', 'Failed 2FA for ' + xusername + ' from ' + cleanRemoteAddr(req.clientIp) + ' port ' + req.port); }
1146
+ parent.debug('web', 'handleLoginRequest: invalid 2FA token');
1147
+ const ua = getUserAgentInfo(req);
1148
+ 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, msgid: 108, msgArgs: [req.clientIp, ua.browserStr, ua.osStr] });
1149
+ obj.setbad2Fa(req);
1150
+ } else {
1151
+ parent.debug('web', 'handleLoginRequest: 2FA token required');
1152
+ }
1153
+
1154
+ // Wait and redirect the user
1155
+ setTimeout(function () {
1156
+ req.session.loginmode = 4;
1157
+ if ((user.email != null) && (user.emailVerified == true) && (domain.mailserver != null) && (user.otpekey != null)) { req.session.temail = 1; }
1158
+ if ((user.phone != null) && (parent.smsserver != null)) { req.session.tsms = 1; }
1159
+ if ((user.otpdev != null) && (parent.firebase != null)) { req.session.tpush = 1; }
1160
+ req.session.tuserid = userid;
1161
+ req.session.tuser = xusername;
1162
+ req.session.tpass = xpassword;
1163
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1164
+ }, randomWaitTime);
1165
+ } else {
1166
+ // Check if we need to remember this device
1167
+ if ((req.body.remembertoken === 'on') && ((domain.twofactorcookiedurationdays == null) || (domain.twofactorcookiedurationdays > 0))) {
1168
+ var maxCookieAge = domain.twofactorcookiedurationdays;
1169
+ if (typeof maxCookieAge != 'number') { maxCookieAge = 30; }
1170
+ const twoFactorCookie = obj.parent.encodeCookie({ userid: user._id, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey);
1171
+ res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: parent.config.settings.cookiesamesite, secure: true });
1172
+ }
1173
+
1174
+ // Check if email address needs to be confirmed
1175
+ const emailcheck = ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap'))
1176
+ if (emailcheck && (user.emailVerified !== true)) {
1177
+ parent.debug('web', 'Redirecting using ' + user.name + ' to email check login page');
1178
+ req.session.messageid = 3; // "Email verification required" message
1179
+ req.session.loginmode = 7;
1180
+ req.session.passhint = user.email;
1181
+ req.session.cuserid = userid;
1182
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1183
+ return;
1184
+ }
1185
+
1186
+ // Login successful
1187
+ if (obj.parent.authlog) { obj.parent.authLog('https', 'Accepted password for ' + xusername + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); }
1188
+ parent.debug('web', 'handleLoginRequest: successful 2FA login');
1189
+ if (authData != null) { if (loginOptions == null) { loginOptions = {}; } loginOptions.twoFactorType = authData.twoFactorType; }
1190
+ completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions);
1191
+ }
1192
+ });
1193
+ return;
1194
+ }
1195
+
1196
+ // Check if email address needs to be confirmed
1197
+ const emailcheck = ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap'))
1198
+ if (emailcheck && (user.emailVerified !== true)) {
1199
+ parent.debug('web', 'Redirecting using ' + user.name + ' to email check login page');
1200
+ req.session.messageid = 3; // "Email verification required" message
1201
+ req.session.loginmode = 7;
1202
+ req.session.passhint = user.email;
1203
+ req.session.cuserid = userid;
1204
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1205
+ return;
1206
+ }
1207
+
1208
+ // Login successful
1209
+ if (obj.parent.authlog) { obj.parent.authLog('https', 'Accepted password for ' + xusername + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); }
1210
+ parent.debug('web', 'handleLoginRequest: successful login');
1211
+ if (twoFactorSkip != null) { if (loginOptions == null) { loginOptions = {}; } loginOptions.twoFactorType = twoFactorSkip.twoFactorType; }
1212
+ completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions);
1213
+ } else {
1214
+ // Login failed, log the error
1215
+ if (obj.parent.authlog) { obj.parent.authLog('https', 'Failed password for ' + xusername + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); }
1216
+
1217
+ // Wait a random delay
1218
+ setTimeout(function () {
1219
+ // If the account is locked, display that.
1220
+ if (typeof xusername == 'string') {
1221
+ var xuserid = 'user/' + domain.id + '/' + xusername.toLowerCase();
1222
+ if (err == 'locked') {
1223
+ parent.debug('web', 'handleLoginRequest: login failed, locked account');
1224
+ req.session.messageid = 110; // Account locked.
1225
+ const ua = getUserAgentInfo(req);
1226
+ 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, msgid: 109, msgArgs: [req.clientIp, ua.browserStr, ua.osStr] });
1227
+ obj.setbadLogin(req);
1228
+ } else {
1229
+ parent.debug('web', 'handleLoginRequest: login failed, bad username and password');
1230
+ req.session.messageid = 112; // Login failed, check username and password.
1231
+ const ua = getUserAgentInfo(req);
1232
+ obj.parent.DispatchEvent(['*', 'server-users', xuserid], obj, { action: 'authfail', userid: xuserid, username: xusername, domain: domain.id, msg: 'Invalid user login attempt from ' + req.clientIp, msgid: 110, msgArgs: [req.clientIp, ua.browserStr, ua.osStr] });
1233
+ obj.setbadLogin(req);
1234
+ }
1235
+ }
1236
+
1237
+ // Clean up login mode and display password hint if present.
1238
+ delete req.session.loginmode;
1239
+ if ((passhint != null) && (passhint.length > 0)) {
1240
+ req.session.passhint = passhint;
1241
+ } else {
1242
+ delete req.session.passhint;
1243
+ }
1244
+
1245
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1246
+ }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095)); // Wait for 2 to ~6 seconds.
1247
+ }
1248
+ });
1249
+ }
1250
+
1251
+ function completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions) {
1252
+ // Check if we need to change the password
1253
+ 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))))) {
1254
+ // Request a password change
1255
+ parent.debug('web', 'handleLoginRequest: login ok, password change requested');
1256
+ req.session.loginmode = 6;
1257
+ req.session.messageid = 113; // Password change requested.
1258
+ req.session.resettokenuserid = userid;
1259
+ req.session.resettokenusername = xusername;
1260
+ req.session.resettokenpassword = xpassword;
1261
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1262
+ return;
1263
+ }
1264
+
1265
+ // Save login time
1266
+ user.pastlogin = user.login;
1267
+ user.login = user.access = Math.floor(Date.now() / 1000);
1268
+ obj.db.SetUser(user);
1269
+
1270
+ // Notify account login
1271
+ const targets = ['*', 'server-users', user._id];
1272
+ if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
1273
+ const ua = getUserAgentInfo(req);
1274
+ const loginEvent = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'login', msgid: 107, msgArgs: [req.clientIp, ua.browserStr, ua.osStr], msg: 'Account login', domain: domain.id, ip: req.clientIp, userAgent: req.headers['user-agent'] };
1275
+ if (loginOptions != null) {
1276
+ if ((loginOptions.tokenName != null) && (loginOptions.tokenUser != null)) { loginEvent.tokenName = loginOptions.tokenName; loginEvent.tokenUser = loginOptions.tokenUser; } // If a login token was used, add it to the event.
1277
+ if (loginOptions.twoFactorType != null) { loginEvent.twoFactorType = loginOptions.twoFactorType; }
1278
+ }
1279
+ obj.parent.DispatchEvent(targets, obj, loginEvent);
1280
+
1281
+ // Regenerate session when signing in to prevent fixation
1282
+ //req.session.regenerate(function () {
1283
+ // Store the user's primary key in the session store to be retrieved, or in this case the entire user object
1284
+ delete req.session.u2f;
1285
+ delete req.session.loginmode;
1286
+ delete req.session.tuserid;
1287
+ delete req.session.tuser;
1288
+ delete req.session.tpass;
1289
+ delete req.session.temail;
1290
+ delete req.session.tsms;
1291
+ delete req.session.tpush;
1292
+ delete req.session.messageid;
1293
+ delete req.session.passhint;
1294
+ delete req.session.cuserid;
1295
+ delete req.session.expire;
1296
+ delete req.session.currentNode;
1297
+ req.session.userid = userid;
1298
+ req.session.ip = req.clientIp;
1299
+ setSessionRandom(req);
1300
+
1301
+ // If a login token was used, add this information and expire time to the session.
1302
+ if ((loginOptions != null) && (loginOptions.tokenName != null) && (loginOptions.tokenUser != null)) {
1303
+ req.session.loginToken = loginOptions.tokenUser;
1304
+ if (loginOptions.expire != null) { req.session.expire = loginOptions.expire; }
1305
+ }
1306
+
1307
+ if (req.body.viewmode) { req.session.viewmode = req.body.viewmode; }
1308
+ if (req.body.host) {
1309
+ // TODO: This is a terrible search!!! FIX THIS.
1310
+ /*
1311
+ obj.db.GetAllType('node', function (err, docs) {
1312
+ for (var i = 0; i < docs.length; i++) {
1313
+ if (docs[i].name == req.body.host) {
1314
+ req.session.currentNode = docs[i]._id;
1315
+ break;
1316
+ }
1317
+ }
1318
+ console.log("CurrentNode: " + req.session.currentNode);
1319
+ // This redirect happens after finding node is completed
1320
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1321
+ });
1322
+ */
1323
+ parent.debug('web', 'handleLoginRequest: login ok (1)');
1324
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } // Temporary
1325
+ } else {
1326
+ parent.debug('web', 'handleLoginRequest: login ok (2)');
1327
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1328
+ }
1329
+ //});
1330
+ }
1331
+
1332
+ function handleCreateAccountRequest(req, res, direct) {
1333
+ const domain = checkUserIpAddress(req, res);
1334
+ if (domain == null) { return; }
1335
+ if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handleCreateAccountRequest: failed checks.'); res.sendStatus(404); return; }
1336
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1337
+ if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
1338
+
1339
+ // Check if we are in maintenance mode
1340
+ if (parent.config.settings.maintenancemode != null) {
1341
+ req.session.messageid = 115; // Server under maintenance
1342
+ req.session.loginmode = 1;
1343
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1344
+ return;
1345
+ }
1346
+
1347
+ // Always lowercase the email address
1348
+ if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
1349
+
1350
+ // If the email is the username, set this here.
1351
+ if (domain.usernameisemail) { req.body.username = req.body.email; }
1352
+
1353
+ // Accounts that start with ~ are not allowed
1354
+ if ((typeof req.body.username != 'string') || (req.body.username.length < 1) || (req.body.username[0] == '~')) {
1355
+ parent.debug('web', 'handleCreateAccountRequest: unable to create account (0)');
1356
+ req.session.loginmode = 2;
1357
+ req.session.messageid = 100; // Unable to create account.
1358
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1359
+ return;
1360
+ }
1361
+
1362
+ // Count the number of users in this domain
1363
+ var domainUserCount = 0;
1364
+ for (var i in obj.users) { if (obj.users[i].domain == domain.id) { domainUserCount++; } }
1365
+
1366
+ // Check if we are allowed to create new users using the login screen
1367
+ if ((domain.newaccounts !== 1) && (domain.newaccounts !== true) && (domainUserCount > 0)) {
1368
+ parent.debug('web', 'handleCreateAccountRequest: domainUserCount > 1.');
1369
+ res.sendStatus(401);
1370
+ return;
1371
+ }
1372
+
1373
+ // Check if this request is for an allows email domain
1374
+ if ((domain.newaccountemaildomains != null) && Array.isArray(domain.newaccountemaildomains)) {
1375
+ var i = -1;
1376
+ if (typeof req.body.email == 'string') { i = req.body.email.indexOf('@'); }
1377
+ if (i == -1) {
1378
+ parent.debug('web', 'handleCreateAccountRequest: unable to create account (1)');
1379
+ req.session.loginmode = 2;
1380
+ req.session.messageid = 100; // Unable to create account.
1381
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1382
+ return;
1383
+ }
1384
+ var emailok = false, emaildomain = req.body.email.substring(i + 1).toLowerCase();
1385
+ for (var i in domain.newaccountemaildomains) { if (emaildomain == domain.newaccountemaildomains[i].toLowerCase()) { emailok = true; } }
1386
+ if (emailok == false) {
1387
+ parent.debug('web', 'handleCreateAccountRequest: unable to create account (2)');
1388
+ req.session.loginmode = 2;
1389
+ req.session.messageid = 100; // Unable to create account.
1390
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1391
+ return;
1392
+ }
1393
+ }
1394
+
1395
+ // Check if we exceed the maximum number of user accounts
1396
+ obj.db.isMaxType(domain.limits.maxuseraccounts, 'user', domain.id, function (maxExceed) {
1397
+ if (maxExceed) {
1398
+ parent.debug('web', 'handleCreateAccountRequest: account limit reached');
1399
+ req.session.loginmode = 2;
1400
+ req.session.messageid = 101; // Account limit reached.
1401
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1402
+ } else {
1403
+ 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)) {
1404
+ parent.debug('web', 'handleCreateAccountRequest: unable to create account (3)');
1405
+ req.session.loginmode = 2;
1406
+ req.session.messageid = 100; // Unable to create account.
1407
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1408
+ } else {
1409
+ // Check if this email was already verified
1410
+ obj.db.GetUserWithVerifiedEmail(domain.id, req.body.email, function (err, docs) {
1411
+ if ((docs != null) && (docs.length > 0)) {
1412
+ parent.debug('web', 'handleCreateAccountRequest: Existing account with this email address');
1413
+ req.session.loginmode = 2;
1414
+ req.session.messageid = 102; // Existing account with this email address.
1415
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1416
+ } else {
1417
+ // Check if there is domain.newAccountToken, check if supplied token is valid
1418
+ if ((domain.newaccountspass != null) && (domain.newaccountspass != '') && (req.body.anewaccountpass != domain.newaccountspass)) {
1419
+ parent.debug('web', 'handleCreateAccountRequest: Invalid account creation token');
1420
+ req.session.loginmode = 2;
1421
+ req.session.messageid = 103; // Invalid account creation token.
1422
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1423
+ return;
1424
+ }
1425
+ // Check if user exists
1426
+ if (obj.users['user/' + domain.id + '/' + req.body.username.toLowerCase()]) {
1427
+ parent.debug('web', 'handleCreateAccountRequest: Username already exists');
1428
+ req.session.loginmode = 2;
1429
+ req.session.messageid = 104; // Username already exists.
1430
+ } else {
1431
+ 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), access: Math.floor(Date.now() / 1000), domain: domain.id };
1432
+ if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; }
1433
+ if (obj.common.validateStrArray(domain.newaccountrealms)) { user.groups = domain.newaccountrealms; }
1434
+ 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; }
1435
+ if (domainUserCount == 0) { user.siteadmin = 4294967295; /*if (domain.newaccounts === 2) { delete domain.newaccounts; }*/ } // If this is the first user, give the account site admin.
1436
+
1437
+ // Auto-join any user groups
1438
+ if (typeof domain.newaccountsusergroups == 'object') {
1439
+ for (var i in domain.newaccountsusergroups) {
1440
+ var ugrpid = domain.newaccountsusergroups[i];
1441
+ if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
1442
+ var ugroup = obj.userGroups[ugrpid];
1443
+ if (ugroup != null) {
1444
+ // Add group to the user
1445
+ if (user.links == null) { user.links = {}; }
1446
+ user.links[ugroup._id] = { rights: 1 };
1447
+
1448
+ // Add user to the group
1449
+ ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
1450
+ db.Set(ugroup);
1451
+
1452
+ // Notify user group change
1453
+ 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 };
1454
+ 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.
1455
+ parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
1456
+ }
1457
+ }
1458
+ }
1459
+
1460
+ obj.users[user._id] = user;
1461
+ req.session.userid = user._id;
1462
+ req.session.ip = req.clientIp; // Bind this session to the IP address of the request
1463
+ setSessionRandom(req);
1464
+ // Create a user, generate a salt and hash the password
1465
+ require('./pass').hash(req.body.password1, function (err, salt, hash, tag) {
1466
+ if (err) throw err;
1467
+ user.salt = salt;
1468
+ user.hash = hash;
1469
+ delete user.passtype;
1470
+ obj.db.SetUser(user);
1471
+
1472
+ // Send the verification email
1473
+ if ((domain.mailserver != null) && (domain.auth != 'sspi') && (domain.auth != 'ldap') && (obj.common.validateEmail(user.email, 1, 256) == true)) { domain.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key); }
1474
+ }, 0);
1475
+ 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 };
1476
+ 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.
1477
+ obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
1478
+ }
1479
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1480
+ }
1481
+ });
1482
+ }
1483
+ }
1484
+ });
1485
+ }
1486
+
1487
+ // Called to process an account password reset
1488
+ function handleResetPasswordRequest(req, res, direct) {
1489
+ const domain = checkUserIpAddress(req, res);
1490
+ if (domain == null) { return; }
1491
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1492
+ if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
1493
+
1494
+ // Check everything is ok
1495
+ const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false));
1496
+ if ((allowAccountReset === false) || (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')) {
1497
+ parent.debug('web', 'handleResetPasswordRequest: checks failed');
1498
+ delete req.session.u2f;
1499
+ delete req.session.loginmode;
1500
+ delete req.session.tuserid;
1501
+ delete req.session.tuser;
1502
+ delete req.session.tpass;
1503
+ delete req.session.resettokenuserid;
1504
+ delete req.session.resettokenusername;
1505
+ delete req.session.resettokenpassword;
1506
+ delete req.session.temail;
1507
+ delete req.session.tsms;
1508
+ delete req.session.tpush;
1509
+ delete req.session.messageid;
1510
+ delete req.session.passhint;
1511
+ delete req.session.cuserid;
1512
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1513
+ return;
1514
+ }
1515
+
1516
+ // Authenticate the user
1517
+ obj.authenticate(req.session.resettokenusername, req.session.resettokenpassword, domain, function (err, userid, passhint, loginOptions) {
1518
+ if (userid) {
1519
+ // Login
1520
+ var user = obj.users[userid];
1521
+
1522
+ // If we have password requirements, check this here.
1523
+ if (!obj.common.checkPasswordRequirements(req.body.rpassword1, domain.passwordrequirements)) {
1524
+ parent.debug('web', 'handleResetPasswordRequest: password rejected, use a different one (1)');
1525
+ req.session.loginmode = 6;
1526
+ req.session.messageid = 105; // Password rejected, use a different one.
1527
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1528
+ return;
1529
+ }
1530
+
1531
+ // Check if the password is the same as a previous one
1532
+ obj.checkOldUserPasswords(domain, user, req.body.rpassword1, function (result) {
1533
+ if (result != 0) {
1534
+ // This is the same password as an older one, request a password change again
1535
+ parent.debug('web', 'handleResetPasswordRequest: password rejected, use a different one (2)');
1536
+ req.session.loginmode = 6;
1537
+ req.session.messageid = 105; // Password rejected, use a different one.
1538
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1539
+ } else {
1540
+ // Update the password, use a different salt.
1541
+ require('./pass').hash(req.body.rpassword1, function (err, salt, hash, tag) {
1542
+ const nowSeconds = Math.floor(Date.now() / 1000);
1543
+ if (err) { parent.debug('web', 'handleResetPasswordRequest: hash error.'); throw err; }
1544
+
1545
+ if (domain.passwordrequirements != null) {
1546
+ // Save password hint if this feature is enabled
1547
+ 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; }
1548
+
1549
+ // Save previous password if this feature is enabled
1550
+ if ((typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
1551
+ if (user.oldpasswords == null) { user.oldpasswords = []; }
1552
+ user.oldpasswords.push({ salt: user.salt, hash: user.hash, start: user.passchange, end: nowSeconds });
1553
+ const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
1554
+ if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
1555
+ }
1556
+ }
1557
+
1558
+ user.salt = salt;
1559
+ user.hash = hash;
1560
+ user.passchange = user.access = nowSeconds;
1561
+ delete user.passtype;
1562
+ obj.db.SetUser(user);
1563
+
1564
+ // Event the account change
1565
+ var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'User password reset', domain: domain.id };
1566
+ 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.
1567
+ obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
1568
+
1569
+ // Login successful
1570
+ parent.debug('web', 'handleResetPasswordRequest: success');
1571
+ req.session.userid = userid;
1572
+ req.session.ip = req.clientIp; // Bind this session to the IP address of the request
1573
+ setSessionRandom(req);
1574
+ completeLoginRequest(req, res, domain, obj.users[userid], userid, req.session.tuser, req.session.tpass, direct, loginOptions);
1575
+ }, 0);
1576
+ }
1577
+ }, 0);
1578
+ } else {
1579
+ // Failed, error out.
1580
+ parent.debug('web', 'handleResetPasswordRequest: failed authenticate()');
1581
+ delete req.session.u2f;
1582
+ delete req.session.loginmode;
1583
+ delete req.session.tuserid;
1584
+ delete req.session.tuser;
1585
+ delete req.session.tpass;
1586
+ delete req.session.resettokenuserid;
1587
+ delete req.session.resettokenusername;
1588
+ delete req.session.resettokenpassword;
1589
+ delete req.session.temail;
1590
+ delete req.session.tsms;
1591
+ delete req.session.tpush;
1592
+ delete req.session.messageid;
1593
+ delete req.session.passhint;
1594
+ delete req.session.cuserid;
1595
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1596
+ return;
1597
+ }
1598
+ });
1599
+ }
1600
+
1601
+ // Called to process an account reset request
1602
+ function handleResetAccountRequest(req, res, direct) {
1603
+ const domain = checkUserIpAddress(req, res);
1604
+ if (domain == null) { return; }
1605
+ const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false));
1606
+ if ((allowAccountReset === false) || (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; }
1607
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1608
+ if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
1609
+
1610
+ // Always lowercase the email address
1611
+ if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
1612
+
1613
+ // Get the email from the body or session.
1614
+ var email = req.body.email;
1615
+ if ((email == null) || (email == '')) { email = req.session.temail; }
1616
+
1617
+ // Check the email string format
1618
+ if (!email || checkEmail(email) == false) {
1619
+ parent.debug('web', 'handleResetAccountRequest: Invalid email');
1620
+ req.session.loginmode = 3;
1621
+ req.session.messageid = 106; // Invalid email.
1622
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1623
+ } else {
1624
+ obj.db.GetUserWithVerifiedEmail(domain.id, email, function (err, docs) {
1625
+ // Remove all accounts that start with ~ since they are special accounts.
1626
+ var cleanDocs = [];
1627
+ if ((err == null) && (docs.length > 0)) {
1628
+ for (var i in docs) {
1629
+ const user = docs[i];
1630
+ const locked = ((user.siteadmin != null) && (user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)); // No password recovery for locked accounts
1631
+ const specialAccount = (user._id.split('/')[2].startsWith('~')); // No password recovery for special accounts
1632
+ if ((specialAccount == false) && (locked == false)) { cleanDocs.push(user); }
1633
+ }
1634
+ }
1635
+ docs = cleanDocs;
1636
+
1637
+ // Check if we have any account that match this email address
1638
+ if ((err != null) || (docs.length == 0)) {
1639
+ parent.debug('web', 'handleResetAccountRequest: Account not found');
1640
+ req.session.loginmode = 3;
1641
+ 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.
1642
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1643
+ } else {
1644
+ // 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.
1645
+ var responseSent = false;
1646
+ for (var i in docs) {
1647
+ var user = docs[i];
1648
+ if (checkUserOneTimePasswordRequired(domain, user, req) == true) {
1649
+ // Second factor setup, request it now.
1650
+ checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result, authData) {
1651
+ if (result == false) {
1652
+ if (i == 0) {
1653
+
1654
+ // Check if 2FA is allowed for this IP address
1655
+ if (obj.checkAllow2Fa(req) == false) {
1656
+ // Wait and redirect the user
1657
+ setTimeout(function () {
1658
+ req.session.messageid = 114; // IP address blocked, try again later.
1659
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1660
+ }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
1661
+ return;
1662
+ }
1663
+
1664
+ // 2-step auth is required, but the token is not present or not valid.
1665
+ parent.debug('web', 'handleResetAccountRequest: Invalid 2FA token, try again');
1666
+ if ((req.body.token != null) || (req.body.hwtoken != null)) {
1667
+ var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
1668
+ if ((req.body.hwtoken == '**sms**') && sms2fa) {
1669
+ // Cause a token to be sent to the user's phone number
1670
+ user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
1671
+ obj.db.SetUser(user);
1672
+ parent.debug('web', 'Sending 2FA SMS for password recovery to: ' + user.phone);
1673
+ parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
1674
+ req.session.messageid = 4; // SMS sent.
1675
+ } else {
1676
+ req.session.messageid = 108; // Invalid token, try again.
1677
+ const ua = getUserAgentInfo(req);
1678
+ 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, msgid: 108, msgArgs: [req.clientIp, ua.browserStr, ua.osStr] });
1679
+ obj.setbad2Fa(req);
1680
+ }
1681
+ }
1682
+ req.session.loginmode = 5;
1683
+ req.session.temail = email;
1684
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1685
+ }
1686
+ } else {
1687
+ // Send email to perform recovery.
1688
+ delete req.session.temail;
1689
+ if (domain.mailserver != null) {
1690
+ domain.mailserver.sendAccountResetMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
1691
+ if (i == 0) {
1692
+ parent.debug('web', 'handleResetAccountRequest: Hold on, reset mail sent.');
1693
+ req.session.loginmode = 1;
1694
+ req.session.messageid = 1; // If valid, reset mail sent.
1695
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1696
+ }
1697
+ } else {
1698
+ if (i == 0) {
1699
+ parent.debug('web', 'handleResetAccountRequest: Unable to sent email.');
1700
+ req.session.loginmode = 3;
1701
+ req.session.messageid = 109; // Unable to sent email.
1702
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1703
+ }
1704
+ }
1705
+ }
1706
+ });
1707
+ } else {
1708
+ // No second factor, send email to perform recovery.
1709
+ if (domain.mailserver != null) {
1710
+ domain.mailserver.sendAccountResetMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
1711
+ if (i == 0) {
1712
+ parent.debug('web', 'handleResetAccountRequest: Hold on, reset mail sent.');
1713
+ req.session.loginmode = 1;
1714
+ req.session.messageid = 1; // If valid, reset mail sent.
1715
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1716
+ }
1717
+ } else {
1718
+ if (i == 0) {
1719
+ parent.debug('web', 'handleResetAccountRequest: Unable to sent email.');
1720
+ req.session.loginmode = 3;
1721
+ req.session.messageid = 109; // Unable to sent email.
1722
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1723
+ }
1724
+ }
1725
+ }
1726
+ }
1727
+ }
1728
+ });
1729
+ }
1730
+ }
1731
+
1732
+ // Handle account email change and email verification request
1733
+ function handleCheckAccountEmailRequest(req, res, direct) {
1734
+ const domain = checkUserIpAddress(req, res);
1735
+ if (domain == null) { return; }
1736
+ if ((domain.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; }
1737
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1738
+ if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
1739
+
1740
+ // Always lowercase the email address
1741
+ if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
1742
+
1743
+ // Get the email from the body or session.
1744
+ var email = req.body.email;
1745
+ if ((email == null) || (email == '')) { email = req.session.temail; }
1746
+
1747
+ // Check if this request is for an allows email domain
1748
+ if ((domain.newaccountemaildomains != null) && Array.isArray(domain.newaccountemaildomains)) {
1749
+ var i = -1;
1750
+ if (typeof req.body.email == 'string') { i = req.body.email.indexOf('@'); }
1751
+ if (i == -1) {
1752
+ parent.debug('web', 'handleCreateAccountRequest: unable to create account (1)');
1753
+ req.session.loginmode = 7;
1754
+ req.session.messageid = 106; // Invalid email.
1755
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1756
+ return;
1757
+ }
1758
+ var emailok = false, emaildomain = req.body.email.substring(i + 1).toLowerCase();
1759
+ for (var i in domain.newaccountemaildomains) { if (emaildomain == domain.newaccountemaildomains[i].toLowerCase()) { emailok = true; } }
1760
+ if (emailok == false) {
1761
+ parent.debug('web', 'handleCreateAccountRequest: unable to create account (2)');
1762
+ req.session.loginmode = 7;
1763
+ req.session.messageid = 106; // Invalid email.
1764
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1765
+ return;
1766
+ }
1767
+ }
1768
+
1769
+ // Check the email string format
1770
+ if (!email || checkEmail(email) == false) {
1771
+ parent.debug('web', 'handleCheckAccountEmailRequest: Invalid email');
1772
+ req.session.loginmode = 7;
1773
+ req.session.messageid = 106; // Invalid email.
1774
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1775
+ } else {
1776
+ // Check is email already exists
1777
+ obj.db.GetUserWithVerifiedEmail(domain.id, email, function (err, docs) {
1778
+ if ((err != null) || ((docs.length > 0) && (docs.find(function (u) { return (u._id === req.session.cuserid); }) < 0))) {
1779
+ // Email already exitst
1780
+ req.session.messageid = 102; // Existing account with this email address.
1781
+ } else {
1782
+ // Update the user and notify of user email address change
1783
+ var user = obj.users[req.session.cuserid];
1784
+ if (user.email != email) {
1785
+ user.email = email;
1786
+ db.SetUser(user);
1787
+ var targets = ['*', 'server-users', user._id];
1788
+ if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
1789
+ var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Account changed: ' + user.name, domain: domain.id };
1790
+ 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.
1791
+ parent.DispatchEvent(targets, obj, event);
1792
+ }
1793
+
1794
+ // Send the verification email
1795
+ domain.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
1796
+
1797
+ // Send the response
1798
+ req.session.messageid = 2; // Email sent.
1799
+ }
1800
+ req.session.loginmode = 7;
1801
+ delete req.session.cuserid;
1802
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1803
+ });
1804
+ }
1805
+ }
1806
+
1807
+ // Called to process a web based email verification request
1808
+ function handleCheckMailRequest(req, res) {
1809
+ const domain = checkUserIpAddress(req, res);
1810
+ if (domain == null) { return; }
1811
+ if ((domain.auth == 'sspi') || (domain.auth == 'ldap') || (domain.mailserver == null)) { parent.debug('web', 'handleCheckMailRequest: failed checks.'); res.sendStatus(404); return; }
1812
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1813
+
1814
+ if (req.query.c != null) {
1815
+ var cookie = obj.parent.decodeCookie(req.query.c, domain.mailserver.mailCookieEncryptionKey, 30);
1816
+ if ((cookie != null) && (cookie.u != null) && (cookie.u.startsWith('user/')) && (cookie.e != null)) {
1817
+ var idsplit = cookie.u.split('/');
1818
+ if ((idsplit.length != 3) || (idsplit[1] != domain.id)) {
1819
+ parent.debug('web', 'handleCheckMailRequest: Invalid domain.');
1820
+ 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));
1821
+ } else {
1822
+ obj.db.Get(cookie.u, function (err, docs) {
1823
+ if (docs.length == 0) {
1824
+ parent.debug('web', 'handleCheckMailRequest: Invalid username.');
1825
+ 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));
1826
+ } else {
1827
+ var user = docs[0];
1828
+ if (user.email != cookie.e) {
1829
+ parent.debug('web', 'handleCheckMailRequest: Invalid e-mail.');
1830
+ 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));
1831
+ } else {
1832
+ if (cookie.a == 1) {
1833
+ // Account email verification
1834
+ if (user.emailVerified == true) {
1835
+ parent.debug('web', 'handleCheckMailRequest: email already verified.');
1836
+ 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));
1837
+ } else {
1838
+ obj.db.GetUserWithVerifiedEmail(domain.id, user.email, function (err, docs) {
1839
+ if ((docs.length > 0) && (docs.find(function (u) { return (u._id === user._id); }) < 0)) {
1840
+ parent.debug('web', 'handleCheckMailRequest: email already in use.');
1841
+ 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));
1842
+ } else {
1843
+ parent.debug('web', 'handleCheckMailRequest: email verification success.');
1844
+
1845
+ // Set the verified flag
1846
+ obj.users[user._id].emailVerified = true;
1847
+ user.emailVerified = true;
1848
+ obj.db.SetUser(user);
1849
+
1850
+ // Event the change
1851
+ 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 };
1852
+ 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.
1853
+ obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
1854
+
1855
+ // Send the confirmation page
1856
+ 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));
1857
+
1858
+ // Send a notification
1859
+ obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: 'Email verified', value: user.email, nolog: 1, id: Math.random() });
1860
+
1861
+ // Send to authlog
1862
+ if (obj.parent.authlog) { obj.parent.authLog('https', 'Verified email address ' + user.email + ' for user ' + user.name); }
1863
+ }
1864
+ });
1865
+ }
1866
+ } else if (cookie.a == 2) {
1867
+ // Account reset
1868
+ if (user.emailVerified != true) {
1869
+ parent.debug('web', 'handleCheckMailRequest: email not verified.');
1870
+ 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));
1871
+ } else {
1872
+ if (req.query.confirm == 1) {
1873
+ // Set a temporary password
1874
+ obj.crypto.randomBytes(16, function (err, buf) {
1875
+ var newpass = buf.toString('base64').split('=').join('').split('/').join('').split('+').join('');
1876
+ require('./pass').hash(newpass, function (err, salt, hash, tag) {
1877
+ if (err) throw err;
1878
+
1879
+ // Change the password
1880
+ var userinfo = obj.users[user._id];
1881
+ userinfo.salt = salt;
1882
+ userinfo.hash = hash;
1883
+ delete userinfo.passtype;
1884
+ userinfo.passchange = userinfo.access = Math.floor(Date.now() / 1000);
1885
+ delete userinfo.passhint;
1886
+ obj.db.SetUser(userinfo);
1887
+
1888
+ // Event the change
1889
+ 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 };
1890
+ 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.
1891
+ obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
1892
+
1893
+ // Send the new password
1894
+ 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));
1895
+ parent.debug('web', 'handleCheckMailRequest: send temporary password.');
1896
+
1897
+ // Send to authlog
1898
+ if (obj.parent.authlog) { obj.parent.authLog('https', 'Performed account reset for user ' + user.name); }
1899
+ }, 0);
1900
+ });
1901
+ } else {
1902
+ // Display a link for the user to confirm password reset
1903
+ // 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.
1904
+ 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));
1905
+ }
1906
+ }
1907
+ } else {
1908
+ 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));
1909
+ }
1910
+ }
1911
+ }
1912
+ });
1913
+ }
1914
+ } else {
1915
+ 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));
1916
+ }
1917
+ }
1918
+ }
1919
+
1920
+ // Called to process an agent invite GET/POST request
1921
+ function handleInviteRequest(req, res) {
1922
+ const domain = getDomain(req);
1923
+ if (domain == null) { parent.debug('web', 'handleInviteRequest: failed checks.'); res.sendStatus(404); return; }
1924
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1925
+ if ((req.body.inviteCode == null) || (req.body.inviteCode == '')) { render(req, res, getRenderPage('invite', req, domain), getRenderArgs({ messageid: 0 }, req, domain)); return; } // No invitation code
1926
+
1927
+ // Each for a device group that has this invite code.
1928
+ for (var i in obj.meshes) {
1929
+ if ((obj.meshes[i].domain == domain.id) && (obj.meshes[i].deleted == null) && (obj.meshes[i].invite != null) && (obj.meshes[i].invite.codes.indexOf(req.body.inviteCode) >= 0)) {
1930
+ // Send invitation link, valid for 1 minute.
1931
+ res.redirect(domain.url + 'agentinvite?c=' + parent.encodeCookie({ a: 4, mid: i, f: obj.meshes[i].invite.flags, ag: obj.meshes[i].invite.ag, expire: 1 }, parent.invitationLinkEncryptionKey) + (req.query.key ? ('&key=' + req.query.key) : '') + (req.query.hide ? ('&hide=' + req.query.hide) : ''));
1932
+ return;
1933
+ }
1934
+ }
1935
+
1936
+ render(req, res, getRenderPage('invite', req, domain), getRenderArgs({ messageid: 100 }, req, domain)); // Bad invitation code
1937
+ }
1938
+
1939
+ // Called to render the MSTSC (RDP) or SSH web page
1940
+ function handleMSTSCRequest(req, res, page) {
1941
+ const domain = getDomain(req);
1942
+ if (domain == null) { parent.debug('web', 'handleMSTSCRequest: failed checks.'); res.sendStatus(404); return; }
1943
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1944
+
1945
+ // Check if we are in maintenance mode
1946
+ if ((parent.config.settings.maintenancemode != null) && (req.query.loginscreen !== '1')) {
1947
+ 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));
1948
+ return;
1949
+ }
1950
+
1951
+ // Set features we want to send to this page
1952
+ var features = 0;
1953
+ if (domain.allowsavingdevicecredentials === false) { features |= 1; }
1954
+
1955
+ if (req.query.ws != null) {
1956
+ // This is a query with a websocket relay cookie, check that the cookie is valid and use it.
1957
+ var rcookie = parent.decodeCookie(req.query.ws, parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
1958
+ if ((rcookie != null) && (rcookie.domainid == domain.id) && (rcookie.nodeid != null) && (rcookie.tcpport != null)) {
1959
+
1960
+ // Fetch the node from the database
1961
+ obj.db.Get(rcookie.nodeid, function (err, nodes) {
1962
+ if ((err != null) || (nodes.length != 1)) { res.sendStatus(404); return; }
1963
+ const node = nodes[0];
1964
+
1965
+ // Check if we have RDP credentials for this device
1966
+ var serverCredentials = false;
1967
+ if (domain.allowsavingdevicecredentials !== false) {
1968
+ if (page == 'ssh') {
1969
+ serverCredentials = ((typeof node.rdp == 'object') && (typeof node.rdp.d == 'string') && (typeof node.rdp.u == 'string') && (typeof node.rdp.p == 'string'))
1970
+ } else {
1971
+ serverCredentials = ((typeof node.ssh == 'object') && (typeof node.ssh.u == 'string'))
1972
+ }
1973
+ }
1974
+
1975
+ // Render the page
1976
+ render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: req.query.ws, name: encodeURIComponent(req.query.name).replace(/'/g, '%27'), serverCredentials: serverCredentials, features: features }, req, domain));
1977
+ });
1978
+ return;
1979
+ }
1980
+ }
1981
+
1982
+ // Get the logged in user if present
1983
+ var user = null;
1984
+
1985
+ // If there is a login token, use that
1986
+ if (req.query.login != null) {
1987
+ var ucookie = parent.decodeCookie(req.query.login, parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
1988
+ if ((ucookie != null) && (ucookie.a === 3) && (typeof ucookie.u == 'string')) { user = obj.users[ucookie.u]; }
1989
+ }
1990
+
1991
+ // If no token, see if we have an active session
1992
+ if ((user == null) && (req.session.userid != null)) { user = obj.users[req.session.userid]; }
1993
+
1994
+ // If still no user, see if we have a default user
1995
+ if ((user == null) && (obj.args.user)) { user = obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]; }
1996
+
1997
+ // No user login, exit now
1998
+ if (user == null) { res.sendStatus(401); return; }
1999
+
2000
+ // Check the nodeid
2001
+ if (req.query.node != null) {
2002
+ var nodeidsplit = req.query.node.split('/');
2003
+ if (nodeidsplit.length == 1) {
2004
+ req.query.node = 'node/' + domain.id + '/' + nodeidsplit[0]; // Format the nodeid correctly
2005
+ } else if (nodeidsplit.length == 3) {
2006
+ if ((nodeidsplit[0] != 'node') || (nodeidsplit[1] != domain.id)) { req.query.node = null; } // Check the nodeid format
2007
+ } else {
2008
+ req.query.node = null; // Bad nodeid
2009
+ }
2010
+ }
2011
+
2012
+ // If there is no nodeid, exit now
2013
+ if (req.query.node == null) { render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: '', name: '', features: features }, req, domain)); return; }
2014
+
2015
+ // Fetch the node from the database
2016
+ obj.db.Get(req.query.node, function (err, nodes) {
2017
+ if ((err != null) || (nodes.length != 1)) { res.sendStatus(404); return; }
2018
+ const node = nodes[0];
2019
+
2020
+ // Check access rights, must have remote control rights
2021
+ if ((obj.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0) { res.sendStatus(401); return; }
2022
+
2023
+ // Figure out the target port
2024
+ var port = 0, serverCredentials = false;
2025
+ if (page == 'ssh') {
2026
+ // SSH port
2027
+ port = 22;
2028
+ if (typeof node.sshport == 'number') { port = node.sshport; }
2029
+
2030
+ // Check if we have SSH credentials for this device
2031
+ if (domain.allowsavingdevicecredentials !== false) { serverCredentials = ((typeof node.ssh == 'object') && (typeof node.ssh.u == 'string')); }
2032
+ } else {
2033
+ // RDP port
2034
+ port = 3389;
2035
+ if (typeof node.rdpport == 'number') { port = node.rdpport; }
2036
+
2037
+ // Check if we have RDP credentials for this device
2038
+ if (domain.allowsavingdevicecredentials !== false) { serverCredentials = ((typeof node.rdp == 'object') && (typeof node.rdp.d == 'string') && (typeof node.rdp.u == 'string') && (typeof node.rdp.p == 'string')); }
2039
+ }
2040
+ 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; } }
2041
+
2042
+ // Generate a cookie and respond
2043
+ var cookie = parent.encodeCookie({ userid: user._id, domainid: user.domain, nodeid: node._id, tcpport: port }, parent.loginCookieEncryptionKey);
2044
+ render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: cookie, name: encodeURIComponent(node.name).replace(/'/g, '%27'), serverCredentials: serverCredentials, features: features }, req, domain));
2045
+ });
2046
+ }
2047
+
2048
+ // Called to handle push-only requests
2049
+ function handleFirebasePushOnlyRelayRequest(req, res) {
2050
+ parent.debug('email', 'handleFirebasePushOnlyRelayRequest');
2051
+ if ((req.body == null) || (req.body.msg == null) || (obj.parent.firebase == null)) { res.sendStatus(404); return; }
2052
+ if (obj.parent.config.firebase.pushrelayserver == null) { res.sendStatus(404); return; }
2053
+ if ((typeof obj.parent.config.firebase.pushrelayserver == 'string') && (req.query.key != obj.parent.config.firebase.pushrelayserver)) { res.sendStatus(404); return; }
2054
+ var data = null;
2055
+ try { data = JSON.parse(req.body.msg) } catch (ex) { res.sendStatus(404); return; }
2056
+ if (typeof data != 'object') { res.sendStatus(404); return; }
2057
+ if (typeof data.pmt != 'string') { res.sendStatus(404); return; }
2058
+ if (typeof data.payload != 'object') { res.sendStatus(404); return; }
2059
+ if (typeof data.payload.notification != 'object') { res.sendStatus(404); return; }
2060
+ if (typeof data.payload.notification.title != 'string') { res.sendStatus(404); return; }
2061
+ if (typeof data.payload.notification.body != 'string') { res.sendStatus(404); return; }
2062
+ if (typeof data.options != 'object') { res.sendStatus(404); return; }
2063
+ if ((data.options.priority != 'Normal') && (data.options.priority != 'High')) { res.sendStatus(404); return; }
2064
+ if ((typeof data.options.timeToLive != 'number') || (data.options.timeToLive < 1)) { res.sendStatus(404); return; }
2065
+ parent.debug('email', 'handleFirebasePushOnlyRelayRequest - ok');
2066
+ obj.parent.firebase.sendToDevice({ pmt: data.pmt }, data.payload, data.options, function (id, err, errdesc) {
2067
+ if (err == null) { res.sendStatus(200); } else { res.sendStatus(500); }
2068
+ });
2069
+ }
2070
+
2071
+ // Called to handle two-way push notification relay request
2072
+ function handleFirebaseRelayRequest(ws, req) {
2073
+ parent.debug('email', 'handleFirebaseRelayRequest');
2074
+ if (obj.parent.firebase == null) { try { ws.close(); } catch (e) { } return; }
2075
+ if (obj.parent.firebase.setupRelay == null) { try { ws.close(); } catch (e) { } return; }
2076
+ if (obj.parent.config.firebase.relayserver == null) { try { ws.close(); } catch (e) { } return; }
2077
+ if ((typeof obj.parent.config.firebase.relayserver == 'string') && (req.query.key != obj.parent.config.firebase.relayserver)) { res.sendStatus(404); try { ws.close(); } catch (e) { } return; }
2078
+ obj.parent.firebase.setupRelay(ws);
2079
+ }
2080
+
2081
+ // Called to process an agent invite request
2082
+ function handleAgentInviteRequest(req, res) {
2083
+ const domain = getDomain(req);
2084
+ if ((domain == null) || ((req.query.m == null) && (req.query.c == null))) { parent.debug('web', 'handleAgentInviteRequest: failed checks.'); res.sendStatus(404); return; }
2085
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2086
+
2087
+ if (req.query.c != null) {
2088
+ // A cookie is specified in the query string, use that
2089
+ var cookie = obj.parent.decodeCookie(req.query.c, obj.parent.invitationLinkEncryptionKey);
2090
+ if (cookie == null) { res.sendStatus(404); return; }
2091
+ var mesh = obj.meshes[cookie.mid];
2092
+ if (mesh == null) { res.sendStatus(404); return; }
2093
+ var installflags = cookie.f;
2094
+ if (typeof installflags != 'number') { installflags = 0; }
2095
+ var showagents = cookie.ag;
2096
+ if (typeof showagents != 'number') { showagents = 0; }
2097
+ parent.debug('web', 'handleAgentInviteRequest using cookie.');
2098
+
2099
+ // Build the mobile agent URL, this is used to connect mobile devices
2100
+ var agentServerName = obj.getWebServerName(domain);
2101
+ if (typeof obj.args.agentaliasdns == 'string') { agentServerName = obj.args.agentaliasdns; }
2102
+ var xdomain = (domain.dns == null) ? domain.id : '';
2103
+ var agentHttpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
2104
+ if (obj.args.agentport != null) { agentHttpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
2105
+ if (obj.args.agentaliasport != null) { agentHttpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
2106
+ var magenturl = 'mc://' + agentServerName + ((agentHttpsPort != 443) ? (':' + agentHttpsPort) : '') + ((xdomain != '') ? ('/' + xdomain) : '') + ',' + obj.agentCertificateHashBase64 + ',' + mesh._id.split('/')[2];
2107
+
2108
+ var meshcookie = parent.encodeCookie({ m: mesh._id.split('/')[2] }, parent.invitationLinkEncryptionKey);
2109
+ 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, showagents: showagents, magenturl: magenturl }, req, domain));
2110
+ } else if (req.query.m != null) {
2111
+ // The MeshId is specified in the query string, use that
2112
+ var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.m.toLowerCase()];
2113
+ if (mesh == null) { res.sendStatus(404); return; }
2114
+ var installflags = 0;
2115
+ if (req.query.f) { installflags = parseInt(req.query.f); }
2116
+ if (typeof installflags != 'number') { installflags = 0; }
2117
+ var showagents = 0;
2118
+ if (req.query.f) { showagents = parseInt(req.query.ag); }
2119
+ if (typeof showagents != 'number') { showagents = 0; }
2120
+ parent.debug('web', 'handleAgentInviteRequest using meshid.');
2121
+
2122
+ // Build the mobile agent URL, this is used to connect mobile devices
2123
+ var agentServerName = obj.getWebServerName(domain);
2124
+ if (typeof obj.args.agentaliasdns == 'string') { agentServerName = obj.args.agentaliasdns; }
2125
+ var xdomain = (domain.dns == null) ? domain.id : '';
2126
+ var agentHttpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
2127
+ if (obj.args.agentport != null) { agentHttpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
2128
+ if (obj.args.agentaliasport != null) { agentHttpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
2129
+ var magenturl = 'mc://' + agentServerName + ((agentHttpsPort != 443) ? (':' + agentHttpsPort) : '') + ((xdomain != '') ? ('/' + xdomain) : '') + ',' + obj.agentCertificateHashBase64 + ',' + mesh._id.split('/')[2];
2130
+
2131
+ var meshcookie = parent.encodeCookie({ m: mesh._id.split('/')[2] }, parent.invitationLinkEncryptionKey);
2132
+ 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, showagents: showagents, magenturl: magenturl }, req, domain));
2133
+ }
2134
+ }
2135
+
2136
+ // Called to process an agent invite request
2137
+ function handleUserImageRequest(req, res) {
2138
+ const domain = getDomain(req);
2139
+ if (domain == null) { parent.debug('web', 'handleUserImageRequest: failed checks.'); res.sendStatus(404); return; }
2140
+ if ((req.session == null) || (req.session.userid == null)) { parent.debug('web', 'handleUserImageRequest: failed checks 2.'); res.sendStatus(404); return; }
2141
+ var imageUserId = req.session.userid;
2142
+ if ((req.query.id != null)) {
2143
+ var user = obj.users[req.session.userid];
2144
+ if ((user == null) || (user.siteadmin == null) && ((user.siteadmin & 2) == 0)) { res.sendStatus(404); return; }
2145
+ imageUserId = 'user/' + domain.id + '/' + req.query.id;
2146
+ }
2147
+ obj.db.Get('im' + imageUserId, function (err, docs) {
2148
+ if ((err != null) || (docs == null) || (docs.length != 1) || (typeof docs[0].image != 'string')) { res.sendStatus(404); return; }
2149
+ var imagebase64 = docs[0].image;
2150
+ if (imagebase64.startsWith('data:image/png;base64,')) {
2151
+ res.set('Content-Type', 'image/png');
2152
+ res.set({ 'Cache-Control': 'no-store' });
2153
+ res.send(Buffer.from(imagebase64.substring(22), 'base64'));
2154
+ } else if (imagebase64.startsWith('data:image/jpeg;base64,')) {
2155
+ res.set('Content-Type', 'image/jpeg');
2156
+ res.set({ 'Cache-Control': 'no-store' });
2157
+ res.send(Buffer.from(imagebase64.substring(23), 'base64'));
2158
+ } else {
2159
+ res.sendStatus(404);
2160
+ }
2161
+ });
2162
+ }
2163
+
2164
+ function handleDeleteAccountRequest(req, res, direct) {
2165
+ parent.debug('web', 'handleDeleteAccountRequest()');
2166
+ const domain = checkUserIpAddress(req, res);
2167
+ if (domain == null) { return; }
2168
+ if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handleDeleteAccountRequest: failed checks.'); res.sendStatus(404); return; }
2169
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2170
+ if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
2171
+
2172
+ var user = null;
2173
+ if (req.body.authcookie) {
2174
+ // If a authentication cookie is provided, decode it here
2175
+ var loginCookie = obj.parent.decodeCookie(req.body.authcookie, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
2176
+ if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { user = obj.users[loginCookie.userid]; }
2177
+ } else {
2178
+ // Check if the user is logged and we have all required parameters
2179
+ if (!req.session || !req.session.userid || !req.body.apassword1 || (req.body.apassword1 != req.body.apassword2) || (req.session.userid.split('/')[1] != domain.id)) {
2180
+ parent.debug('web', 'handleDeleteAccountRequest: required parameters not present.');
2181
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2182
+ return;
2183
+ } else {
2184
+ user = obj.users[req.session.userid];
2185
+ }
2186
+ }
2187
+ if (!user) { parent.debug('web', 'handleDeleteAccountRequest: user not found.'); res.sendStatus(404); return; }
2188
+ if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) { parent.debug('web', 'handleDeleteAccountRequest: account settings locked.'); res.sendStatus(404); return; }
2189
+
2190
+ // Check if the password is correct
2191
+ obj.authenticate(user._id.split('/')[2], req.body.apassword1, domain, function (err, userid, passhint, loginOptions) {
2192
+ var deluser = obj.users[userid];
2193
+ if ((userid != null) && (deluser != null)) {
2194
+ // Remove all links to this user
2195
+ if (deluser.links != null) {
2196
+ for (var i in deluser.links) {
2197
+ if (i.startsWith('mesh/')) {
2198
+ // Get the device group
2199
+ var mesh = obj.meshes[i];
2200
+ if (mesh) {
2201
+ // Remove user from the mesh
2202
+ if (mesh.links[deluser._id] != null) { delete mesh.links[deluser._id]; parent.db.Set(mesh); }
2203
+
2204
+ // Notify mesh change
2205
+ var change = 'Removed user ' + deluser.name + ' from group ' + mesh.name;
2206
+ 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 };
2207
+ 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.
2208
+ parent.DispatchEvent(['*', mesh._id, deluser._id, user._id], obj, event);
2209
+ }
2210
+ } else if (i.startsWith('node/')) {
2211
+ // Get the node and the rights for this node
2212
+ obj.GetNodeWithRights(domain, deluser, i, function (node, rights, visible) {
2213
+ if ((node == null) || (node.links == null) || (node.links[deluser._id] == null)) return;
2214
+
2215
+ // Remove the link and save the node to the database
2216
+ delete node.links[deluser._id];
2217
+ if (Object.keys(node.links).length == 0) { delete node.links; }
2218
+ db.Set(obj.cleanDevice(node));
2219
+
2220
+ // Event the node change
2221
+ 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) }
2222
+ 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.
2223
+ parent.DispatchEvent(['*', node.meshid, node._id], obj, event);
2224
+ });
2225
+ } else if (i.startsWith('ugrp/')) {
2226
+ // Get the device group
2227
+ var ugroup = obj.userGroups[i];
2228
+ if (ugroup) {
2229
+ // Remove user from the user group
2230
+ if (ugroup.links[deluser._id] != null) { delete ugroup.links[deluser._id]; parent.db.Set(ugroup); }
2231
+
2232
+ // Notify user group change
2233
+ var change = 'Removed user ' + deluser.name + ' from user group ' + ugroup.name;
2234
+ 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 };
2235
+ 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.
2236
+ parent.DispatchEvent(['*', ugroup._id, user._id, deluser._id], obj, event);
2237
+ }
2238
+ }
2239
+ }
2240
+ }
2241
+
2242
+ obj.db.Remove('ws' + deluser._id); // Remove user web state
2243
+ obj.db.Remove('nt' + deluser._id); // Remove notes for this user
2244
+ obj.db.Remove('ntp' + deluser._id); // Remove personal notes for this user
2245
+ obj.db.Remove('im' + deluser._id); // Remove image for this user
2246
+
2247
+ // Delete any login tokens
2248
+ parent.db.GetAllTypeNodeFiltered(['logintoken-' + deluser._id], domain.id, 'logintoken', null, function (err, docs) {
2249
+ if ((err == null) && (docs != null)) { for (var i = 0; i < docs.length; i++) { parent.db.Remove(docs[i]._id, function () { }); } }
2250
+ });
2251
+
2252
+ // Delete all files on the server for this account
2253
+ try {
2254
+ var deluserpath = obj.getServerRootFilePath(deluser);
2255
+ if (deluserpath != null) { obj.deleteFolderRec(deluserpath); }
2256
+ } catch (e) { }
2257
+
2258
+ // Remove the user
2259
+ obj.db.Remove(deluser._id);
2260
+ delete obj.users[deluser._id];
2261
+ req.session = null;
2262
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2263
+ obj.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', userid: deluser._id, username: deluser.name, action: 'accountremove', msg: 'Account removed', domain: domain.id });
2264
+ parent.debug('web', 'handleDeleteAccountRequest: removed user.');
2265
+ } else {
2266
+ parent.debug('web', 'handleDeleteAccountRequest: auth failed.');
2267
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2268
+ }
2269
+ });
2270
+ }
2271
+
2272
+ // Check a user's password
2273
+ obj.checkUserPassword = function (domain, user, password, func) {
2274
+ // Check the old password
2275
+ if (user.passtype != null) {
2276
+ // IIS default clear or weak password hashing (SHA-1)
2277
+ require('./pass').iishash(user.passtype, password, user.salt, function (err, hash) {
2278
+ if (err) { parent.debug('web', 'checkUserPassword: SHA-1 fail.'); return func(false); }
2279
+ if (hash == user.hash) {
2280
+ if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { parent.debug('web', 'checkUserPassword: SHA-1 locked.'); return func(false); } // Account is locked
2281
+ parent.debug('web', 'checkUserPassword: SHA-1 ok.');
2282
+ return func(true); // Allow password change
2283
+ }
2284
+ func(false);
2285
+ });
2286
+ } else {
2287
+ // Default strong password hashing (pbkdf2 SHA384)
2288
+ require('./pass').hash(password, user.salt, function (err, hash, tag) {
2289
+ if (err) { parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 fail.'); return func(false); }
2290
+ if (hash == user.hash) {
2291
+ if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 locked.'); return func(false); } // Account is locked
2292
+ parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 ok.');
2293
+ return func(true); // Allow password change
2294
+ }
2295
+ func(false);
2296
+ }, 0);
2297
+ }
2298
+ }
2299
+
2300
+ // Check a user's old passwords
2301
+ // Callback: 0=OK, 1=OldPass, 2=CommonPass
2302
+ obj.checkOldUserPasswords = function (domain, user, password, func) {
2303
+ // Check how many old passwords we need to check
2304
+ if ((domain.passwordrequirements != null) && (typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
2305
+ if (user.oldpasswords != null) {
2306
+ const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
2307
+ if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
2308
+ }
2309
+ } else {
2310
+ delete user.oldpasswords;
2311
+ }
2312
+
2313
+ // If there is no old passwords, exit now.
2314
+ var oldPassCount = 1;
2315
+ if (user.oldpasswords != null) { oldPassCount += user.oldpasswords.length; }
2316
+ var oldPassCheckState = { response: 0, count: oldPassCount, user: user, func: func };
2317
+
2318
+ // Test against common passwords if this feature is enabled
2319
+ // Example of common passwords: 123456789, password123
2320
+ if ((domain.passwordrequirements != null) && (domain.passwordrequirements.bancommonpasswords == true)) {
2321
+ oldPassCheckState.count++;
2322
+ require('wildleek')(password).then(function (wild) {
2323
+ if (wild == true) { oldPassCheckState.response = 2; }
2324
+ if (--oldPassCheckState.count == 0) { oldPassCheckState.func(oldPassCheckState.response); }
2325
+ });
2326
+ }
2327
+
2328
+ // Try current password
2329
+ require('./pass').hash(password, user.salt, function oldPassCheck(err, hash, tag) {
2330
+ if ((err == null) && (hash == tag.user.hash)) { tag.response = 1; }
2331
+ if (--tag.count == 0) { tag.func(tag.response); }
2332
+ }, oldPassCheckState);
2333
+
2334
+ // Try each old password
2335
+ if (user.oldpasswords != null) {
2336
+ for (var i in user.oldpasswords) {
2337
+ const oldpassword = user.oldpasswords[i];
2338
+ // Default strong password hashing (pbkdf2 SHA384)
2339
+ require('./pass').hash(password, oldpassword.salt, function oldPassCheck(err, hash, tag) {
2340
+ if ((err == null) && (hash == tag.oldPassword.hash)) { tag.state.response = 1; }
2341
+ if (--tag.state.count == 0) { tag.state.func(tag.state.response); }
2342
+ }, { oldPassword: oldpassword, state: oldPassCheckState });
2343
+ }
2344
+ }
2345
+ }
2346
+
2347
+ // Handle password changes
2348
+ function handlePasswordChangeRequest(req, res, direct) {
2349
+ const domain = checkUserIpAddress(req, res);
2350
+ if (domain == null) { return; }
2351
+ if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handlePasswordChangeRequest: failed checks (1).'); res.sendStatus(404); return; }
2352
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2353
+ if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
2354
+
2355
+ // Check if the user is logged and we have all required parameters
2356
+ if (!req.session || !req.session.userid || !req.body.apassword0 || !req.body.apassword1 || (req.body.apassword1 != req.body.apassword2) || (req.session.userid.split('/')[1] != domain.id)) {
2357
+ parent.debug('web', 'handlePasswordChangeRequest: failed checks (2).');
2358
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2359
+ return;
2360
+ }
2361
+
2362
+ // Get the current user
2363
+ var user = obj.users[req.session.userid];
2364
+ if (!user) {
2365
+ parent.debug('web', 'handlePasswordChangeRequest: user not found.');
2366
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2367
+ return;
2368
+ }
2369
+
2370
+ // Check account settings locked
2371
+ if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) {
2372
+ parent.debug('web', 'handlePasswordChangeRequest: account settings locked.');
2373
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2374
+ return;
2375
+ }
2376
+
2377
+ // Check old password
2378
+ obj.checkUserPassword(domain, user, req.body.apassword1, function (result) {
2379
+ if (result == true) {
2380
+ // Check if the new password is allowed, only do this if this feature is enabled.
2381
+ parent.checkOldUserPasswords(domain, user, command.newpass, function (result) {
2382
+ if (result == 1) {
2383
+ parent.debug('web', 'handlePasswordChangeRequest: old password reuse attempt.');
2384
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2385
+ } else if (result == 2) {
2386
+ parent.debug('web', 'handlePasswordChangeRequest: commonly used password use attempt.');
2387
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2388
+ } else {
2389
+ // Update the password
2390
+ require('./pass').hash(req.body.apassword1, function (err, salt, hash, tag) {
2391
+ const nowSeconds = Math.floor(Date.now() / 1000);
2392
+ if (err) { parent.debug('web', 'handlePasswordChangeRequest: hash error.'); throw err; }
2393
+ if (domain.passwordrequirements != null) {
2394
+ // Save password hint if this feature is enabled
2395
+ 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; }
2396
+
2397
+ // Save previous password if this feature is enabled
2398
+ if ((typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
2399
+ if (user.oldpasswords == null) { user.oldpasswords = []; }
2400
+ user.oldpasswords.push({ salt: user.salt, hash: user.hash, start: user.passchange, end: nowSeconds });
2401
+ const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
2402
+ if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
2403
+ }
2404
+ }
2405
+ user.salt = salt;
2406
+ user.hash = hash;
2407
+ user.passchange = user.access = nowSeconds;
2408
+ delete user.passtype;
2409
+
2410
+ obj.db.SetUser(user);
2411
+ req.session.viewmode = 2;
2412
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2413
+ 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 });
2414
+ }, 0);
2415
+ }
2416
+ });
2417
+ }
2418
+ });
2419
+ }
2420
+
2421
+ // Called when a strategy login occured
2422
+ // This is called after a succesful Oauth to Twitter, Google, GitHub...
2423
+ function handleStrategyLogin(req, res) {
2424
+ const domain = checkUserIpAddress(req, res);
2425
+ if (domain == null) { return; }
2426
+ parent.debug('web', 'handleStrategyLogin: ' + JSON.stringify(req.user));
2427
+ if ((req.user != null) && (req.user.sid != null)) {
2428
+ const userid = 'user/' + domain.id + '/' + req.user.sid;
2429
+ var user = obj.users[userid];
2430
+ if (user == null) {
2431
+ var newAccountAllowed = false;
2432
+ var newAccountRealms = null;
2433
+
2434
+ if (domain.newaccounts === true) { newAccountAllowed = true; }
2435
+ if (obj.common.validateStrArray(domain.newaccountrealms)) { newAccountRealms = domain.newaccountrealms; }
2436
+
2437
+ if ((domain.authstrategies != null) && (domain.authstrategies[req.user.strategy] != null)) {
2438
+ if (domain.authstrategies[req.user.strategy].newaccounts === true) { newAccountAllowed = true; }
2439
+ if (obj.common.validateStrArray(domain.authstrategies[req.user.strategy].newaccountrealms)) { newAccountRealms = domain.authstrategies[req.user.strategy].newaccountrealms; }
2440
+ }
2441
+
2442
+ if (newAccountAllowed === true) {
2443
+ // Create the user
2444
+ parent.debug('web', 'handleStrategyLogin: creating new user: ' + userid);
2445
+ user = { type: 'user', _id: userid, name: req.user.name, email: req.user.email, creation: Math.floor(Date.now() / 1000), login: Math.floor(Date.now() / 1000), access: Math.floor(Date.now() / 1000), domain: domain.id };
2446
+ if (req.user.email != null) { user.email = req.user.email; user.emailVerified = true; }
2447
+ if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; } // New accounts automatically assigned server rights.
2448
+ 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.
2449
+ if (newAccountRealms) { user.groups = newAccountRealms; } // New accounts automatically part of some groups (Realms).
2450
+ obj.users[userid] = user;
2451
+
2452
+ // Auto-join any user groups
2453
+ var newaccountsusergroups = null;
2454
+ if (typeof domain.newaccountsusergroups == 'object') { newaccountsusergroups = domain.newaccountsusergroups; }
2455
+ if (typeof domain.authstrategies[req.user.strategy].newaccountsusergroups == 'object') { newaccountsusergroups = domain.authstrategies[req.user.strategy].newaccountsusergroups; }
2456
+ if (newaccountsusergroups) {
2457
+ for (var i in newaccountsusergroups) {
2458
+ var ugrpid = newaccountsusergroups[i];
2459
+ if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
2460
+ var ugroup = obj.userGroups[ugrpid];
2461
+ if (ugroup != null) {
2462
+ // Add group to the user
2463
+ if (user.links == null) { user.links = {}; }
2464
+ user.links[ugroup._id] = { rights: 1 };
2465
+
2466
+ // Add user to the group
2467
+ ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
2468
+ db.Set(ugroup);
2469
+
2470
+ // Notify user group change
2471
+ 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 };
2472
+ 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.
2473
+ parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
2474
+ }
2475
+ }
2476
+ }
2477
+
2478
+ // Save the user
2479
+ obj.db.SetUser(user);
2480
+
2481
+ // Event user creation
2482
+ var targets = ['*', 'server-users'];
2483
+ 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 };
2484
+ 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.
2485
+ parent.DispatchEvent(targets, obj, event);
2486
+
2487
+ req.session.userid = userid;
2488
+ setSessionRandom(req);
2489
+
2490
+ // Notify account login using SSO
2491
+ var targets = ['*', 'server-users', user._id];
2492
+ if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
2493
+ const ua = getUserAgentInfo(req);
2494
+ const loginEvent = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'login', msgid: 107, msgArgs: [req.clientIp, ua.browserStr, ua.osStr], msg: 'Account login', domain: domain.id, ip: req.clientIp, userAgent: req.headers['user-agent'], twoFactorType: 'sso' };
2495
+ obj.parent.DispatchEvent(targets, obj, loginEvent);
2496
+ } else {
2497
+ // New users not allowed
2498
+ parent.debug('web', 'handleStrategyLogin: Can\'t create new accounts');
2499
+ req.session.loginmode = 1;
2500
+ req.session.messageid = 100; // Unable to create account.
2501
+ res.redirect(domain.url + getQueryPortion(req));
2502
+ return;
2503
+ }
2504
+ } else {
2505
+ // Login success
2506
+ var userChange = false;
2507
+ if ((req.user.name != null) && (req.user.name != user.name)) { user.name = req.user.name; userChange = true; }
2508
+ if ((req.user.email != null) && (req.user.email != user.email)) { user.email = req.user.email; user.emailVerified = true; userChange = true; }
2509
+ if (userChange) {
2510
+ obj.db.SetUser(user);
2511
+
2512
+ // Event user change
2513
+ var targets = ['*', 'server-users'];
2514
+ var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Account changed', domain: domain.id };
2515
+ 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.
2516
+ parent.DispatchEvent(targets, obj, event);
2517
+ }
2518
+ parent.debug('web', 'handleStrategyLogin: succesful login: ' + userid);
2519
+ req.session.userid = userid;
2520
+ setSessionRandom(req);
2521
+
2522
+ // Notify account login using SSO
2523
+ var targets = ['*', 'server-users', user._id];
2524
+ if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
2525
+ const ua = getUserAgentInfo(req);
2526
+ const loginEvent = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'login', msgid: 107, msgArgs: [req.clientIp, ua.browserStr, ua.osStr], msg: 'Account login', domain: domain.id, ip: req.clientIp, userAgent: req.headers['user-agent'], twoFactorType: 'sso' };
2527
+ obj.parent.DispatchEvent(targets, obj, loginEvent);
2528
+ }
2529
+ }
2530
+ //res.redirect(domain.url); // This does not handle cookie correctly.
2531
+ res.set('Content-Type', 'text/html');
2532
+ res.end('<html><head><meta http-equiv="refresh" content=0;url="' + domain.url + '"></head><body></body></html>');
2533
+ }
2534
+
2535
+ // Indicates that any request to "/" should render "default" or "login" depending on login state
2536
+ function handleRootRequest(req, res, direct) {
2537
+ const domain = checkUserIpAddress(req, res);
2538
+ if (domain == null) { return; }
2539
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2540
+ if (!obj.args) { parent.debug('web', 'handleRootRequest: no obj.args.'); res.sendStatus(500); return; }
2541
+
2542
+ // If the session is expired, clear it.
2543
+ if ((req.session != null) && (typeof req.session.expire == 'number') && ((req.session.expire - Date.now()) <= 0)) { for (var i in req.session) { delete req.session[i]; } }
2544
+
2545
+ // Check if we are in maintenance mode
2546
+ if ((parent.config.settings.maintenancemode != null) && (req.query.loginscreen !== '1')) {
2547
+ parent.debug('web', 'handleLoginRequest: Server under maintenance.');
2548
+ 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));
2549
+ return;
2550
+ }
2551
+
2552
+ // If set and there is no user logged in, redirect the root page. Make sure not to redirect if /login is used
2553
+ if ((typeof domain.unknownuserrootredirect == 'string') && ((req.session == null) || (req.session.userid == null))) {
2554
+ var q = require('url').parse(req.url, true);
2555
+ if (!q.pathname.endsWith('/login')) { res.redirect(domain.unknownuserrootredirect + getQueryPortion(req)); return; }
2556
+ }
2557
+
2558
+ if ((domain.sspi != null) && ((req.query.login == null) || (obj.parent.loginCookieEncryptionKey == null))) {
2559
+ // Login using SSPI
2560
+ domain.sspi.authenticate(req, res, function (err) {
2561
+ if ((err != null) || (req.connection.user == null)) {
2562
+ if (obj.parent.authlog) { obj.parent.authLog('https', 'Failed SSPI-auth for ' + req.connection.user + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); }
2563
+ parent.debug('web', 'handleRootRequest: SSPI auth required.');
2564
+ res.end('Authentication Required...');
2565
+ } else {
2566
+ if (obj.parent.authlog) { obj.parent.authLog('https', 'Accepted SSPI-auth for ' + req.connection.user + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); }
2567
+ parent.debug('web', 'handleRootRequest: SSPI auth ok.');
2568
+ handleRootRequestEx(req, res, domain, direct);
2569
+ }
2570
+ });
2571
+ } else if (req.query.user && req.query.pass) {
2572
+ // User credentials are being passed in the URL. WARNING: Putting credentials in a URL is bad security... but people are requesting this option.
2573
+ obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid, passhint, loginOptions) {
2574
+ if (obj.parent.authlog) { obj.parent.authLog('https', 'Accepted password for ' + userid + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); }
2575
+ parent.debug('web', 'handleRootRequest: user/pass in URL auth ok.');
2576
+ req.session.userid = userid;
2577
+ delete req.session.currentNode;
2578
+ req.session.ip = req.clientIp; // Bind this session to the IP address of the request
2579
+ setSessionRandom(req);
2580
+ handleRootRequestEx(req, res, domain, direct);
2581
+ });
2582
+ } else if ((req.session != null) && (typeof req.session.loginToken == 'string')) {
2583
+ // Check if the loginToken is still valid
2584
+ obj.db.Get('logintoken-' + req.session.loginToken, function (err, docs) {
2585
+ if ((err != null) || (docs == null) || (docs.length != 1) || (docs[0].tokenUser != req.session.loginToken)) { for (var i in req.session) { delete req.session[i]; } }
2586
+ handleRootRequestEx(req, res, domain, direct); // Login using a different system
2587
+ });
2588
+ } else {
2589
+ // Login using a different system
2590
+ handleRootRequestEx(req, res, domain, direct);
2591
+ }
2592
+ }
2593
+
2594
+ function handleRootRequestEx(req, res, domain, direct) {
2595
+ var nologout = false, user = null;
2596
+ res.set({ 'Cache-Control': 'no-store' });
2597
+
2598
+ // Check if we have an incomplete domain name in the path
2599
+ if ((domain.id != '') && (domain.dns == null) && (req.url.split('/').length == 2)) {
2600
+ parent.debug('web', 'handleRootRequestEx: incomplete domain name in the path.');
2601
+ res.redirect(domain.url + getQueryPortion(req)); // BAD***
2602
+ return;
2603
+ }
2604
+
2605
+ if (obj.args.nousers == true) {
2606
+ // If in single user mode, setup things here.
2607
+ delete req.session.loginmode;
2608
+ req.session.userid = 'user/' + domain.id + '/~';
2609
+ delete req.session.currentNode;
2610
+ req.session.ip = req.clientIp; // Bind this session to the IP address of the request
2611
+ setSessionRandom(req);
2612
+ if (obj.users[req.session.userid] == null) {
2613
+ // Create the dummy user ~ with impossible password
2614
+ parent.debug('web', 'handleRootRequestEx: created dummy user in nouser mode.');
2615
+ obj.users[req.session.userid] = { type: 'user', _id: req.session.userid, name: '~', email: '~', domain: domain.id, siteadmin: 4294967295 };
2616
+ obj.db.SetUser(obj.users[req.session.userid]);
2617
+ }
2618
+ } else if (obj.args.user && obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]) {
2619
+ // If a default user is active, setup the session here.
2620
+ parent.debug('web', 'handleRootRequestEx: auth using default user.');
2621
+ delete req.session.loginmode;
2622
+ req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase();
2623
+ delete req.session.currentNode;
2624
+ req.session.ip = req.clientIp; // Bind this session to the IP address of the request
2625
+ setSessionRandom(req);
2626
+ } else if (req.query.login && (obj.parent.loginCookieEncryptionKey != null)) {
2627
+ var loginCookie = obj.parent.decodeCookie(req.query.login, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
2628
+ //if ((loginCookie != null) && (loginCookie.ip != null) && checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // If the cookie if binded to an IP address, check here.
2629
+ if ((loginCookie != null) && (loginCookie.a == 3) && (loginCookie.u != null) && (loginCookie.u.split('/')[1] == domain.id)) {
2630
+ // If a login cookie was provided, setup the session here.
2631
+ parent.debug('web', 'handleRootRequestEx: cookie auth ok.');
2632
+ delete req.session.loginmode;
2633
+ req.session.userid = loginCookie.u;
2634
+ delete req.session.currentNode;
2635
+ req.session.ip = req.clientIp; // Bind this session to the IP address of the request
2636
+ setSessionRandom(req);
2637
+ } else {
2638
+ parent.debug('web', 'handleRootRequestEx: cookie auth failed.');
2639
+ }
2640
+ } else if (domain.sspi != null) {
2641
+ // SSPI login (Windows only)
2642
+ //console.log(req.connection.user, req.connection.userSid);
2643
+ if ((req.connection.user == null) || (req.connection.userSid == null)) {
2644
+ parent.debug('web', 'handleRootRequestEx: SSPI no user auth.');
2645
+ res.sendStatus(404); return;
2646
+ } else {
2647
+ nologout = true;
2648
+ req.session.userid = 'user/' + domain.id + '/' + req.connection.user.toLowerCase();
2649
+ req.session.usersid = req.connection.userSid;
2650
+ req.session.usersGroups = req.connection.userGroups;
2651
+ delete req.session.currentNode;
2652
+ req.session.ip = req.clientIp; // Bind this session to the IP address of the request
2653
+ setSessionRandom(req);
2654
+
2655
+ // Check if this user exists, create it if not.
2656
+ user = obj.users[req.session.userid];
2657
+ if ((user == null) || (user.sid != req.session.usersid)) {
2658
+ // Create the domain user
2659
+ 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), access: Math.floor(Date.now() / 1000) };
2660
+ if (domain.newaccountsrights) { user2.siteadmin = domain.newaccountsrights; }
2661
+ if (obj.common.validateStrArray(domain.newaccountrealms)) { user2.groups = domain.newaccountrealms; }
2662
+ for (var i in obj.users) { if (obj.users[i].domain == domain.id) { usercount++; } }
2663
+ if (usercount == 0) { user2.siteadmin = 4294967295; } // If this is the first user, give the account site admin.
2664
+
2665
+ // Auto-join any user groups
2666
+ if (typeof domain.newaccountsusergroups == 'object') {
2667
+ for (var i in domain.newaccountsusergroups) {
2668
+ var ugrpid = domain.newaccountsusergroups[i];
2669
+ if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
2670
+ var ugroup = obj.userGroups[ugrpid];
2671
+ if (ugroup != null) {
2672
+ // Add group to the user
2673
+ if (user2.links == null) { user2.links = {}; }
2674
+ user2.links[ugroup._id] = { rights: 1 };
2675
+
2676
+ // Add user to the group
2677
+ ugroup.links[user2._id] = { userid: user2._id, name: user2.name, rights: 1 };
2678
+ db.Set(ugroup);
2679
+
2680
+ // Notify user group change
2681
+ 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 };
2682
+ 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.
2683
+ parent.DispatchEvent(['*', ugroup._id, user2._id], obj, event);
2684
+ }
2685
+ }
2686
+ }
2687
+
2688
+ obj.users[req.session.userid] = user2;
2689
+ obj.db.SetUser(user2);
2690
+ 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 };
2691
+ 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.
2692
+ obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
2693
+ parent.debug('web', 'handleRootRequestEx: SSPI new domain user.');
2694
+ }
2695
+ }
2696
+ }
2697
+
2698
+ // Figure out the minimal password requirement
2699
+ var passRequirements = null;
2700
+ if (domain.passwordrequirements != null) {
2701
+ if (domain.passrequirementstr == null) {
2702
+ var passRequirements = {};
2703
+ if (typeof domain.passwordrequirements.min == 'number') { passRequirements.min = domain.passwordrequirements.min; }
2704
+ if (typeof domain.passwordrequirements.max == 'number') { passRequirements.max = domain.passwordrequirements.max; }
2705
+ if (typeof domain.passwordrequirements.upper == 'number') { passRequirements.upper = domain.passwordrequirements.upper; }
2706
+ if (typeof domain.passwordrequirements.lower == 'number') { passRequirements.lower = domain.passwordrequirements.lower; }
2707
+ if (typeof domain.passwordrequirements.numeric == 'number') { passRequirements.numeric = domain.passwordrequirements.numeric; }
2708
+ if (typeof domain.passwordrequirements.nonalpha == 'number') { passRequirements.nonalpha = domain.passwordrequirements.nonalpha; }
2709
+ domain.passwordrequirementsstr = encodeURIComponent(JSON.stringify(passRequirements));
2710
+ }
2711
+ passRequirements = domain.passwordrequirementsstr;
2712
+ }
2713
+
2714
+ // If a user exists and is logged in, serve the default app, otherwise server the login app.
2715
+ if (req.session && req.session.userid && obj.users[req.session.userid]) {
2716
+ const user = obj.users[req.session.userid];
2717
+
2718
+ // Check if we are in maintenance mode
2719
+ if ((parent.config.settings.maintenancemode != null) && (user.siteadmin != 4294967295)) {
2720
+ req.session.messageid = 115; // Server under maintenance
2721
+ req.session.loginmode = 1;
2722
+ res.redirect(domain.url);
2723
+ return;
2724
+ }
2725
+
2726
+ // If the request has a "meshmessengerid", redirect to MeshMessenger
2727
+ // This situation happens when you get a push notification for a chat session, but are not logged in.
2728
+ if (req.query.meshmessengerid != null) {
2729
+ res.redirect(domain.url + 'messenger?id=' + req.query.meshmessengerid + ((req.query.key != null) ? ('&key=' + req.query.key) : ''));
2730
+ return;
2731
+ }
2732
+
2733
+ const xdbGetFunc = function dbGetFunc(err, states) {
2734
+ if (dbGetFunc.req.session.userid.split('/')[1] != domain.id) { // Check if the session is for the correct domain
2735
+ parent.debug('web', 'handleRootRequestEx: incorrect domain.');
2736
+ dbGetFunc.req.session = null;
2737
+ dbGetFunc.res.redirect(domain.url + getQueryPortion(dbGetFunc.req)); // BAD***
2738
+ return;
2739
+ }
2740
+
2741
+ // Check if this is a locked account
2742
+ if ((dbGetFunc.user.siteadmin != null) && ((dbGetFunc.user.siteadmin & 32) != 0) && (dbGetFunc.user.siteadmin != 0xFFFFFFFF)) {
2743
+ // Locked account
2744
+ parent.debug('web', 'handleRootRequestEx: locked account.');
2745
+ delete dbGetFunc.req.session.userid;
2746
+ delete dbGetFunc.req.session.currentNode;
2747
+ delete dbGetFunc.req.session.passhint;
2748
+ delete dbGetFunc.req.session.cuserid;
2749
+ dbGetFunc.req.session.messageid = 110; // Account locked.
2750
+ dbGetFunc.res.redirect(domain.url + getQueryPortion(dbGetFunc.req)); // BAD***
2751
+ return;
2752
+ }
2753
+
2754
+ var viewmode = 1;
2755
+ if (dbGetFunc.req.session.viewmode) {
2756
+ viewmode = dbGetFunc.req.session.viewmode;
2757
+ delete dbGetFunc.req.session.viewmode;
2758
+ } else if (dbGetFunc.req.query.viewmode) {
2759
+ viewmode = dbGetFunc.req.query.viewmode;
2760
+ }
2761
+ var currentNode = '';
2762
+ if (dbGetFunc.req.session.currentNode) {
2763
+ currentNode = dbGetFunc.req.session.currentNode;
2764
+ delete dbGetFunc.req.session.currentNode;
2765
+ } else if (dbGetFunc.req.query.node) {
2766
+ currentNode = 'node/' + domain.id + '/' + dbGetFunc.req.query.node;
2767
+ }
2768
+ var logoutcontrols = {};
2769
+ if (obj.args.nousers != true) { logoutcontrols.name = user.name; }
2770
+
2771
+ // Give the web page a list of supported server features for this domain and user
2772
+ const allFeatures = obj.getDomainUserFeatures(domain, dbGetFunc.user, dbGetFunc.req);
2773
+
2774
+ // Create a authentication cookie
2775
+ const authCookie = obj.parent.encodeCookie({ userid: dbGetFunc.user._id, domainid: domain.id, ip: req.clientIp }, obj.parent.loginCookieEncryptionKey);
2776
+ const authRelayCookie = obj.parent.encodeCookie({ ruserid: dbGetFunc.user._id, domainid: domain.id }, obj.parent.loginCookieEncryptionKey);
2777
+
2778
+ // Send the main web application
2779
+ var extras = (dbGetFunc.req.query.key != null) ? ('&key=' + dbGetFunc.req.query.key) : '';
2780
+ 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
2781
+ var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
2782
+
2783
+ // Clean up the U2F challenge if needed
2784
+ if (dbGetFunc.req.session.u2f) { delete dbGetFunc.req.session.u2f; };
2785
+
2786
+ // Intel AMT Scanning options
2787
+ var amtscanoptions = '';
2788
+ if (typeof domain.amtscanoptions == 'string') { amtscanoptions = encodeURIComponent(domain.amtscanoptions); }
2789
+ else if (obj.common.validateStrArray(domain.amtscanoptions)) { domain.amtscanoptions = domain.amtscanoptions.join(','); amtscanoptions = encodeURIComponent(domain.amtscanoptions); }
2790
+
2791
+ // Fetch the web state
2792
+ parent.debug('web', 'handleRootRequestEx: success.');
2793
+
2794
+ var webstate = '';
2795
+ if ((err == null) && (states != null) && (Array.isArray(states)) && (states.length == 1) && (states[0].state != null)) { webstate = obj.filterUserWebState(states[0].state); }
2796
+ if ((webstate == '') && (typeof domain.defaultuserwebstate == 'object')) { webstate = JSON.stringify(domain.defaultuserwebstate); } // User has no web state, use defaults.
2797
+ if (typeof domain.forceduserwebstate == 'object') { // Forces initial user web state if present, use it.
2798
+ var webstate2 = {};
2799
+ try { if (webstate != '') { webstate2 = JSON.parse(webstate); } } catch (ex) { }
2800
+ for (var i in domain.forceduserwebstate) { webstate2[i] = domain.forceduserwebstate[i]; }
2801
+ webstate = JSON.stringify(webstate2);
2802
+ }
2803
+
2804
+ // Custom user interface
2805
+ var customui = '';
2806
+ if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); }
2807
+
2808
+ // Server features
2809
+ var serverFeatures = 127;
2810
+ if (domain.myserver === false) { serverFeatures = 0; } // 64 = Show "My Server" tab
2811
+ else if (typeof domain.myserver == 'object') {
2812
+ if (domain.myserver.backup !== true) { serverFeatures -= 1; } // Disallow simple server backups
2813
+ if (domain.myserver.restore !== true) { serverFeatures -= 2; } // Disallow simple server restore
2814
+ if (domain.myserver.upgrade !== true) { serverFeatures -= 4; } // Disallow server upgrade
2815
+ if (domain.myserver.errorlog !== true) { serverFeatures -= 8; } // Disallow show server crash log
2816
+ if (domain.myserver.console !== true) { serverFeatures -= 16; } // Disallow server console
2817
+ if (domain.myserver.trace !== true) { serverFeatures -= 32; } // Disallow server tracing
2818
+ }
2819
+ if (obj.db.databaseType != 1) { // If not using NeDB, we can't backup using the simple system.
2820
+ if ((serverFeatures & 1) != 0) { serverFeatures -= 1; } // Disallow server backups
2821
+ if ((serverFeatures & 2) != 0) { serverFeatures -= 2; } // Disallow simple server restore
2822
+ }
2823
+
2824
+ // Refresh the session
2825
+ render(dbGetFunc.req, dbGetFunc.res, getRenderPage('default', dbGetFunc.req, domain), getRenderArgs({
2826
+ authCookie: authCookie,
2827
+ authRelayCookie: authRelayCookie,
2828
+ viewmode: viewmode,
2829
+ currentNode: currentNode,
2830
+ logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27'),
2831
+ domain: domain.id,
2832
+ debuglevel: parent.debugLevel,
2833
+ serverDnsName: obj.getWebServerName(domain),
2834
+ serverRedirPort: args.redirport,
2835
+ serverPublicPort: httpsPort,
2836
+ serverfeatures: serverFeatures,
2837
+ features: allFeatures.features,
2838
+ features2: allFeatures.features2,
2839
+ sessiontime: (args.sessiontime) ? args.sessiontime : 60,
2840
+ mpspass: args.mpspass,
2841
+ passRequirements: passRequirements,
2842
+ customui: customui,
2843
+ webcerthash: Buffer.from(obj.webCertificateFullHashs[domain.id], 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'),
2844
+ footer: (domain.footer == null) ? '' : domain.footer,
2845
+ webstate: encodeURIComponent(webstate).replace(/'/g, '%27'),
2846
+ amtscanoptions: amtscanoptions,
2847
+ pluginHandler: (parent.pluginHandler == null) ? 'null' : parent.pluginHandler.prepExports()
2848
+ }, dbGetFunc.req, domain), user);
2849
+ }
2850
+ xdbGetFunc.req = req;
2851
+ xdbGetFunc.res = res;
2852
+ xdbGetFunc.user = user;
2853
+ obj.db.Get('ws' + user._id, xdbGetFunc);
2854
+ } else {
2855
+ // Send back the login application
2856
+ // If this is a 2 factor auth request, look for a hardware key challenge.
2857
+ // Normal login 2 factor request
2858
+ if (req.session && (req.session.loginmode == 4) && (req.session.tuserid)) {
2859
+ var user = obj.users[req.session.tuserid];
2860
+ if (user != null) {
2861
+ parent.debug('web', 'handleRootRequestEx: sending 2FA challenge.');
2862
+ getHardwareKeyChallenge(req, domain, user, function (hwchallenge) { handleRootRequestLogin(req, res, domain, hwchallenge, passRequirements); });
2863
+ return;
2864
+ }
2865
+ }
2866
+ // Password recovery 2 factor request
2867
+ if (req.session && (req.session.loginmode == 5) && (req.session.temail)) {
2868
+ obj.db.GetUserWithVerifiedEmail(domain.id, req.session.temail, function (err, docs) {
2869
+ if ((err != null) || (docs.length == 0)) {
2870
+ parent.debug('web', 'handleRootRequestEx: password recover 2FA fail.');
2871
+ req.session = null;
2872
+ res.redirect(domain.url + getQueryPortion(req)); // BAD***
2873
+ } else {
2874
+ var user = obj.users[docs[0]._id];
2875
+ if (user != null) {
2876
+ parent.debug('web', 'handleRootRequestEx: password recover 2FA challenge.');
2877
+ getHardwareKeyChallenge(req, domain, user, function (hwchallenge) { handleRootRequestLogin(req, res, domain, hwchallenge, passRequirements); });
2878
+ } else {
2879
+ parent.debug('web', 'handleRootRequestEx: password recover 2FA no user.');
2880
+ req.session = null;
2881
+ res.redirect(domain.url + getQueryPortion(req)); // BAD***
2882
+ }
2883
+ }
2884
+ });
2885
+ return;
2886
+ }
2887
+ handleRootRequestLogin(req, res, domain, '', passRequirements);
2888
+ }
2889
+ }
2890
+
2891
+ // Return a list of server supported features for a given domain and user
2892
+ obj.getDomainUserFeatures = function(domain, user, req) {
2893
+ var features = 0;
2894
+ var features2 = 0;
2895
+ if (obj.args.wanonly == true) { features += 0x00000001; } // WAN-only mode
2896
+ if (obj.args.lanonly == true) { features += 0x00000002; } // LAN-only mode
2897
+ if (obj.args.nousers == true) { features += 0x00000004; } // Single user mode
2898
+ if (domain.userQuota == -1) { features += 0x00000008; } // No server files mode
2899
+ if (obj.args.mpstlsoffload) { features += 0x00000010; } // No mutual-auth CIRA
2900
+ if ((parent.config.settings.allowframing != null) || (domain.allowframing != null)) { features += 0x00000020; } // Allow site within iframe
2901
+ if ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true)) { features += 0x00000040; } // Email invites
2902
+ if (obj.args.webrtc == true) { features += 0x00000080; } // Enable WebRTC (Default false for now)
2903
+ // 0x00000100 --> This feature flag is free for future use.
2904
+ if (obj.args.allowhighqualitydesktop !== false) { features += 0x00000200; } // Enable AllowHighQualityDesktop (Default true)
2905
+ if ((obj.args.lanonly == true) || (obj.args.mpsport == 0)) { features += 0x00000400; } // No CIRA
2906
+ if ((obj.parent.serverSelfWriteAllowed == true) && (user != null) && ((user.siteadmin & 0x00000010) != 0)) { features += 0x00000800; } // Server can self-write (Allows self-update)
2907
+ 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
2908
+ if (domain.agentnoproxy === true) { features += 0x00002000; } // Indicates that agents should be installed without using a HTTP proxy
2909
+ if ((parent.config.settings.no2factorauth !== true) && domain.yubikey && domain.yubikey.id && domain.yubikey.secret && (user._id.split('/')[2][0] != '~')) { features += 0x00004000; } // Indicates Yubikey support
2910
+ if (domain.geolocation == true) { features += 0x00008000; } // Enable geo-location features
2911
+ if ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true)) { features += 0x00010000; } // Enable password hints
2912
+ if (parent.config.settings.no2factorauth !== true) { features += 0x00020000; } // Enable WebAuthn/FIDO2 support
2913
+ if ((obj.args.nousers != true) && (domain.passwordrequirements != null) && (domain.passwordrequirements.force2factor === true) && (user._id.split('/')[2][0] != '~')) {
2914
+ // Check if we can skip 2nd factor auth because of the source IP address
2915
+ var skip2factor = false;
2916
+ if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
2917
+ for (var i in domain.passwordrequirements.skip2factor) {
2918
+ if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) { skip2factor = true; }
2919
+ }
2920
+ }
2921
+ if (skip2factor == false) { features += 0x00040000; } // Force 2-factor auth
2922
+ }
2923
+ 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.
2924
+ if (domain.amtacmactivation) { features += 0x00100000; } // Intel AMT ACM activation/upgrade is possible
2925
+ if (domain.usernameisemail) { features += 0x00200000; } // Username is email address
2926
+ if (parent.mqttbroker != null) { features += 0x00400000; } // This server supports MQTT channels
2927
+ if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null)) { features += 0x00800000; } // using email for 2FA is allowed
2928
+ if (domain.agentinvitecodes == true) { features += 0x01000000; } // Support for agent invite codes
2929
+ if (parent.smsserver != null) { features += 0x02000000; } // SMS messaging is supported
2930
+ if ((parent.smsserver != null) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false))) { features += 0x04000000; } // SMS 2FA is allowed
2931
+ if (domain.sessionrecording != null) { features += 0x08000000; } // Server recordings enabled
2932
+ if (domain.urlswitching === false) { features += 0x10000000; } // Disables the URL switching feature
2933
+ if (domain.novnc === false) { features += 0x20000000; } // Disables noVNC
2934
+ if (domain.mstsc !== true) { features += 0x40000000; } // Disables MSTSC.js
2935
+ if (obj.isTrustedCert(domain) == false) { features += 0x80000000; } // Indicate we are not using a trusted certificate
2936
+ if (obj.parent.amtManager != null) { features2 += 0x00000001; } // Indicates that the Intel AMT manager is active
2937
+ if (obj.parent.firebase != null) { features2 += 0x00000002; } // Indicates the server supports Firebase push messaging
2938
+ if ((obj.parent.firebase != null) && (obj.parent.firebase.pushOnly != true)) { features2 += 0x00000004; } // Indicates the server supports Firebase two-way push messaging
2939
+ if (obj.parent.webpush != null) { features2 += 0x00000008; } // Indicates web push is enabled
2940
+ if (((obj.args.noagentupdate == 1) || (obj.args.noagentupdate == true))) { features2 += 0x00000010; } // No agent update
2941
+ if (parent.amtProvisioningServer != null) { features2 += 0x00000020; } // Intel AMT LAN provisioning server
2942
+ if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.push2factor != false)) && (obj.parent.firebase != null)) { features2 += 0x00000040; } // Indicates device push notification 2FA is enabled
2943
+ if ((typeof domain.passwordrequirements != 'object') || ((domain.passwordrequirements.logintokens !== false) && ((Array.isArray(domain.passwordrequirements.logintokens) == false) || (domain.passwordrequirements.logintokens.indexOf(user._id) >= 0)))) { features2 += 0x00000080; } // Indicates login tokens are allowed
2944
+ if (req.session.loginToken != null) { features2 += 0x00000100; } // LoginToken mode, no account changes.
2945
+ if (domain.ssh == true) { features2 += 0x00000200; } // SSH is enabled
2946
+ if (domain.localsessionrecording === false) { features2 += 0x00000400; } // Disable local recording feature
2947
+ if (domain.clipboardget == false) { features2 += 0x00000800; } // Disable clipboard get
2948
+ if (domain.clipboardset == false) { features2 += 0x00001000; } // Disable clipboard set
2949
+ if ((typeof domain.desktop == 'object') && (domain.desktop.viewonly == true)) { features2 += 0x00002000; } // Indicates remote desktop is viewonly
2950
+ if (domain.mailserver != null) { features2 += 0x00004000; } // Indicates email server is active
2951
+ if (domain.devicesearchbarserverandclientname) { features2 += 0x00008000; } // Search bar will find both server name and client name
2952
+ if (domain.ipkvm) { features2 += 0x00010000; } // Indicates support for IP KVM device groups
2953
+ if ((domain.passwordrequirements) && (domain.passwordrequirements.otp2factor == false)) { features2 += 0x00020000; } // Indicates support for OTP 2FA is disabled
2954
+ if ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.backupcode2factor === false)) { features2 += 0x00040000; } // Indicates 2FA backup codes are disabled
2955
+ if ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.single2factorwarning === false)) { features2 += 0x00080000; } // Indicates no warning if a single 2FA is in use
2956
+ if (domain.nightmode === 1) { features2 += 0x00100000; } // Always night mode
2957
+ if (domain.nightmode === 2) { features2 += 0x00200000; } // Always day mode
2958
+ if (domain.allowsavingdevicecredentials == false) { features2 += 0x00400000; } // Do not allow device credentials to be saved on the server
2959
+ return { features: features, features2: features2 };
2960
+ }
2961
+
2962
+ function handleRootRequestLogin(req, res, domain, hardwareKeyChallenge, passRequirements) {
2963
+ parent.debug('web', 'handleRootRequestLogin()');
2964
+ var features = 0;
2965
+ 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
2966
+ if (domain.usernameisemail) { features += 0x00200000; } // Username is email address
2967
+ var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
2968
+ var loginmode = 0;
2969
+ 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.
2970
+
2971
+ // Format an error message if needed
2972
+ var passhint = null, msgid = 0;
2973
+ if (req.session != null) {
2974
+ msgid = req.session.messageid;
2975
+ if ((msgid == 5) || (loginmode == 7) || ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true))) { passhint = EscapeHtml(req.session.passhint); }
2976
+ delete req.session.messageid;
2977
+ delete req.session.passhint;
2978
+ }
2979
+ const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false));
2980
+ const emailcheck = (allowAccountReset && (domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap'))
2981
+
2982
+ // Check if we are allowed to create new users using the login screen
2983
+ var newAccountsAllowed = true;
2984
+ if ((domain.newaccounts !== 1) && (domain.newaccounts !== true)) { for (var i in obj.users) { if (obj.users[i].domain == domain.id) { newAccountsAllowed = false; break; } } }
2985
+ if (parent.config.settings.maintenancemode != null) { newAccountsAllowed = false; }
2986
+
2987
+ // Encrypt the hardware key challenge state if needed
2988
+ var hwstate = null;
2989
+ if (hardwareKeyChallenge) { hwstate = obj.parent.encodeCookie({ u: req.session.tuser, p: req.session.tpass, c: req.session.u2f }, obj.parent.loginCookieEncryptionKey) }
2990
+
2991
+ // Check if we can use OTP tokens with email. We can't use email for 2FA password recovery (loginmode 5).
2992
+ var otpemail = (loginmode != 5) && (domain.mailserver != null) && (req.session != null) && ((req.session.temail === 1) || (typeof req.session.temail == 'string'));
2993
+ if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; }
2994
+ var otpsms = (parent.smsserver != null) && (req.session != null) && (req.session.tsms === 1);
2995
+ if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; }
2996
+ var otppush = (parent.firebase != null) && (req.session != null) && (req.session.tpush === 1);
2997
+ if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.push2factor == false)) { otppush = false; }
2998
+ const autofido = ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.autofido2fa == true)); // See if FIDO should be automatically prompted if user account has it.
2999
+
3000
+ // See if we support two-factor trusted cookies
3001
+ var twoFactorCookieDays = 30;
3002
+ if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
3003
+
3004
+ // See what authentication strategies we have
3005
+ var authStrategies = [];
3006
+ if (typeof domain.authstrategies == 'object') {
3007
+ if (typeof domain.authstrategies.twitter == 'object') { authStrategies.push('twitter'); }
3008
+ if (typeof domain.authstrategies.google == 'object') { authStrategies.push('google'); }
3009
+ if (typeof domain.authstrategies.github == 'object') { authStrategies.push('github'); }
3010
+ if (typeof domain.authstrategies.reddit == 'object') { authStrategies.push('reddit'); }
3011
+ if (typeof domain.authstrategies.azure == 'object') { authStrategies.push('azure'); }
3012
+ if (typeof domain.authstrategies.oidc == 'object') { authStrategies.push('oidc'); }
3013
+ if (typeof domain.authstrategies.intel == 'object') { authStrategies.push('intel'); }
3014
+ if (typeof domain.authstrategies.jumpcloud == 'object') { authStrategies.push('jumpcloud'); }
3015
+ if (typeof domain.authstrategies.saml == 'object') { authStrategies.push('saml'); }
3016
+ }
3017
+
3018
+ // Custom user interface
3019
+ var customui = '';
3020
+ if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); }
3021
+
3022
+ // Get two-factor screen timeout
3023
+ var twoFactorTimeout = 300000; // Default is 5 minutes, 0 for no timeout.
3024
+ if ((typeof domain.passwordrequirements == 'object') && (typeof domain.passwordrequirements.twofactortimeout == 'number')) {
3025
+ twoFactorTimeout = domain.passwordrequirements.twofactortimeout * 1000;
3026
+ }
3027
+
3028
+ // Render the login page
3029
+ render(req, res,
3030
+ getRenderPage((domain.sitestyle == 2) ? 'login2' : 'login', req, domain),
3031
+ getRenderArgs({
3032
+ loginmode: loginmode,
3033
+ rootCertLink: getRootCertLink(domain),
3034
+ newAccount: newAccountsAllowed,
3035
+ newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1),
3036
+ serverDnsName: obj.getWebServerName(domain),
3037
+ serverPublicPort: httpsPort,
3038
+ passlogin: (typeof domain.showpasswordlogin == 'boolean') ? domain.showpasswordlogin : true,
3039
+ emailcheck: emailcheck,
3040
+ features: features,
3041
+ sessiontime: (args.sessiontime) ? args.sessiontime : 60,
3042
+ passRequirements: passRequirements,
3043
+ customui: customui,
3044
+ footer: (domain.loginfooter == null) ? '' : domain.loginfooter,
3045
+ hkey: encodeURIComponent(hardwareKeyChallenge).replace(/'/g, '%27'),
3046
+ messageid: msgid,
3047
+ passhint: passhint,
3048
+ welcometext: domain.welcometext ? encodeURIComponent(domain.welcometext).split('\'').join('\\\'') : null,
3049
+ welcomePictureFullScreen: ((typeof domain.welcomepicturefullscreen == 'boolean') ? domain.welcomepicturefullscreen : false),
3050
+ hwstate: hwstate,
3051
+ otpemail: otpemail,
3052
+ otpsms: otpsms,
3053
+ otppush: otppush,
3054
+ autofido: autofido,
3055
+ twoFactorCookieDays: twoFactorCookieDays,
3056
+ authStrategies: authStrategies.join(','),
3057
+ loginpicture: (typeof domain.loginpicture == 'string'),
3058
+ tokenTimeout: twoFactorTimeout // Two-factor authentication screen timeout in milliseconds
3059
+ }, req, domain, (domain.sitestyle == 2) ? 'login2' : 'login'));
3060
+ }
3061
+
3062
+ // Handle a post request on the root
3063
+ function handleRootPostRequest(req, res) {
3064
+ const domain = checkUserIpAddress(req, res);
3065
+ if (domain == null) { return; }
3066
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.end("Not Found"); return; } // Check 3FA URL key
3067
+ parent.debug('web', 'handleRootPostRequest, action: ' + req.body.action);
3068
+
3069
+ switch (req.body.action) {
3070
+ case 'login': { handleLoginRequest(req, res, true); break; }
3071
+ case 'tokenlogin': {
3072
+ if (req.body.hwstate) {
3073
+ var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 10);
3074
+ if (cookie != null) { req.session.tuser = cookie.u; req.session.tpass = cookie.p; req.session.u2f = cookie.c; }
3075
+ }
3076
+ handleLoginRequest(req, res, true); break;
3077
+ }
3078
+ case 'pushlogin': {
3079
+ if (req.body.hwstate) {
3080
+ var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 1);
3081
+ if ((cookie != null) && (typeof cookie.u == 'string') && (cookie.d == domain.id) && (cookie.a == 'pushAuth')) {
3082
+ // Push authentication is a success, login the user
3083
+ req.session = { userid: cookie.u };
3084
+
3085
+ // Check if we need to remember this device
3086
+ if ((req.body.remembertoken === 'on') && ((domain.twofactorcookiedurationdays == null) || (domain.twofactorcookiedurationdays > 0))) {
3087
+ var maxCookieAge = domain.twofactorcookiedurationdays;
3088
+ if (typeof maxCookieAge != 'number') { maxCookieAge = 30; }
3089
+ const twoFactorCookie = obj.parent.encodeCookie({ userid: cookie.u, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey);
3090
+ res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: parent.config.settings.cookiesamesite, secure: true });
3091
+ }
3092
+
3093
+ handleRootRequestEx(req, res, domain);
3094
+ return;
3095
+ }
3096
+ }
3097
+ handleLoginRequest(req, res, true); break;
3098
+ }
3099
+ case 'changepassword': { handlePasswordChangeRequest(req, res, true); break; }
3100
+ case 'deleteaccount': { handleDeleteAccountRequest(req, res, true); break; }
3101
+ case 'createaccount': { handleCreateAccountRequest(req, res, true); break; }
3102
+ case 'resetpassword': { handleResetPasswordRequest(req, res, true); break; }
3103
+ case 'resetaccount': { handleResetAccountRequest(req, res, true); break; }
3104
+ case 'checkemail': { handleCheckAccountEmailRequest(req, res, true); break; }
3105
+ default: { handleLoginRequest(req, res, true); break; }
3106
+ }
3107
+ }
3108
+
3109
+ // Return true if it looks like we are using a real TLS certificate.
3110
+ obj.isTrustedCert = function (domain) {
3111
+ if ((domain != null) && (typeof domain.trustedcert == 'boolean')) return domain.trustedcert; // If the status of the cert specified, use that.
3112
+ if (typeof obj.args.trustedcert == 'boolean') return obj.args.trustedcert; // If the status of the cert specified, use that.
3113
+ if (obj.args.tlsoffload != null) return true; // We are using TLS offload, a real cert is likely used.
3114
+ 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.
3115
+ if (obj.certificates.WebIssuer.indexOf('MeshCentralRoot-') == 0) return false; // Our cert is issued by self-signed cert.
3116
+ if (obj.certificates.CommonName.indexOf('.') == -1) return false; // Our cert is named with a fake name
3117
+ return true; // This is a guess
3118
+ }
3119
+
3120
+ // Get the link to the root certificate if needed
3121
+ function getRootCertLink(domain) {
3122
+ // Check if the HTTPS certificate is issued from MeshCentralRoot, if so, add download link to root certificate.
3123
+ if (obj.isTrustedCert(domain) == false) {
3124
+ // Get the domain suffix
3125
+ var xdomain = (domain.dns == null) ? domain.id : '';
3126
+ if (xdomain != '') xdomain += '/';
3127
+ return '<a href=/' + xdomain + 'MeshServerRootCert.cer title="Download the root certificate for this server">Root Certificate</a>';
3128
+ }
3129
+ return '';
3130
+ }
3131
+
3132
+ // Serve the xterm page
3133
+ function handleXTermRequest(req, res) {
3134
+ const domain = checkUserIpAddress(req, res);
3135
+ if (domain == null) { return; }
3136
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
3137
+
3138
+ parent.debug('web', 'handleXTermRequest: sending xterm');
3139
+ res.set({ 'Cache-Control': 'no-store' });
3140
+ if (req.session && req.session.userid) {
3141
+ if (req.session.userid.split('/')[1] != domain.id) { res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain
3142
+ var user = obj.users[req.session.userid];
3143
+ if ((user == null) || (req.query.nodeid == null)) { res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the user exists
3144
+
3145
+ // Check permissions
3146
+ obj.GetNodeWithRights(domain, user, req.query.nodeid, function (node, rights, visible) {
3147
+ if ((node == null) || ((rights & 8) == 0) || ((rights != 0xFFFFFFFF) && ((rights & 512) != 0))) { res.redirect(domain.url + getQueryPortion(req)); return; }
3148
+
3149
+ var logoutcontrols = { name: user.name };
3150
+ var extras = (req.query.key != null) ? ('&key=' + req.query.key) : '';
3151
+ 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
3152
+
3153
+ // Create a authentication cookie
3154
+ const authCookie = obj.parent.encodeCookie({ userid: user._id, domainid: domain.id, ip: req.clientIp }, obj.parent.loginCookieEncryptionKey);
3155
+ const authRelayCookie = obj.parent.encodeCookie({ ruserid: user._id, domainid: domain.id }, obj.parent.loginCookieEncryptionKey);
3156
+ var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
3157
+ 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));
3158
+ });
3159
+ } else {
3160
+ res.redirect(domain.url + getQueryPortion(req));
3161
+ return;
3162
+ }
3163
+ }
3164
+
3165
+ // Render the terms of service.
3166
+ function handleTermsRequest(req, res) {
3167
+ const domain = checkUserIpAddress(req, res);
3168
+ if (domain == null) { return; }
3169
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
3170
+
3171
+ // See if term.txt was loaded from the database
3172
+ if ((parent.configurationFiles != null) && (parent.configurationFiles['terms.txt'] != null)) {
3173
+ // Send the terms from the database
3174
+ res.set({ 'Cache-Control': 'no-store' });
3175
+ if (req.session && req.session.userid) {
3176
+ if (req.session.userid.split('/')[1] != domain.id) { req.session = null; res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain
3177
+ var user = obj.users[req.session.userid];
3178
+ var logoutcontrols = { name: user.name };
3179
+ var extras = (req.query.key != null) ? ('&key=' + req.query.key) : '';
3180
+ 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
3181
+ 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));
3182
+ } else {
3183
+ render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(parent.configurationFiles['terms.txt'].toString()).split('\'').join('\\\''), logoutControls: encodeURIComponent('{}') }, req, domain));
3184
+ }
3185
+ } else {
3186
+ // See if there is a terms.txt file in meshcentral-data
3187
+ var p = obj.path.join(obj.parent.datapath, 'terms.txt');
3188
+ if (obj.fs.existsSync(p)) {
3189
+ obj.fs.readFile(p, 'utf8', function (err, data) {
3190
+ if (err != null) { parent.debug('web', 'handleTermsRequest: no terms.txt'); res.sendStatus(404); return; }
3191
+
3192
+ // Send the terms from terms.txt
3193
+ res.set({ 'Cache-Control': 'no-store' });
3194
+ if (req.session && req.session.userid) {
3195
+ if (req.session.userid.split('/')[1] != domain.id) { req.session = null; res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain
3196
+ var user = obj.users[req.session.userid];
3197
+ var logoutcontrols = { name: user.name };
3198
+ var extras = (req.query.key != null) ? ('&key=' + req.query.key) : '';
3199
+ 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
3200
+ render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(data).split('\'').join('\\\''), logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain));
3201
+ } else {
3202
+ render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(data).split('\'').join('\\\''), logoutControls: encodeURIComponent('{}') }, req, domain));
3203
+ }
3204
+ });
3205
+ } else {
3206
+ // Send the default terms
3207
+ parent.debug('web', 'handleTermsRequest: sending default terms');
3208
+ res.set({ 'Cache-Control': 'no-store' });
3209
+ if (req.session && req.session.userid) {
3210
+ if (req.session.userid.split('/')[1] != domain.id) { req.session = null; res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain
3211
+ var user = obj.users[req.session.userid];
3212
+ var logoutcontrols = { name: user.name };
3213
+ var extras = (req.query.key != null) ? ('&key=' + req.query.key) : '';
3214
+ 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
3215
+ render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain));
3216
+ } else {
3217
+ render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ logoutControls: encodeURIComponent('{}') }, req, domain));
3218
+ }
3219
+ }
3220
+ }
3221
+ }
3222
+
3223
+ // Render the messenger application.
3224
+ function handleMessengerRequest(req, res) {
3225
+ const domain = getDomain(req);
3226
+ if (domain == null) { parent.debug('web', 'handleMessengerRequest: no domain'); res.sendStatus(404); return; }
3227
+ parent.debug('web', 'handleMessengerRequest()');
3228
+
3229
+ // Check if we are in maintenance mode
3230
+ if (parent.config.settings.maintenancemode != null) {
3231
+ 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));
3232
+ return;
3233
+ }
3234
+
3235
+ // Check if this session is for a user
3236
+ if (req.query.id == null) { res.sendStatus(404); return; }
3237
+ var idSplit = decodeURIComponent(req.query.id).split('/');
3238
+ if ((idSplit.length != 7) || (idSplit[0] != 'meshmessenger')) { res.sendStatus(404); return; }
3239
+ if ((idSplit[1] == 'user') && (idSplit[4] == 'user')) {
3240
+ // This is a user to user conversation, both users must be logged in.
3241
+ var user1 = idSplit[1] + '/' + idSplit[2] + '/' + idSplit[3]
3242
+ var user2 = idSplit[4] + '/' + idSplit[5] + '/' + idSplit[6]
3243
+ if (!req.session || !req.session.userid) {
3244
+ // Redirect to login page
3245
+ if (req.query.key != null) { res.redirect(domain.url + '?key=' + req.query.key + '&meshmessengerid=' + req.query.id); } else { res.redirect(domain.url + '?meshmessengerid=' + req.query.id); }
3246
+ return;
3247
+ }
3248
+ if ((req.session.userid != user1) && (req.session.userid != user2)) { res.sendStatus(404); return; }
3249
+ }
3250
+
3251
+ // Get WebRTC configuration
3252
+ var webRtcConfig = null;
3253
+ 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'); }
3254
+ else if (args.webrtconfig && (typeof args.webrtconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(args.webrtconfig)).replace(/'/g, '%27'); }
3255
+
3256
+ // Setup other options
3257
+ var options = { webrtconfig: webRtcConfig };
3258
+ if (typeof domain.meshmessengertitle == 'string') { options.meshMessengerTitle = domain.meshmessengertitle; } else { options.meshMessengerTitle = '!'; }
3259
+
3260
+ // Get the userid and name
3261
+ if ((domain.meshmessengertitle != null) && (req.query.id != null) && (req.query.id.startsWith('meshmessenger/node'))) {
3262
+ if (idSplit.length == 7) {
3263
+ const user = obj.users[idSplit[4] + '/' + idSplit[5] + '/' + idSplit[6]];
3264
+ if (user != null) {
3265
+ if (domain.meshmessengertitle.indexOf('{0}') >= 0) { options.username = encodeURIComponent(user.realname ? user.realname : user.name).replace(/'/g, '%27'); }
3266
+ if (domain.meshmessengertitle.indexOf('{1}') >= 0) { options.userid = encodeURIComponent(user.name).replace(/'/g, '%27'); }
3267
+ }
3268
+ }
3269
+ }
3270
+
3271
+ // Render the page
3272
+ res.set({ 'Cache-Control': 'no-store' });
3273
+ render(req, res, getRenderPage('messenger', req, domain), getRenderArgs(options, req, domain));
3274
+ }
3275
+
3276
+ // Handle messenger image request
3277
+ function handleMessengerImageRequest(req, res) {
3278
+ const domain = getDomain(req);
3279
+ if (domain == null) { parent.debug('web', 'handleMessengerImageRequest: no domain'); res.sendStatus(404); return; }
3280
+ parent.debug('web', 'handleMessengerImageRequest()');
3281
+
3282
+ // Check if we are in maintenance mode
3283
+ if (parent.config.settings.maintenancemode != null) { res.sendStatus(404); return; }
3284
+
3285
+ //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
3286
+ if (domain.meshmessengerpicture) {
3287
+ // Use the configured messenger logo picture
3288
+ try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.meshmessengerpicture)); return; } catch (ex) { }
3289
+ }
3290
+
3291
+ var imagefile = 'images/messenger.png';
3292
+ if (domain.webpublicpath != null) {
3293
+ obj.fs.exists(obj.path.join(domain.webpublicpath, imagefile), function (exists) {
3294
+ if (exists) {
3295
+ // Use the domain logo picture
3296
+ try { res.sendFile(obj.path.join(domain.webpublicpath, imagefile)); } catch (ex) { res.sendStatus(404); }
3297
+ } else {
3298
+ // Use the default logo picture
3299
+ try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
3300
+ }
3301
+ });
3302
+ } else if (parent.webPublicOverridePath) {
3303
+ obj.fs.exists(obj.path.join(obj.parent.webPublicOverridePath, imagefile), function (exists) {
3304
+ if (exists) {
3305
+ // Use the override logo picture
3306
+ try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, imagefile)); } catch (ex) { res.sendStatus(404); }
3307
+ } else {
3308
+ // Use the default logo picture
3309
+ try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
3310
+ }
3311
+ });
3312
+ } else {
3313
+ // Use the default logo picture
3314
+ try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
3315
+ }
3316
+ }
3317
+
3318
+ // Returns the server root certificate encoded in base64
3319
+ function getRootCertBase64() {
3320
+ var rootcert = obj.certificates.root.cert;
3321
+ var i = rootcert.indexOf('-----BEGIN CERTIFICATE-----\r\n');
3322
+ if (i >= 0) { rootcert = rootcert.substring(i + 29); }
3323
+ i = rootcert.indexOf('-----END CERTIFICATE-----');
3324
+ if (i >= 0) { rootcert = rootcert.substring(i, 0); }
3325
+ return Buffer.from(rootcert, 'base64').toString('base64');
3326
+ }
3327
+
3328
+ // Returns the mesh server root certificate
3329
+ function handleRootCertRequest(req, res) {
3330
+ const domain = getDomain(req);
3331
+ if (domain == null) { parent.debug('web', 'handleRootCertRequest: no domain'); res.sendStatus(404); return; }
3332
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
3333
+ if ((obj.userAllowedIp != null) && (checkIpAddressEx(req, res, obj.userAllowedIp, false) === false)) { parent.debug('web', 'handleRootCertRequest: invalid ip'); return; } // Check server-wide IP filter only.
3334
+ parent.debug('web', 'handleRootCertRequest()');
3335
+ setContentDispositionHeader(res, 'application/octet-stream', certificates.RootName + '.cer', null, 'rootcert.cer');
3336
+ res.send(Buffer.from(getRootCertBase64(), 'base64'));
3337
+ }
3338
+
3339
+ // Handle user public file downloads
3340
+ function handleDownloadUserFiles(req, res) {
3341
+ const domain = checkUserIpAddress(req, res);
3342
+ if (domain == null) { return; }
3343
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
3344
+
3345
+ if (obj.common.validateString(req.path, 1, 4096) == false) { res.sendStatus(404); return; }
3346
+ var domainname = 'domain', spliturl = decodeURIComponent(req.path).split('/'), filename = '';
3347
+ if ((spliturl.length < 3) || (obj.common.IsFilenameValid(spliturl[2]) == false) || (domain.userQuota == -1)) { res.sendStatus(404); return; }
3348
+ if (domain.id != '') { domainname = 'domain-' + domain.id; }
3349
+ var path = obj.path.join(obj.filespath, domainname + '/user-' + spliturl[2] + '/Public');
3350
+ 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; } }
3351
+
3352
+ var stat = null;
3353
+ try { stat = obj.fs.statSync(path); } catch (e) { }
3354
+ if ((stat != null) && ((stat.mode & 0x004000) == 0)) {
3355
+ if (req.query.download == 1) {
3356
+ setContentDispositionHeader(res, 'application/octet-stream', filename, null, 'file.bin');
3357
+ try { res.sendFile(obj.path.resolve(__dirname, path)); } catch (e) { res.sendStatus(404); }
3358
+ } else {
3359
+ render(req, res, getRenderPage((domain.sitestyle == 2) ? 'download2' : 'download', req, domain), getRenderArgs({ rootCertLink: getRootCertLink(domain), messageid: 1, fileurl: req.path + '?download=1', filename: filename, filesize: stat.size }, req, domain));
3360
+ }
3361
+ } else {
3362
+ render(req, res, getRenderPage((domain.sitestyle == 2) ? 'download2' : 'download', req, domain), getRenderArgs({ rootCertLink: getRootCertLink(domain), messageid: 2 }, req, domain));
3363
+ }
3364
+ }
3365
+
3366
+ // Handle device file request
3367
+ function handleDeviceFile(req, res) {
3368
+ const domain = checkUserIpAddress(req, res);
3369
+ if (domain == null) { return; }
3370
+ if ((req.query.c == null) || (req.query.f == null)) { res.sendStatus(404); return; }
3371
+
3372
+ // Check the inbound desktop sharing cookie
3373
+ var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
3374
+ if ((c == null) || (c.domainid !== domain.id)) { res.sendStatus(404); return; }
3375
+
3376
+ // Check userid
3377
+ const user = obj.users[c.userid];
3378
+ if ((c == user)) { res.sendStatus(404); return; }
3379
+
3380
+ // If this cookie has restricted usages, check that it's allowed to perform downloads
3381
+ if (Array.isArray(c.usages) && (c.usages.indexOf(10) < 0)) { res.sendStatus(404); return; } // Check protocol #10
3382
+
3383
+ if (c.nid != null) { req.query.n = c.nid.split('/')[2]; } // This cookie is restricted to a specific nodeid.
3384
+ if (req.query.n == null) { res.sendStatus(404); return; }
3385
+
3386
+ // Check if this user has permission to manage this computer
3387
+ obj.GetNodeWithRights(domain, user, 'node/' + domain.id + '/' + req.query.n, function (node, rights, visible) {
3388
+ if ((node == null) || ((rights & MESHRIGHT_REMOTECONTROL) == 0) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device
3389
+
3390
+ // All good, start the file transfer
3391
+ req.query.id = getRandomLowerCase(12);
3392
+ obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, null, res, req, domain, user, node.meshid, node._id);
3393
+ });
3394
+ }
3395
+
3396
+ // Handle download of a server file by an agent
3397
+ function handleAgentDownloadFile(req, res) {
3398
+ const domain = checkUserIpAddress(req, res);
3399
+ if (domain == null) { return; }
3400
+ if (req.query.c == null) { res.sendStatus(404); return; }
3401
+
3402
+ // Check the inbound desktop sharing cookie
3403
+ var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 5); // 5 minute timeout
3404
+ 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; }
3405
+
3406
+ // Send the file back
3407
+ try { res.sendFile(obj.path.join(obj.filespath, 'tmp', c.f)); return; } catch (ex) { res.sendStatus(404); }
3408
+ }
3409
+
3410
+ // Handle logo request
3411
+ function handleLogoRequest(req, res) {
3412
+ const domain = checkUserIpAddress(req, res);
3413
+ if (domain == null) { return; }
3414
+
3415
+ //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
3416
+ if (domain.titlepicture) {
3417
+ if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.titlepicture] != null)) {
3418
+ // Use the logo in the database
3419
+ res.set({ 'Content-Type': domain.titlepicture.toLowerCase().endsWith('.png')?'image/png':'image/jpeg' });
3420
+ res.send(parent.configurationFiles[domain.titlepicture]);
3421
+ return;
3422
+ } else {
3423
+ // Use the logo on file
3424
+ try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.titlepicture)); return; } catch (ex) { }
3425
+ }
3426
+ }
3427
+
3428
+ if ((domain.webpublicpath != null) && (obj.fs.existsSync(obj.path.join(domain.webpublicpath, 'images/logoback.png')))) {
3429
+ // Use the domain logo picture
3430
+ try { res.sendFile(obj.path.join(domain.webpublicpath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
3431
+ } else if (parent.webPublicOverridePath && obj.fs.existsSync(obj.path.join(obj.parent.webPublicOverridePath, 'images/logoback.png'))) {
3432
+ // Use the override logo picture
3433
+ try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
3434
+ } else {
3435
+ // Use the default logo picture
3436
+ try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
3437
+ }
3438
+ }
3439
+
3440
+ // Handle login logo request
3441
+ function handleLoginLogoRequest(req, res) {
3442
+ const domain = checkUserIpAddress(req, res);
3443
+ if (domain == null) { return; }
3444
+
3445
+ //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
3446
+ if (domain.loginpicture) {
3447
+ if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.loginpicture] != null)) {
3448
+ // Use the logo in the database
3449
+ res.set({ 'Content-Type': domain.loginpicture.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
3450
+ res.send(parent.configurationFiles[domain.loginpicture]);
3451
+ return;
3452
+ } else {
3453
+ // Use the logo on file
3454
+ try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.loginpicture)); return; } catch (ex) { res.sendStatus(404); }
3455
+ }
3456
+ } else {
3457
+ res.sendStatus(404);
3458
+ }
3459
+ }
3460
+
3461
+ // Handle translation request
3462
+ function handleTranslationsRequest(req, res) {
3463
+ const domain = checkUserIpAddress(req, res);
3464
+ if (domain == null) { return; }
3465
+ //if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
3466
+ if ((obj.userAllowedIp != null) && (checkIpAddressEx(req, res, obj.userAllowedIp, false) === false)) { return; } // Check server-wide IP filter only.
3467
+
3468
+ var user = null;
3469
+ if (obj.args.user != null) {
3470
+ // A default user is active
3471
+ user = obj.users['user/' + domain.id + '/' + obj.args.user];
3472
+ if (!user) { parent.debug('web', 'handleTranslationsRequest: user not found.'); res.sendStatus(401); return; }
3473
+ } else {
3474
+ // Check if the user is logged and we have all required parameters
3475
+ if (!req.session || !req.session.userid) { parent.debug('web', 'handleTranslationsRequest: failed checks (2).'); res.sendStatus(401); return; }
3476
+
3477
+ // Get the current user
3478
+ user = obj.users[req.session.userid];
3479
+ if (!user) { parent.debug('web', 'handleTranslationsRequest: user not found.'); res.sendStatus(401); return; }
3480
+ if (user.siteadmin != 0xFFFFFFFF) { parent.debug('web', 'handleTranslationsRequest: user not site administrator.'); res.sendStatus(401); return; }
3481
+ }
3482
+
3483
+ var data = '';
3484
+ req.setEncoding('utf8');
3485
+ req.on('data', function (chunk) { data += chunk; });
3486
+ req.on('end', function () {
3487
+ try { data = JSON.parse(data); } catch (ex) { data = null; }
3488
+ if (data == null) { res.sendStatus(404); return; }
3489
+ if (data.action == 'getTranslations') {
3490
+ if (obj.fs.existsSync(obj.path.join(obj.parent.datapath, 'translate.json'))) {
3491
+ // Return the translation file (JSON)
3492
+ try { res.sendFile(obj.path.join(obj.parent.datapath, 'translate.json')); } catch (ex) { res.sendStatus(404); }
3493
+ } else if (obj.fs.existsSync(obj.path.join(__dirname, 'translate', 'translate.json'))) {
3494
+ // Return the default translation file (JSON)
3495
+ try { res.sendFile(obj.path.join(__dirname, 'translate', 'translate.json')); } catch (ex) { res.sendStatus(404); }
3496
+ } else { res.sendStatus(404); }
3497
+ } else if (data.action == 'setTranslations') {
3498
+ 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 })); } });
3499
+ } else if (data.action == 'translateServer') {
3500
+ if (obj.pendingTranslation === true) { res.send(JSON.stringify({ response: 'Server is already performing a translation.' })); return; }
3501
+ const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
3502
+ if (nodeVersion < 8) { res.send(JSON.stringify({ response: 'Server requires NodeJS 8.x or better.' })); return; }
3503
+ var translateFile = obj.path.join(obj.parent.datapath, 'translate.json');
3504
+ if (obj.fs.existsSync(translateFile) == false) { translateFile = obj.path.join(__dirname, 'translate', 'translate.json'); }
3505
+ if (obj.fs.existsSync(translateFile) == false) { res.send(JSON.stringify({ response: 'Unable to find translate.js file on the server.' })); return; }
3506
+ res.send(JSON.stringify({ response: 'ok' }));
3507
+ console.log('Started server translation...');
3508
+ obj.pendingTranslation = true;
3509
+ require('child_process').exec('node translate.js translateall \"' + translateFile + '\"', { maxBuffer: 512000, timeout: 120000, cwd: obj.path.join(__dirname, 'translate') }, function (error, stdout, stderr) {
3510
+ delete obj.pendingTranslation;
3511
+ //console.log('error', error);
3512
+ //console.log('stdout', stdout);
3513
+ //console.log('stderr', stderr);
3514
+ //console.log('Server restart...'); // Perform a server restart
3515
+ //process.exit(0);
3516
+ console.log('Server translation completed.');
3517
+ });
3518
+ } else {
3519
+ // Unknown request
3520
+ res.sendStatus(404);
3521
+ }
3522
+ });
3523
+ }
3524
+
3525
+ // Handle welcome image request
3526
+ function handleWelcomeImageRequest(req, res) {
3527
+ const domain = checkUserIpAddress(req, res);
3528
+ if (domain == null) { return; }
3529
+
3530
+ //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
3531
+ if (domain.welcomepicture) {
3532
+ if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.welcomepicture] != null)) {
3533
+ // Use the welcome image in the database
3534
+ res.set({ 'Content-Type': domain.welcomepicture.toLowerCase().endsWith('.png')?'image/png':'image/jpeg' });
3535
+ res.send(parent.configurationFiles[domain.welcomepicture]);
3536
+ return;
3537
+ }
3538
+
3539
+ // Use the configured logo picture
3540
+ try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.welcomepicture)); return; } catch (ex) { }
3541
+ }
3542
+
3543
+ var imagefile = 'images/mainwelcome.jpg';
3544
+ if (domain.sitestyle == 2) { imagefile = 'images/login/back.png'; }
3545
+ if (domain.webpublicpath != null) {
3546
+ obj.fs.exists(obj.path.join(domain.webpublicpath, imagefile), function (exists) {
3547
+ if (exists) {
3548
+ // Use the domain logo picture
3549
+ try { res.sendFile(obj.path.join(domain.webpublicpath, imagefile)); } catch (ex) { res.sendStatus(404); }
3550
+ } else {
3551
+ // Use the default logo picture
3552
+ try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
3553
+ }
3554
+ });
3555
+ } else if (parent.webPublicOverridePath) {
3556
+ obj.fs.exists(obj.path.join(obj.parent.webPublicOverridePath, imagefile), function (exists) {
3557
+ if (exists) {
3558
+ // Use the override logo picture
3559
+ try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, imagefile)); } catch (ex) { res.sendStatus(404); }
3560
+ } else {
3561
+ // Use the default logo picture
3562
+ try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
3563
+ }
3564
+ });
3565
+ } else {
3566
+ // Use the default logo picture
3567
+ try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
3568
+ }
3569
+ }
3570
+
3571
+ // Download a session recording
3572
+ function handleGetRecordings(req, res) {
3573
+ const domain = checkUserIpAddress(req, res);
3574
+ if (domain == null) return;
3575
+
3576
+ // Check the query
3577
+ if ((domain.sessionrecording == null) || (req.query.file == null) || (obj.common.IsFilenameValid(req.query.file) !== true) || (req.query.file.endsWith('.mcrec') == false)) { res.sendStatus(401); return; }
3578
+
3579
+ // Get the recording path
3580
+ var recordingsPath = null;
3581
+ if (domain.sessionrecording.filepath) { recordingsPath = domain.sessionrecording.filepath; } else { recordingsPath = parent.recordpath; }
3582
+ if (recordingsPath == null) { res.sendStatus(401); return; }
3583
+
3584
+ // Get the user and check user rights
3585
+ var authUserid = null;
3586
+ if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
3587
+ if (authUserid == null) { res.sendStatus(401); return; }
3588
+ const user = obj.users[authUserid];
3589
+ if (user == null) { res.sendStatus(401); return; }
3590
+ if ((user.siteadmin & 512) == 0) { res.sendStatus(401); return; } // Check if we have right to get recordings
3591
+
3592
+ // Send the recorded file
3593
+ setContentDispositionHeader(res, 'application/octet-stream', req.query.file, null, 'recording.mcrec');
3594
+ try { res.sendFile(obj.path.join(recordingsPath, req.query.file)); } catch (ex) { res.sendStatus(404); }
3595
+ }
3596
+
3597
+ // Stream a session recording
3598
+ function handleGetRecordingsWebSocket(ws, req) {
3599
+ var domain = checkAgentIpAddress(ws, req);
3600
+ if (domain == null) { parent.debug('web', 'Got recordings file transfer connection with bad domain or blocked IP address ' + req.clientIp + ', dropping.'); try { ws.close(); } catch (ex) { } return; }
3601
+
3602
+ // Check the query
3603
+ if ((domain.sessionrecording == null) || (req.query.file == null) || (obj.common.IsFilenameValid(req.query.file) !== true) || (req.query.file.endsWith('.mcrec') == false)) { try { ws.close(); } catch (ex) { } return; }
3604
+
3605
+ // Get the recording path
3606
+ var recordingsPath = null;
3607
+ if (domain.sessionrecording.filepath) { recordingsPath = domain.sessionrecording.filepath; } else { recordingsPath = parent.recordpath; }
3608
+ if (recordingsPath == null) { try { ws.close(); } catch (ex) { } return; }
3609
+
3610
+ // Get the user and check user rights
3611
+ var authUserid = null;
3612
+ if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
3613
+ if (authUserid == null) { try { ws.close(); } catch (ex) { } return; }
3614
+ const user = obj.users[authUserid];
3615
+ if (user == null) { try { ws.close(); } catch (ex) { } return; }
3616
+ if ((user.siteadmin & 512) == 0) { try { ws.close(); } catch (ex) { } return; } // Check if we have right to get recordings
3617
+ const filefullpath = obj.path.join(recordingsPath, req.query.file);
3618
+
3619
+ obj.fs.stat(filefullpath, function(err, stats) {
3620
+ if (err) {
3621
+ try { ws.close(); } catch (ex) { } // File does not exist
3622
+ } else {
3623
+ obj.fs.open(filefullpath, 'r', function (err, fd) {
3624
+ if (err == null) {
3625
+ // When data is received from the web socket
3626
+ ws.on('message', function (msg) {
3627
+ if (typeof msg != 'string') return;
3628
+ var command;
3629
+ try { command = JSON.parse(msg); } catch (e) { return; }
3630
+ if ((command == null) || (typeof command.action != 'string')) return;
3631
+ switch (command.action) {
3632
+ case 'get': {
3633
+ const buffer = Buffer.alloc(8 + command.size);
3634
+ //buffer.writeUInt32BE((command.ptr >> 32), 0);
3635
+ buffer.writeUInt32BE((command.ptr & 0xFFFFFFFF), 4);
3636
+ obj.fs.read(fd, buffer, 8, command.size, command.ptr, function (err, bytesRead, buffer) { if (bytesRead > (buffer.length - 8)) { buffer = buffer.slice(0, bytesRead + 8); } ws.send(buffer); });
3637
+ break;
3638
+ }
3639
+ }
3640
+ });
3641
+
3642
+ // If error, do nothing
3643
+ ws.on('error', function (err) { try { ws.close(); } catch (ex) { } obj.fs.close(fd, function (err) { }); });
3644
+
3645
+ // If the web socket is closed
3646
+ ws.on('close', function (req) { try { ws.close(); } catch (ex) { } obj.fs.close(fd, function (err) { }); });
3647
+
3648
+ ws.send(JSON.stringify({ "action": "info", "name": req.query.file, "size": stats.size }));
3649
+ } else {
3650
+ try { ws.close(); } catch (ex) { }
3651
+ }
3652
+ });
3653
+ }
3654
+ });
3655
+ }
3656
+
3657
+ // Serve the player page
3658
+ function handlePlayerRequest(req, res) {
3659
+ const domain = checkUserIpAddress(req, res);
3660
+ if (domain == null) { return; }
3661
+
3662
+ parent.debug('web', 'handlePlayerRequest: sending player');
3663
+ res.set({ 'Cache-Control': 'no-store' });
3664
+ render(req, res, getRenderPage('player', req, domain), getRenderArgs({}, req, domain));
3665
+ }
3666
+
3667
+ // Serve the guest sharing page
3668
+ function handleSharingRequest(req, res) {
3669
+ const domain = getDomain(req, res);
3670
+ if (domain == null) { return; }
3671
+ if (req.query.c == null) { res.sendStatus(404); return; }
3672
+ if (domain.guestdevicesharing === false) { res.sendStatus(404); return; } // This feature is not allowed.
3673
+
3674
+ // Check the inbound guest sharing cookie
3675
+ var c = obj.parent.decodeCookie(req.query.c, obj.parent.invitationLinkEncryptionKey, 9999999999); // Decode cookies with unlimited time.
3676
+ if (c == null) { res.sendStatus(404); return; }
3677
+
3678
+ if (c.a === 5) {
3679
+ // This is the older style sharing cookie with everything encoded within it.
3680
+ // This cookie style gives a very large URL, so it's not used anymore.
3681
+ if ((typeof c.p !== 'number') || (c.p < 1) || (c.p > 7) || (typeof c.uid != 'string') || (typeof c.nid != 'string') || (typeof c.gn != 'string') || (typeof c.cf != 'number') || (typeof c.pid != 'string')) { res.sendStatus(404); return; }
3682
+ handleSharingRequestEx(req, res, domain, c);
3683
+ return;
3684
+ }
3685
+ if (c.a === 6) {
3686
+ // This is the new style sharing cookie, just encodes the pointer to the sharing information in the database.
3687
+ // Gives a much more compact URL.
3688
+ if (typeof c.pid != 'string') { res.sendStatus(404); return; }
3689
+
3690
+ // Check the expired time, expire message.
3691
+ if ((c.e != null) && (c.e <= 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; }
3692
+
3693
+ obj.db.Get('deviceshare-' + c.pid, function (err, docs) {
3694
+ if ((err != null) || (docs == null) || (docs.length != 1)) { res.sendStatus(404); return; }
3695
+ const doc = docs[0];
3696
+
3697
+ // If this is a recurrent share, check if we are at the currect time to make use of it
3698
+ if (typeof doc.recurring == 'number') {
3699
+ const now = Date.now();
3700
+ if (now >= doc.startTime) { // We don't want to move the validity window before the start time
3701
+ const deltaTime = (now - doc.startTime);
3702
+ if (doc.recurring === 1) {
3703
+ // This moves the start time to the next valid daily window
3704
+ const oneDay = (24 * 60 * 60 * 1000);
3705
+ var addition = Math.floor(deltaTime / oneDay);
3706
+ if ((deltaTime - (addition * oneDay)) > (doc.duration * 60000)) { addition++; } // If we are passed the current windows, move to the next one. This will show link as not being valid yet.
3707
+ doc.startTime += (addition * oneDay);
3708
+ } else if (doc.recurring === 2) {
3709
+ // This moves the start time to the next valid weekly window
3710
+ const oneWeek = (7 * 24 * 60 * 60 * 1000);
3711
+ var addition = Math.floor(deltaTime / oneWeek);
3712
+ if ((deltaTime - (addition * oneWeek)) > (doc.duration * 60000)) { addition++; } // If we are passed the current windows, move to the next one. This will show link as not being valid yet.
3713
+ doc.startTime += (addition * oneWeek);
3714
+ }
3715
+ }
3716
+ }
3717
+
3718
+ // Generate an old style cookie from the information in the database
3719
+ var cookie = { a: 5, p: doc.p, gn: doc.guestName, nid: doc.nodeid, cf: doc.consent, pid: doc.publicid, k: doc.extrakey };
3720
+ if (doc.userid) { cookie.uid = doc.userid; }
3721
+ if ((cookie.userid == null) && (cookie.pid.startsWith('AS:node/'))) { cookie.nouser = 1; }
3722
+ if (doc.startTime != null) {
3723
+ if (doc.expireTime != null) { cookie.start = doc.startTime; cookie.expire = doc.expireTime; }
3724
+ else if (doc.duration != null) { cookie.start = doc.startTime; cookie.expire = doc.startTime + (doc.duration * 60000); }
3725
+ }
3726
+ if (doc.viewOnly === true) { cookie.vo = 1; }
3727
+ handleSharingRequestEx(req, res, domain, cookie);
3728
+ });
3729
+ return;
3730
+ }
3731
+ res.sendStatus(404); return;
3732
+ }
3733
+
3734
+ // Serve the guest sharing page
3735
+ function handleSharingRequestEx(req, res, domain, c) {
3736
+ // Check the expired time, expire message.
3737
+ if ((c.expire != null) && (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; }
3738
+
3739
+ // Check the public id
3740
+ obj.db.GetAllTypeNodeFiltered([c.nid], domain.id, 'deviceshare', null, function (err, docs) {
3741
+ // Check if any desktop sharing links are present, expire message.
3742
+ 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; }
3743
+
3744
+ // Search for the device share public identifier, expire message.
3745
+ var found = false;
3746
+ for (var i = 0; i < docs.length; i++) { if ((docs[i].publicid == c.pid) && ((docs[i].extrakey == null) || (docs[i].extrakey === c.k))) { found = true; } }
3747
+ 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; }
3748
+
3749
+ // Get information about this node
3750
+ obj.db.Get(c.nid, function (err, nodes) {
3751
+ if ((err != null) || (nodes == null) || (nodes.length != 1)) { res.sendStatus(404); return; }
3752
+ var node = nodes[0];
3753
+
3754
+ // Check the start time, not yet valid message.
3755
+ if ((c.start != null) && (c.expire != null) && ((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; }
3756
+
3757
+ // Looks good, let's create the outbound session cookies.
3758
+ // Consent flags are 1 = Notify, 8 = Prompt, 64 = Privacy Bar.
3759
+ const authCookieData = { userid: c.uid, domainid: domain.id, nid: c.nid, ip: req.clientIp, p: c.p, gn: c.gn, cf: c.cf, r: 8, expire: c.expire, pid: c.pid, vo: c.vo };
3760
+ if ((authCookieData.userid == null) && (authCookieData.pid.startsWith('AS:node/'))) { authCookieData.nouser = 1; }
3761
+ if (c.k != null) { authCookieData.k = c.k; }
3762
+ const authCookie = obj.parent.encodeCookie(authCookieData, obj.parent.loginCookieEncryptionKey);
3763
+
3764
+ // Server features
3765
+ var features2 = 0;
3766
+ if (obj.args.allowhighqualitydesktop !== false) { features2 += 1; } // Enable AllowHighQualityDesktop (Default true)
3767
+
3768
+ // Lets respond by sending out the desktop viewer.
3769
+ var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
3770
+ parent.debug('web', 'handleSharingRequest: Sending guest sharing page for \"' + c.uid + '\", guest \"' + c.gn + '\".');
3771
+ res.set({ 'Cache-Control': 'no-store' });
3772
+ render(req, res, getRenderPage('sharing', 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, nodeName: encodeURIComponent(node.name).replace(/'/g, '%27'), features: c.p, features2: features2 }, req, domain));
3773
+ });
3774
+ });
3775
+ }
3776
+
3777
+ // Handle domain redirection
3778
+ obj.handleDomainRedirect = function (req, res) {
3779
+ const domain = checkUserIpAddress(req, res);
3780
+ if (domain == null) { return; }
3781
+ if (domain.redirects == null) { res.sendStatus(404); return; }
3782
+ var urlArgs = '', urlName = null, splitUrl = req.originalUrl.split('?');
3783
+ if (splitUrl.length > 1) { urlArgs = '?' + splitUrl[1]; }
3784
+ if ((splitUrl.length > 0) && (splitUrl[0].length > 1)) { urlName = splitUrl[0].substring(1).toLowerCase(); }
3785
+ if ((urlName == null) || (domain.redirects[urlName] == null) || (urlName[0] == '_')) { res.sendStatus(404); return; }
3786
+ if (domain.redirects[urlName] == '~showversion') {
3787
+ // Show the current version
3788
+ res.end('MeshCentral v' + obj.parent.currentVer);
3789
+ } else {
3790
+ // Perform redirection
3791
+ res.redirect(domain.redirects[urlName] + urlArgs + getQueryPortion(req));
3792
+ }
3793
+ }
3794
+
3795
+ // Take a "user/domain/userid/path/file" format and return the actual server disk file path if access is allowed
3796
+ obj.getServerFilePath = function (user, domain, path) {
3797
+ var splitpath = path.split('/'), serverpath = obj.path.join(obj.filespath, 'domain'), filename = '';
3798
+ if ((splitpath.length < 3) || (splitpath[0] != 'user' && splitpath[0] != 'mesh') || (splitpath[1] != domain.id)) return null; // Basic validation
3799
+ var objid = splitpath[0] + '/' + splitpath[1] + '/' + splitpath[2];
3800
+ if (splitpath[0] == 'user' && (objid != user._id)) return null; // User validation, only self allowed
3801
+ if (splitpath[0] == 'mesh') { if ((obj.GetMeshRights(user, objid) & 32) == 0) { return null; } } // Check mesh server file rights
3802
+ if (splitpath[1] != '') { serverpath += '-' + splitpath[1]; } // Add the domain if needed
3803
+ serverpath += ('/' + splitpath[0] + '-' + splitpath[2]);
3804
+ 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
3805
+ return { fullpath: obj.path.resolve(obj.filespath, serverpath), path: serverpath, name: filename, quota: obj.getQuota(objid, domain) };
3806
+ };
3807
+
3808
+ // Return the maximum number of bytes allowed in the user account "My Files".
3809
+ obj.getQuota = function (objid, domain) {
3810
+ if (objid == null) return 0;
3811
+ if (objid.startsWith('user/')) {
3812
+ var user = obj.users[objid];
3813
+ if (user == null) return 0;
3814
+ if (user.siteadmin == 0xFFFFFFFF) return null; // Administrators have no user limit
3815
+ if ((user.quota != null) && (typeof user.quota == 'number')) { return user.quota; }
3816
+ if ((domain != null) && (domain.userquota != null) && (typeof domain.userquota == 'number')) { return domain.userquota; }
3817
+ return null; // By default, the user will have no limit
3818
+ } else if (objid.startsWith('mesh/')) {
3819
+ var mesh = obj.meshes[objid];
3820
+ if (mesh == null) return 0;
3821
+ if ((mesh.quota != null) && (typeof mesh.quota == 'number')) { return mesh.quota; }
3822
+ if ((domain != null) && (domain.meshquota != null) && (typeof domain.meshquota == 'number')) { return domain.meshquota; }
3823
+ return null; // By default, the mesh will have no limit
3824
+ }
3825
+ return 0;
3826
+ };
3827
+
3828
+ // Download a file from the server
3829
+ function handleDownloadFile(req, res) {
3830
+ const domain = checkUserIpAddress(req, res);
3831
+ if (domain == null) { return; }
3832
+ if ((req.query.link == null) || (req.session == null) || (req.session.userid == null) || (domain == null) || (domain.userQuota == -1)) { res.sendStatus(404); return; }
3833
+ const user = obj.users[req.session.userid];
3834
+ if (user == null) { res.sendStatus(404); return; }
3835
+ const file = obj.getServerFilePath(user, domain, req.query.link);
3836
+ if (file == null) { res.sendStatus(404); return; }
3837
+ setContentDispositionHeader(res, 'application/octet-stream', file.name, null, 'file.bin');
3838
+ obj.fs.exists(file.fullpath, function (exists) { if (exists == true) { res.sendFile(file.fullpath); } else { res.sendStatus(404); } });
3839
+ }
3840
+
3841
+ // Upload a MeshCore.js file to the server
3842
+ function handleUploadMeshCoreFile(req, res) {
3843
+ const domain = checkUserIpAddress(req, res);
3844
+ if (domain == null) { return; }
3845
+ if (domain.id !== '') { res.sendStatus(401); return; }
3846
+
3847
+ var authUserid = null;
3848
+ if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
3849
+
3850
+ const multiparty = require('multiparty');
3851
+ const form = new multiparty.Form();
3852
+ form.parse(req, function (err, fields, files) {
3853
+ // If an authentication cookie is embedded in the form, use that.
3854
+ if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
3855
+ var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
3856
+ if ((loginCookie != null) && (loginCookie.ip != null) && checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
3857
+ if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
3858
+ }
3859
+ if (authUserid == null) { res.sendStatus(401); return; }
3860
+ if ((fields == null) || (fields.attrib == null) || (fields.attrib.length != 1)) { res.sendStatus(404); return; }
3861
+
3862
+ // Get the user
3863
+ const user = obj.users[authUserid];
3864
+ if (user == null) { res.sendStatus(401); return; } // Check this user exists
3865
+
3866
+ // Get the node and check node rights
3867
+ const nodeid = fields.attrib[0];
3868
+ obj.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
3869
+ if ((node == null) || (rights != 0xFFFFFFFF) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device
3870
+ for (var i in files.files) {
3871
+ var file = files.files[i];
3872
+ obj.fs.readFile(file.path, 'utf8', function (err, data) {
3873
+ if (err != null) return;
3874
+ data = obj.common.IntToStr(0) + data; // Add the 4 bytes encoding type & flags (Set to 0 for raw)
3875
+ obj.sendMeshAgentCore(user, domain, fields.attrib[0], 'custom', data); // Upload the core
3876
+ try { obj.fs.unlinkSync(file.path); } catch (e) { }
3877
+ });
3878
+ }
3879
+ res.send('');
3880
+ });
3881
+ });
3882
+ }
3883
+
3884
+ // Upload a MeshCore.js file to the server
3885
+ function handleOneClickRecoveryFile(req, res) {
3886
+ const domain = checkUserIpAddress(req, res);
3887
+ if (domain == null) { return; }
3888
+ if (domain.id !== '') { res.sendStatus(401); return; }
3889
+
3890
+ var authUserid = null;
3891
+ if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
3892
+
3893
+ const multiparty = require('multiparty');
3894
+ const form = new multiparty.Form();
3895
+ form.parse(req, function (err, fields, files) {
3896
+ // If an authentication cookie is embedded in the form, use that.
3897
+ if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
3898
+ var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
3899
+ if ((loginCookie != null) && (loginCookie.ip != null) && checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
3900
+ if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
3901
+ }
3902
+ if (authUserid == null) { res.sendStatus(401); return; }
3903
+ if ((fields == null) || (fields.attrib == null) || (fields.attrib.length != 1)) { res.sendStatus(404); return; }
3904
+
3905
+ // Get the user
3906
+ const user = obj.users[authUserid];
3907
+ if (user == null) { res.sendStatus(401); return; } // Check this user exists
3908
+
3909
+ // Get the node and check node rights
3910
+ const nodeid = fields.attrib[0];
3911
+ obj.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
3912
+ if ((node == null) || (rights != 0xFFFFFFFF) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device
3913
+ for (var i in files.files) {
3914
+ var file = files.files[i];
3915
+
3916
+ // Event Intel AMT One Click Recovery, this will cause Intel AMT wake operations on this and other servers.
3917
+ parent.DispatchEvent('*', obj, { action: 'oneclickrecovery', userid: user._id, username: user.name, nodeids: [node._id], domain: domain.id, nolog: 1, file: file.path });
3918
+
3919
+ //try { obj.fs.unlinkSync(file.path); } catch (e) { } // TODO: Remove this file after 30 minutes.
3920
+ }
3921
+ res.send('');
3922
+ });
3923
+ });
3924
+ }
3925
+
3926
+ // Upload a file to the server
3927
+ function handleUploadFile(req, res) {
3928
+ const domain = checkUserIpAddress(req, res);
3929
+ if (domain == null) { return; }
3930
+ if (domain.userQuota == -1) { res.sendStatus(401); return; }
3931
+ var authUserid = null;
3932
+ if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
3933
+ const multiparty = require('multiparty');
3934
+ const form = new multiparty.Form();
3935
+ form.parse(req, function (err, fields, files) {
3936
+ // If an authentication cookie is embedded in the form, use that.
3937
+ if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
3938
+ var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
3939
+ if ((loginCookie != null) && (loginCookie.ip != null) && checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
3940
+ if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
3941
+ }
3942
+ if (authUserid == null) { res.sendStatus(401); return; }
3943
+
3944
+ // Get the user
3945
+ const user = obj.users[authUserid];
3946
+ if ((user == null) || (user.siteadmin & 8) == 0) { res.sendStatus(401); return; } // Check if we have file rights
3947
+
3948
+ if ((fields == null) || (fields.link == null) || (fields.link.length != 1)) { /*console.log('UploadFile, Invalid Fields:', fields, files);*/ console.log('err4'); res.sendStatus(404); return; }
3949
+ var xfile = null;
3950
+ try { xfile = obj.getServerFilePath(user, domain, decodeURIComponent(fields.link[0])); } catch (ex) { }
3951
+ if (xfile == null) { res.sendStatus(404); return; }
3952
+ // Get total bytes in the path
3953
+ var totalsize = readTotalFileSize(xfile.fullpath);
3954
+ if ((xfile.quota == null) || (totalsize < xfile.quota)) { // Check if the quota is not already broken
3955
+ if (fields.name != null) {
3956
+
3957
+ // See if we need to create the folder
3958
+ var domainx = 'domain';
3959
+ if (domain.id.length > 0) { domainx = 'domain-' + usersplit[1]; }
3960
+ try { obj.fs.mkdirSync(obj.parent.filespath); } catch (ex) { }
3961
+ try { obj.fs.mkdirSync(obj.parent.path.join(obj.parent.filespath, domainx)); } catch (ex) { }
3962
+ try { obj.fs.mkdirSync(xfile.fullpath); } catch (ex) { }
3963
+
3964
+ // Upload method where all the file data is within the fields.
3965
+ var names = fields.name[0].split('*'), sizes = fields.size[0].split('*'), types = fields.type[0].split('*'), datas = fields.data[0].split('*');
3966
+ if ((names.length == sizes.length) && (types.length == datas.length) && (names.length == types.length)) {
3967
+ for (var i = 0; i < names.length; i++) {
3968
+ if (obj.common.IsFilenameValid(names[i]) == false) { res.sendStatus(404); return; }
3969
+ var filedata = Buffer.from(datas[i].split(',')[1], 'base64');
3970
+ if ((xfile.quota == null) || ((totalsize + filedata.length) < xfile.quota)) { // Check if quota would not be broken if we add this file
3971
+ // Create the user folder if needed
3972
+ (function (fullpath, filename, filedata) {
3973
+ obj.fs.mkdir(xfile.fullpath, function () {
3974
+ // Write the file
3975
+ obj.fs.writeFile(obj.path.join(xfile.fullpath, filename), filedata, function () {
3976
+ obj.parent.DispatchEvent([user._id], obj, 'updatefiles'); // Fire an event causing this user to update this files
3977
+ });
3978
+ });
3979
+ })(xfile.fullpath, names[i], filedata);
3980
+ } else {
3981
+ // Send a notification
3982
+ obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: "Disk quota exceed", value: names[i], nolog: 1, id: Math.random() });
3983
+ }
3984
+ }
3985
+ }
3986
+ } else {
3987
+ // More typical upload method, the file data is in a multipart mime post.
3988
+ for (var i in files.files) {
3989
+ var file = files.files[i], fpath = obj.path.join(xfile.fullpath, file.originalFilename);
3990
+ 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
3991
+
3992
+ // See if we need to create the folder
3993
+ var domainx = 'domain';
3994
+ if (domain.id.length > 0) { domainx = 'domain-' + domain.id; }
3995
+ try { obj.fs.mkdirSync(obj.parent.filespath); } catch (e) { }
3996
+ try { obj.fs.mkdirSync(obj.parent.path.join(obj.parent.filespath, domainx)); } catch (e) { }
3997
+ try { obj.fs.mkdirSync(xfile.fullpath); } catch (e) { }
3998
+
3999
+ // Rename the file
4000
+ obj.fs.rename(file.path, fpath, function (err) {
4001
+ if (err && (err.code === 'EXDEV')) {
4002
+ // On some Linux, the rename will fail with a "EXDEV" error, do a copy+unlink instead.
4003
+ obj.common.copyFile(file.path, fpath, function (err) {
4004
+ obj.fs.unlink(file.path, function (err) {
4005
+ obj.parent.DispatchEvent([user._id], obj, 'updatefiles'); // Fire an event causing this user to update this files
4006
+ });
4007
+ });
4008
+ } else {
4009
+ obj.parent.DispatchEvent([user._id], obj, 'updatefiles'); // Fire an event causing this user to update this files
4010
+ }
4011
+ });
4012
+ } else {
4013
+ // Send a notification
4014
+ obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: "Disk quota exceed", value: file.originalFilename, nolog: 1, id: Math.random() });
4015
+ try { obj.fs.unlink(file.path, function (err) { }); } catch (e) { }
4016
+ }
4017
+ }
4018
+ }
4019
+ } else {
4020
+ // Send a notification
4021
+ obj.parent.DispatchEvent([user._id], obj, { action: 'notify', value: "Disk quota exceed", nolog: 1, id: Math.random() });
4022
+ }
4023
+ res.send('');
4024
+ });
4025
+ }
4026
+
4027
+ // Upload a file to the server and then batch upload to many agents
4028
+ function handleUploadFileBatch(req, res) {
4029
+ const domain = checkUserIpAddress(req, res);
4030
+ if (domain == null) { return; }
4031
+ var authUserid = null;
4032
+ if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
4033
+ const multiparty = require('multiparty');
4034
+ const form = new multiparty.Form();
4035
+ form.parse(req, function (err, fields, files) {
4036
+ // If an authentication cookie is embedded in the form, use that.
4037
+ if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
4038
+ var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
4039
+ if ((loginCookie != null) && (loginCookie.ip != null) && checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
4040
+ if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
4041
+ }
4042
+ if (authUserid == null) { res.sendStatus(401); return; }
4043
+
4044
+ // Get the user
4045
+ const user = obj.users[authUserid];
4046
+ if (user == null) { parent.debug('web', 'Batch upload error, invalid user.'); res.sendStatus(401); return; } // Check if user exists
4047
+
4048
+ // Get fields
4049
+ if ((fields == null) || (fields.nodeIds == null) || (fields.nodeIds.length != 1)) { res.sendStatus(404); return; }
4050
+ var cmd = { nodeids: fields.nodeIds[0].split(','), files: [], user: user, domain: domain, overwrite: false, createFolder: false };
4051
+ if ((fields.winpath != null) && (fields.winpath.length == 1)) { cmd.windowsPath = fields.winpath[0]; }
4052
+ if ((fields.linuxpath != null) && (fields.linuxpath.length == 1)) { cmd.linuxPath = fields.linuxpath[0]; }
4053
+ if ((fields.overwriteFiles != null) && (fields.overwriteFiles.length == 1) && (fields.overwriteFiles[0] == 'on')) { cmd.overwrite = true; }
4054
+ if ((fields.createFolder != null) && (fields.createFolder.length == 1) && (fields.createFolder[0] == 'on')) { cmd.createFolder = true; }
4055
+
4056
+ // Check if we have at least one target path
4057
+ if ((cmd.windowsPath == null) && (cmd.linuxPath == null)) {
4058
+ parent.debug('web', 'Batch upload error, invalid fields: ' + JSON.stringify(fields));
4059
+ res.send('');
4060
+ return;
4061
+ }
4062
+
4063
+ // Get server temporary path
4064
+ var serverpath = obj.path.join(obj.filespath, 'tmp')
4065
+ try { obj.fs.mkdirSync(obj.parent.filespath); } catch (ex) { }
4066
+ try { obj.fs.mkdirSync(serverpath); } catch (ex) { }
4067
+
4068
+ // More typical upload method, the file data is in a multipart mime post.
4069
+ for (var i in files.files) {
4070
+ var file = files.files[i], ftarget = getRandomPassword() + '-' + file.originalFilename, fpath = obj.path.join(serverpath, ftarget);
4071
+ cmd.files.push({ name: file.originalFilename, target: ftarget });
4072
+ // Rename the file
4073
+ obj.fs.rename(file.path, fpath, function (err) {
4074
+ if (err && (err.code === 'EXDEV')) {
4075
+ // On some Linux, the rename will fail with a "EXDEV" error, do a copy+unlink instead.
4076
+ obj.common.copyFile(file.path, fpath, function (err) { obj.fs.unlink(file.path, function (err) { }); });
4077
+ }
4078
+ });
4079
+ }
4080
+
4081
+ // Instruct one of more agents to download a URL to a given local drive location.
4082
+ var tlsCertHash = null;
4083
+ if ((parent.args.ignoreagenthashcheck == null) || (parent.args.ignoreagenthashcheck === false)) { // TODO: If ignoreagenthashcheck is an array of IP addresses, not sure how to handle this.
4084
+ tlsCertHash = obj.webCertificateFullHashs[cmd.domain.id];
4085
+ if (tlsCertHash != null) { tlsCertHash = Buffer.from(tlsCertHash, 'binary').toString('hex'); }
4086
+ }
4087
+ for (var i in cmd.nodeids) {
4088
+ obj.GetNodeWithRights(cmd.domain, cmd.user, cmd.nodeids[i], function (node, rights, visible) {
4089
+ if ((node == null) || ((rights & 8) == 0) || (visible == false)) return; // We don't have remote control rights to this device
4090
+ var agentPath = (((node.agent.id > 0) && (node.agent.id < 5)) || (node.agent.id == 34)) ? cmd.windowsPath : cmd.linuxPath;
4091
+ if (agentPath == null) return;
4092
+
4093
+ // Compute user consent
4094
+ var consent = 0;
4095
+ var mesh = obj.meshes[node.meshid];
4096
+ if (typeof domain.userconsentflags == 'number') { consent |= domain.userconsentflags; } // Add server required consent flags
4097
+ if ((mesh != null) && (typeof mesh.consent == 'number')) { consent |= mesh.consent; } // Add device group user consent
4098
+ if (typeof node.consent == 'number') { consent |= node.consent; } // Add node user consent
4099
+ if (typeof user.consent == 'number') { consent |= user.consent; } // Add user consent
4100
+
4101
+ // Check if we need to add consent flags because of a user group link
4102
+ if ((mesh != null) && (user.links != null) && (user.links[mesh._id] == null) && (user.links[node._id] == null)) {
4103
+ // This user does not have a direct link to the device group or device. Find all user groups the would cause the link.
4104
+ for (var i in user.links) {
4105
+ var ugrp = obj.userGroups[i];
4106
+ if ((ugrp != null) && (ugrp.consent != null) && (ugrp.links != null) && ((ugrp.links[mesh._id] != null) || (ugrp.links[node._id] != null))) {
4107
+ consent |= ugrp.consent; // Add user group consent flags
4108
+ }
4109
+ }
4110
+ }
4111
+
4112
+ // Event that this operation is being performed.
4113
+ var targets = obj.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', cmd.user._id]);
4114
+ var msgid = 103; // "Batch upload of {0} file(s) to folder {1}"
4115
+ 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 };
4116
+ parent.DispatchEvent(targets, obj, event);
4117
+
4118
+ // Send the agent commands to perform the batch upload operation
4119
+ for (var f in cmd.files) {
4120
+ if (cmd.files[f].name != null) {
4121
+ const acmd = { action: 'wget', userid: user._id, username: user.name, realname: user.realname, remoteaddr: req.clientIp, consent: consent, rights: rights, 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 };
4122
+ var agent = obj.wsagents[node._id];
4123
+ if (agent != null) { try { agent.send(JSON.stringify(acmd)); } catch (ex) { } }
4124
+ // TODO: Add support for peer servers.
4125
+ }
4126
+ }
4127
+ });
4128
+ }
4129
+
4130
+ res.send('');
4131
+ });
4132
+ }
4133
+
4134
+ // Subscribe to all events we are allowed to receive
4135
+ obj.subscribe = function (userid, target) {
4136
+ const user = obj.users[userid];
4137
+ const subscriptions = [userid, 'server-allusers'];
4138
+ if (user.siteadmin != null) {
4139
+ // Allow full site administrators of users with all events rights to see all events.
4140
+ if ((user.siteadmin == 0xFFFFFFFF) || ((user.siteadmin & 2048) != 0)) { subscriptions.push('*'); }
4141
+ else if ((user.siteadmin & 2) != 0) {
4142
+ if ((user.groups == null) || (user.groups.length == 0)) {
4143
+ // Subscribe to all user changes
4144
+ subscriptions.push('server-users');
4145
+ } else {
4146
+ // Subscribe to user changes for some groups
4147
+ for (var i in user.groups) { subscriptions.push('server-users:' + i); }
4148
+ }
4149
+ }
4150
+ }
4151
+ if (user.links != null) { for (var i in user.links) { subscriptions.push(i); } }
4152
+ obj.parent.RemoveAllEventDispatch(target);
4153
+ obj.parent.AddEventDispatch(subscriptions, target);
4154
+ return subscriptions;
4155
+ };
4156
+
4157
+ // Handle a web socket relay request
4158
+ function handleRelayWebSocket(ws, req, domain, user, cookie) {
4159
+ if (!(req.query.host)) { console.log('ERR: No host target specified'); try { ws.close(); } catch (e) { } return; } // Disconnect websocket
4160
+ parent.debug('web', 'Websocket relay connected from ' + user.name + ' for ' + req.query.host + '.');
4161
+
4162
+ try { ws._socket.setKeepAlive(true, 240000); } catch (ex) { } // Set TCP keep alive
4163
+
4164
+ // Fetch information about the target
4165
+ obj.db.Get(req.query.host, function (err, docs) {
4166
+ if (docs.length == 0) { console.log('ERR: Node not found'); try { ws.close(); } catch (e) { } return; } // Disconnect websocket
4167
+ var node = docs[0];
4168
+ if (!node.intelamt) { console.log('ERR: Not AMT node'); try { ws.close(); } catch (e) { } return; } // Disconnect websocket
4169
+
4170
+ // Check if this user has permission to manage this computer
4171
+ if ((obj.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0) { console.log('ERR: Access denied (3)'); try { ws.close(); } catch (e) { } return; }
4172
+
4173
+ // Check what connectivity is available for this node
4174
+ var state = parent.GetConnectivityState(req.query.host);
4175
+ var conn = 0;
4176
+ if (!state || state.connectivity == 0) { parent.debug('web', 'ERR: No routing possible (1)'); try { ws.close(); } catch (e) { } return; } else { conn = state.connectivity; }
4177
+
4178
+ // Check what server needs to handle this connection
4179
+ 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
4180
+ var server = obj.parent.GetRoutingServerId(req.query.host, 2); // Check for Intel CIRA connection
4181
+ if (server != null) {
4182
+ if (server.serverid != obj.parent.serverId) {
4183
+ // Do local Intel CIRA routing using a different server
4184
+ parent.debug('web', 'Route Intel AMT CIRA connection to peer server: ' + server.serverid);
4185
+ obj.parent.multiServer.createPeerRelay(ws, req, server.serverid, user);
4186
+ return;
4187
+ }
4188
+ } else {
4189
+ server = obj.parent.GetRoutingServerId(req.query.host, 4); // Check for local Intel AMT connection
4190
+ if ((server != null) && (server.serverid != obj.parent.serverId)) {
4191
+ // Do local Intel AMT routing using a different server
4192
+ parent.debug('web', 'Route Intel AMT direct connection to peer server: ' + server.serverid);
4193
+ obj.parent.multiServer.createPeerRelay(ws, req, server.serverid, user);
4194
+ return;
4195
+ }
4196
+ }
4197
+ }
4198
+
4199
+ // Setup session recording if needed
4200
+ 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
4201
+ // Check again if we need to do recording
4202
+ var record = true;
4203
+
4204
+ // Check user or device group recording
4205
+ if ((typeof domain.sessionrecording == 'object') && ((domain.sessionrecording.onlyselectedusers === true) || (domain.sessionrecording.onlyselecteddevicegroups === true))) {
4206
+ record = false;
4207
+
4208
+ // Check device group recording
4209
+ if (domain.sessionrecording.onlyselecteddevicegroups === true) {
4210
+ var mesh = obj.meshes[node.meshid];
4211
+ if ((mesh.flags != null) && ((mesh.flags & 4) != 0)) { record = true; } // Record the session
4212
+ }
4213
+
4214
+ // Check user recording
4215
+ if (domain.sessionrecording.onlyselectedusers === true) {
4216
+ if ((user.flags != null) && ((user.flags & 2) != 0)) { record = true; } // Record the session
4217
+ }
4218
+ }
4219
+
4220
+ if (record == true) {
4221
+ var now = new Date(Date.now());
4222
+ var recFilename = 'relaysession' + ((domain.id == '') ? '' : '-') + domain.id + '-' + now.getUTCFullYear() + '-' + obj.common.zeroPad(now.getUTCMonth() + 1, 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'
4223
+ var recFullFilename = null;
4224
+ if (domain.sessionrecording.filepath) {
4225
+ try { obj.fs.mkdirSync(domain.sessionrecording.filepath); } catch (e) { }
4226
+ recFullFilename = obj.path.join(domain.sessionrecording.filepath, recFilename);
4227
+ } else {
4228
+ try { obj.fs.mkdirSync(parent.recordpath); } catch (e) { }
4229
+ recFullFilename = obj.path.join(parent.recordpath, recFilename);
4230
+ }
4231
+ var fd = obj.fs.openSync(recFullFilename, 'w');
4232
+ if (fd != null) {
4233
+ // Write the recording file header
4234
+ 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() })
4235
+ recordingEntry(fd, 1, 0, firstBlock, function () { });
4236
+ ws.logfile = { fd: fd, lock: false };
4237
+ if (req.query.p == 2) { ws.send(Buffer.from(String.fromCharCode(0xF0), 'binary')); } // Intel AMT Redirection: Indicate the session is being recorded
4238
+ }
4239
+ }
4240
+ }
4241
+
4242
+ // If Intel AMT CIRA connection is available, use it
4243
+ var ciraconn = parent.mpsserver.GetConnectionToNode(req.query.host, null, false);
4244
+ if (ciraconn != null) {
4245
+ parent.debug('web', 'Opening relay CIRA channel connection to ' + req.query.host + '.');
4246
+
4247
+ // TODO: If the CIRA connection is a relay or LMS connection, we can't detect the TLS state like this.
4248
+ // Compute target port, look at the CIRA port mappings, if non-TLS is allowed, use that, if not use TLS
4249
+ var port = 16993;
4250
+ //if (node.intelamt.tls == 0) port = 16992; // DEBUG: Allow TLS flag to set TLS mode within CIRA
4251
+ if (ciraconn.tag.boundPorts.indexOf(16992) >= 0) port = 16992; // RELEASE: Always use non-TLS mode if available within CIRA
4252
+ if (req.query.p == 2) port += 2;
4253
+
4254
+ // Setup a new CIRA channel
4255
+ if ((port == 16993) || (port == 16995)) {
4256
+ // Perform TLS
4257
+ var ser = new SerialTunnel();
4258
+ var chnl = parent.mpsserver.SetupChannel(ciraconn, port);
4259
+
4260
+ // Let's chain up the TLSSocket <-> SerialTunnel <-> CIRA APF (chnl)
4261
+ // Anything that needs to be forwarded by SerialTunnel will be encapsulated by chnl write
4262
+ ser.forwardwrite = function (data) { if (data.length > 0) { chnl.write(data); } }; // TLS ---> CIRA
4263
+
4264
+ // When APF tunnel return something, update SerialTunnel buffer
4265
+ chnl.onData = function (ciraconn, data) { if (data.length > 0) { try { ser.updateBuffer(data); } catch (ex) { console.log(ex); } } }; // CIRA ---> TLS
4266
+
4267
+ // Handle CIRA tunnel state change
4268
+ chnl.onStateChange = function (ciraconn, state) {
4269
+ parent.debug('webrelay', 'Relay TLS CIRA state change', state);
4270
+ if (state == 0) { try { ws.close(); } catch (e) { } }
4271
+ if (state == 2) {
4272
+ // TLSSocket to encapsulate TLS communication, which then tunneled via SerialTunnel an then wrapped through CIRA APF
4273
+ 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 };
4274
+ if (req.query.tls1only == 1) { tlsoptions.secureProtocol = 'TLSv1_method'; }
4275
+ var tlsock = obj.tls.connect(tlsoptions, function () { parent.debug('webrelay', "CIRA Secure TLS Connection"); ws._socket.resume(); });
4276
+ tlsock.chnl = chnl;
4277
+ tlsock.setEncoding('binary');
4278
+ tlsock.on('error', function (err) { parent.debug('webrelay', "CIRA TLS Connection Error", err); });
4279
+
4280
+ // Decrypted tunnel from TLS communcation to be forwarded to websocket
4281
+ tlsock.on('data', function (data) {
4282
+ // AMT/TLS ---> WS
4283
+ if (ws.interceptor) { data = ws.interceptor.processAmtData(data); } // Run data thru interceptor
4284
+ try { ws.send(data); } catch (ex) { }
4285
+ });
4286
+
4287
+ // If TLS is on, forward it through TLSSocket
4288
+ ws.forwardclient = tlsock;
4289
+ ws.forwardclient.xtls = 1;
4290
+
4291
+ ws.forwardclient.onStateChange = function (ciraconn, state) {
4292
+ parent.debug('webrelay', 'Relay CIRA state change', state);
4293
+ if (state == 0) { try { ws.close(); } catch (e) { } }
4294
+ };
4295
+
4296
+ ws.forwardclient.onData = function (ciraconn, data) {
4297
+ // Run data thru interceptor
4298
+ if (ws.interceptor) { data = ws.interceptor.processAmtData(data); }
4299
+
4300
+ if (data.length > 0) {
4301
+ if (ws.logfile == null) {
4302
+ try { ws.send(data); } catch (e) { }
4303
+ } else {
4304
+ // Log to recording file
4305
+ recordingEntry(ws.logfile.fd, 2, 0, data, function () { try { ws.send(data); } catch (ex) { console.log(ex); } }); // TODO: Add TLS support
4306
+ }
4307
+ }
4308
+ };
4309
+
4310
+ // TODO: Flow control? (Dont' really need it with AMT, but would be nice)
4311
+ ws.forwardclient.onSendOk = function (ciraconn) { };
4312
+ }
4313
+ };
4314
+ } else {
4315
+ // Without TLS
4316
+ ws.forwardclient = parent.mpsserver.SetupChannel(ciraconn, port);
4317
+ ws.forwardclient.xtls = 0;
4318
+ ws._socket.resume();
4319
+
4320
+ ws.forwardclient.onStateChange = function (ciraconn, state) {
4321
+ parent.debug('webrelay', 'Relay CIRA state change', state);
4322
+ if (state == 0) { try { ws.close(); } catch (e) { } }
4323
+ };
4324
+
4325
+ ws.forwardclient.onData = function (ciraconn, data) {
4326
+ //parent.debug('webrelaydata', 'Relay CIRA data to WS', data.length);
4327
+
4328
+ // Run data thru interceptorp
4329
+ if (ws.interceptor) { data = ws.interceptor.processAmtData(data); }
4330
+
4331
+ //console.log('AMT --> WS', Buffer.from(data, 'binary').toString('hex'));
4332
+ if (data.length > 0) {
4333
+ if (ws.logfile == null) {
4334
+ try { ws.send(data); } catch (e) { }
4335
+ } else {
4336
+ // Log to recording file
4337
+ recordingEntry(ws.logfile.fd, 2, 0, data, function () { try { ws.send(data); } catch (ex) { console.log(ex); } });
4338
+ }
4339
+ }
4340
+ };
4341
+
4342
+ // TODO: Flow control? (Dont' really need it with AMT, but would be nice)
4343
+ ws.forwardclient.onSendOk = function (ciraconn) { };
4344
+ }
4345
+
4346
+ // When data is received from the web socket, forward the data into the associated CIRA cahnnel.
4347
+ // If the CIRA connection is pending, the CIRA channel has built-in buffering, so we are ok sending anyway.
4348
+ ws.on('message', function (data) {
4349
+ //parent.debug('webrelaydata', 'Relay WS data to CIRA', data.length);
4350
+ if (typeof data == 'string') { data = Buffer.from(data, 'binary'); }
4351
+
4352
+ // WS ---> AMT/TLS
4353
+ if (ws.interceptor) { data = ws.interceptor.processBrowserData(data); } // Run data thru interceptor
4354
+
4355
+ // Log to recording file
4356
+ if (ws.logfile == null) {
4357
+ // Forward data to the associated TCP connection.
4358
+ try { ws.forwardclient.write(data); } catch (ex) { }
4359
+ } else {
4360
+ // Log to recording file
4361
+ recordingEntry(ws.logfile.fd, 2, 2, data, function () { try { ws.forwardclient.write(data); } catch (ex) { } });
4362
+ }
4363
+ });
4364
+
4365
+ // If error, close the associated TCP connection.
4366
+ ws.on('error', function (err) {
4367
+ console.log('CIRA server websocket error from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.');
4368
+ parent.debug('webrelay', 'Websocket relay closed on error.');
4369
+
4370
+ // Websocket closed, close the CIRA channel and TLS session.
4371
+ if (ws.forwardclient) {
4372
+ if (ws.forwardclient.close) { ws.forwardclient.close(); } // NonTLS, close the CIRA channel
4373
+ if (ws.forwardclient.end) { ws.forwardclient.end(); } // TLS, close the TLS session
4374
+ if (ws.forwardclient.chnl) { ws.forwardclient.chnl.close(); } // TLS, close the CIRA channel
4375
+ delete ws.forwardclient;
4376
+ }
4377
+
4378
+ // Close the recording file
4379
+ if (ws.logfile != null) { recordingEntry(ws.logfile.fd, 3, 0, 'MeshCentralMCREC', function (fd, ws) { obj.fs.close(fd); delete ws.logfile; }, ws); }
4380
+ });
4381
+
4382
+ // If the web socket is closed, close the associated TCP connection.
4383
+ ws.on('close', function (req) {
4384
+ parent.debug('webrelay', 'Websocket relay closed.');
4385
+
4386
+ // Websocket closed, close the CIRA channel and TLS session.
4387
+ if (ws.forwardclient) {
4388
+ if (ws.forwardclient.close) { ws.forwardclient.close(); } // NonTLS, close the CIRA channel
4389
+ if (ws.forwardclient.end) { ws.forwardclient.end(); } // TLS, close the TLS session
4390
+ if (ws.forwardclient.chnl) { ws.forwardclient.chnl.close(); } // TLS, close the CIRA channel
4391
+ delete ws.forwardclient;
4392
+ }
4393
+
4394
+ // Close the recording file
4395
+ if (ws.logfile != null) { recordingEntry(ws.logfile.fd, 3, 0, 'MeshCentralMCREC', function (fd, ws) { obj.fs.close(fd); delete ws.logfile; }, ws); }
4396
+ });
4397
+
4398
+ // 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
4399
+
4400
+ // Fetch Intel AMT credentials & Setup interceptor
4401
+ if (req.query.p == 1) {
4402
+ parent.debug('webrelaydata', 'INTERCEPTOR1', { host: node.host, port: port, user: node.intelamt.user, pass: node.intelamt.pass });
4403
+ ws.interceptor = obj.interceptor.CreateHttpInterceptor({ host: node.host, port: port, user: node.intelamt.user, pass: node.intelamt.pass });
4404
+ ws.interceptor.blockAmtStorage = true;
4405
+ } else if (req.query.p == 2) {
4406
+ parent.debug('webrelaydata', 'INTERCEPTOR2', { user: node.intelamt.user, pass: node.intelamt.pass });
4407
+ ws.interceptor = obj.interceptor.CreateRedirInterceptor({ user: node.intelamt.user, pass: node.intelamt.pass });
4408
+ ws.interceptor.blockAmtStorage = true;
4409
+ }
4410
+
4411
+ return;
4412
+ }
4413
+
4414
+ // If Intel AMT direct connection is possible, option a direct socket
4415
+ if ((conn & 4) != 0) { // We got a new web socket connection, initiate a TCP connection to the target Intel AMT host/port.
4416
+ parent.debug('webrelay', 'Opening relay TCP socket connection to ' + req.query.host + '.');
4417
+
4418
+ // When data is received from the web socket, forward the data into the associated TCP connection.
4419
+ ws.on('message', function (msg) {
4420
+ //parent.debug('webrelaydata', 'TCP relay data to ' + node.host + ', ' + msg.length + ' bytes');
4421
+
4422
+ if (typeof msg == 'string') { msg = Buffer.from(msg, 'binary'); }
4423
+ if (ws.interceptor) { msg = ws.interceptor.processBrowserData(msg); } // Run data thru interceptor
4424
+
4425
+ // Log to recording file
4426
+ if (ws.logfile == null) {
4427
+ // Forward data to the associated TCP connection.
4428
+ try { ws.forwardclient.write(msg); } catch (ex) { }
4429
+ } else {
4430
+ // Log to recording file
4431
+ recordingEntry(ws.logfile.fd, 2, 2, msg, function () { try { ws.forwardclient.write(msg); } catch (ex) { } });
4432
+ }
4433
+ });
4434
+
4435
+ // If error, close the associated TCP connection.
4436
+ ws.on('error', function (err) {
4437
+ console.log('Error with relay web socket connection from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.');
4438
+ parent.debug('webrelay', 'Error with relay web socket connection from ' + req.clientIp + '.');
4439
+ if (ws.forwardclient) { try { ws.forwardclient.destroy(); } catch (e) { } }
4440
+
4441
+ // Close the recording file
4442
+ if (ws.logfile != null) {
4443
+ recordingEntry(ws.logfile.fd, 3, 0, 'MeshCentralMCREC', function (fd) {
4444
+ obj.fs.close(fd);
4445
+ ws.logfile = null;
4446
+ });
4447
+ }
4448
+ });
4449
+
4450
+ // If the web socket is closed, close the associated TCP connection.
4451
+ ws.on('close', function () {
4452
+ parent.debug('webrelay', 'Closing relay web socket connection to ' + req.query.host + '.');
4453
+ if (ws.forwardclient) { try { ws.forwardclient.destroy(); } catch (e) { } }
4454
+
4455
+ // Close the recording file
4456
+ if (ws.logfile != null) {
4457
+ recordingEntry(ws.logfile.fd, 3, 0, 'MeshCentralMCREC', function (fd) {
4458
+ obj.fs.close(fd);
4459
+ ws.logfile = null;
4460
+ });
4461
+ }
4462
+ });
4463
+
4464
+ // Compute target port
4465
+ var port = 16992;
4466
+ if (node.intelamt.tls > 0) port = 16993; // This is a direct connection, use TLS when possible
4467
+ if ((req.query.p == 2) || (req.query.p == 4)) port += 2;
4468
+
4469
+ if (node.intelamt.tls == 0) {
4470
+ // If this is TCP (without TLS) set a normal TCP socket
4471
+ ws.forwardclient = new obj.net.Socket();
4472
+ ws.forwardclient.setEncoding('binary');
4473
+ ws.forwardclient.xstate = 0;
4474
+ ws.forwardclient.forwardwsocket = ws;
4475
+ ws._socket.resume();
4476
+ } else {
4477
+ // If TLS is going to be used, setup a TLS socket
4478
+ 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 };
4479
+ if (req.query.tls1only == 1) { tlsoptions.secureProtocol = 'TLSv1_method'; }
4480
+ ws.forwardclient = obj.tls.connect(port, node.host, tlsoptions, function () {
4481
+ // The TLS connection method is the same as TCP, but located a bit differently.
4482
+ parent.debug('webrelay', 'TLS connected to ' + node.host + ':' + port + '.');
4483
+ ws.forwardclient.xstate = 1;
4484
+ ws._socket.resume();
4485
+ });
4486
+ ws.forwardclient.setEncoding('binary');
4487
+ ws.forwardclient.xstate = 0;
4488
+ ws.forwardclient.forwardwsocket = ws;
4489
+ }
4490
+
4491
+ // When we receive data on the TCP connection, forward it back into the web socket connection.
4492
+ ws.forwardclient.on('data', function (data) {
4493
+ if (typeof data == 'string') { data = Buffer.from(data, 'binary'); }
4494
+ if (obj.parent.debugLevel >= 1) { // DEBUG
4495
+ parent.debug('webrelaydata', 'TCP relay data from ' + node.host + ', ' + data.length + ' bytes.');
4496
+ //if (obj.parent.debugLevel >= 4) { Debug(4, ' ' + Buffer.from(data, 'binary').toString('hex')); }
4497
+ }
4498
+ if (ws.interceptor) { data = ws.interceptor.processAmtData(data); } // Run data thru interceptor
4499
+ if (ws.logfile == null) {
4500
+ // No logging
4501
+ try { ws.send(data); } catch (e) { }
4502
+ } else {
4503
+ // Log to recording file
4504
+ recordingEntry(ws.logfile.fd, 2, 0, data, function () { try { ws.send(data); } catch (e) { } });
4505
+ }
4506
+ });
4507
+
4508
+ // If the TCP connection closes, disconnect the associated web socket.
4509
+ ws.forwardclient.on('close', function () {
4510
+ parent.debug('webrelay', 'TCP relay disconnected from ' + node.host + ':' + port + '.');
4511
+ try { ws.close(); } catch (e) { }
4512
+ });
4513
+
4514
+ // If the TCP connection causes an error, disconnect the associated web socket.
4515
+ ws.forwardclient.on('error', function (err) {
4516
+ parent.debug('webrelay', 'TCP relay error from ' + node.host + ':' + port + ': ' + err);
4517
+ try { ws.close(); } catch (e) { }
4518
+ });
4519
+
4520
+ // Fetch Intel AMT credentials & Setup interceptor
4521
+ if (req.query.p == 1) { ws.interceptor = obj.interceptor.CreateHttpInterceptor({ host: node.host, port: port, user: node.intelamt.user, pass: node.intelamt.pass }); }
4522
+ else if (req.query.p == 2) { ws.interceptor = obj.interceptor.CreateRedirInterceptor({ user: node.intelamt.user, pass: node.intelamt.pass }); }
4523
+
4524
+ if (node.intelamt.tls == 0) {
4525
+ // A TCP connection to Intel AMT just connected, start forwarding.
4526
+ ws.forwardclient.connect(port, node.host, function () {
4527
+ parent.debug('webrelay', 'TCP relay connected to ' + node.host + ':' + port + '.');
4528
+ ws.forwardclient.xstate = 1;
4529
+ ws._socket.resume();
4530
+ });
4531
+ }
4532
+ return;
4533
+ }
4534
+
4535
+ });
4536
+ }
4537
+
4538
+ // Setup agent to/from server file transfer handler
4539
+ function handleAgentFileTransfer(ws, req) {
4540
+ var domain = checkAgentIpAddress(ws, req);
4541
+ if (domain == null) { parent.debug('web', 'Got agent file transfer connection with bad domain or blocked IP address ' + req.clientIp + ', dropping.'); ws.close(); return; }
4542
+ if (req.query.c == null) { parent.debug('web', 'Got agent file transfer connection without a cookie from ' + req.clientIp + ', dropping.'); ws.close(); return; }
4543
+ var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 10); // 10 minute timeout
4544
+ if ((c == null) || (c.a != 'aft')) { parent.debug('web', 'Got agent file transfer connection with invalid cookie from ' + req.clientIp + ', dropping.'); ws.close(); return; }
4545
+ ws.xcmd = c.b; ws.xarg = c.c, ws.xfilelen = 0;
4546
+ ws.send('c'); // Indicate connection of the tunnel. In this case, we are the termination point.
4547
+ ws.send('5'); // Indicate we want to perform file transfers (5 = Files).
4548
+ if (ws.xcmd == 'coredump') {
4549
+ // Check the agent core dump folder if not already present.
4550
+ var coreDumpPath = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps');
4551
+ if (obj.fs.existsSync(coreDumpPath) == false) { try { obj.fs.mkdirSync(coreDumpPath); } catch (ex) { } }
4552
+ ws.xfilepath = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps', ws.xarg);
4553
+ ws.xid = 'coredump';
4554
+ ws.send(JSON.stringify({ action: 'download', sub: 'start', ask: 'coredump', id: 'coredump' })); // Ask for a core dump file
4555
+ }
4556
+
4557
+ // When data is received from the web socket, echo it back
4558
+ ws.on('message', function (data) {
4559
+ if (typeof data == 'string') {
4560
+ // Control message
4561
+ var cmd = null;
4562
+ try { cmd = JSON.parse(data); } catch (ex) { }
4563
+ if ((cmd == null) || (cmd.action != 'download') || (cmd.sub == null)) return;
4564
+ switch (cmd.sub) {
4565
+ case 'start': {
4566
+ // Perform an async file open
4567
+ var callback = function onFileOpen(err, fd) {
4568
+ onFileOpen.xws.xfile = fd;
4569
+ try { onFileOpen.xws.send(JSON.stringify({ action: 'download', sub: 'startack', id: onFileOpen.xws.xid, ack: 1 })); } catch (ex) { } // Ask for a directory (test)
4570
+ };
4571
+ callback.xws = this;
4572
+ obj.fs.open(this.xfilepath + '.part', 'w', callback);
4573
+ break;
4574
+ }
4575
+ }
4576
+ } else {
4577
+ // Binary message
4578
+ if (data.length < 4) return;
4579
+ var flags = data.readInt32BE(0);
4580
+ if ((data.length > 4)) {
4581
+ // Write the file
4582
+ this.xfilelen += (data.length - 4);
4583
+ try {
4584
+ var callback = function onFileDataWritten(err, bytesWritten, buffer) {
4585
+ if (onFileDataWritten.xflags & 1) {
4586
+ // End of file
4587
+ parent.debug('web', "Completed downloads of agent dumpfile, " + onFileDataWritten.xws.xfilelen + " bytes.");
4588
+ if (onFileDataWritten.xws.xfile) {
4589
+ obj.fs.close(onFileDataWritten.xws.xfile, function (err) { });
4590
+ obj.fs.rename(onFileDataWritten.xws.xfilepath + '.part', onFileDataWritten.xws.xfilepath, function (err) { });
4591
+ onFileDataWritten.xws.xfile = null;
4592
+ }
4593
+ try { onFileDataWritten.xws.send(JSON.stringify({ action: 'markcoredump' })); } catch (ex) { } // Ask to delete the core dump file
4594
+ try { onFileDataWritten.xws.close(); } catch (ex) { }
4595
+ } else {
4596
+ // Send ack
4597
+ try { onFileDataWritten.xws.send(JSON.stringify({ action: 'download', sub: 'ack', id: onFileDataWritten.xws.xid })); } catch (ex) { } // Ask for a directory (test)
4598
+ }
4599
+ };
4600
+ callback.xws = this;
4601
+ callback.xflags = flags;
4602
+ obj.fs.write(this.xfile, data, 4, data.length - 4, callback);
4603
+ } catch (ex) { }
4604
+ } else {
4605
+ if (flags & 1) {
4606
+ // End of file
4607
+ parent.debug('web', "Completed downloads of agent dumpfile, " + this.xfilelen + " bytes.");
4608
+ if (this.xfile) {
4609
+ obj.fs.close(this.xfile, function (err) { });
4610
+ obj.fs.rename(this.xfilepath + '.part', this.xfilepath, function (err) { });
4611
+ this.xfile = null;
4612
+ }
4613
+ this.send(JSON.stringify({ action: 'markcoredump' })); // Ask to delete the core dump file
4614
+ try { this.close(); } catch (ex) { }
4615
+ } else {
4616
+ // Send ack
4617
+ this.send(JSON.stringify({ action: 'download', sub: 'ack', id: this.xid })); // Ask for a directory (test)
4618
+ }
4619
+ }
4620
+ }
4621
+ });
4622
+
4623
+ // If error, do nothing.
4624
+ ws.on('error', function (err) { console.log('Agent file transfer server error from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.'); });
4625
+
4626
+ // If closed, do nothing
4627
+ ws.on('close', function (req) {
4628
+ if (this.xfile) {
4629
+ obj.fs.close(this.xfile, function (err) { });
4630
+ obj.fs.unlink(this.xfilepath + '.part', function (err) { }); // Remove a partial file
4631
+ }
4632
+ });
4633
+ }
4634
+
4635
+ // Handle the web socket echo request, just echo back the data sent
4636
+ function handleEchoWebSocket(ws, req) {
4637
+ const domain = checkUserIpAddress(ws, req);
4638
+ if (domain == null) { return; }
4639
+ ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive
4640
+
4641
+ // When data is received from the web socket, echo it back
4642
+ ws.on('message', function (data) {
4643
+ if (data.toString('utf8') == 'close') {
4644
+ try { ws.close(); } catch (e) { console.log(e); }
4645
+ } else {
4646
+ try { ws.send(data); } catch (e) { console.log(e); }
4647
+ }
4648
+ });
4649
+
4650
+ // If error, do nothing.
4651
+ ws.on('error', function (err) { console.log('Echo server error from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.'); });
4652
+
4653
+ // If closed, do nothing
4654
+ ws.on('close', function (req) { });
4655
+ }
4656
+
4657
+ // Handle the 2FA hold web socket
4658
+ // Accept an hold a web socket connection until the 2FA response is received.
4659
+ function handle2faHoldWebSocket(ws, req) {
4660
+ const domain = checkUserIpAddress(ws, req);
4661
+ if (domain == null) { return; }
4662
+ if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.push2factor == false)) { ws.close(); return; } // Push 2FA is disabled
4663
+ if (typeof req.query.c !== 'string') { ws.close(); return; }
4664
+ const cookie = parent.decodeCookie(req.query.c, null, 1);
4665
+ if ((cookie == null) || (cookie.d != domain.id)) { ws.close(); return; }
4666
+ var user = obj.users[cookie.u];
4667
+ if ((user == null) || (typeof user.otpdev != 'string')) { ws.close(); return; }
4668
+ ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive
4669
+
4670
+ // 2FA event subscription
4671
+ obj.parent.AddEventDispatch(['2fadev-' + cookie.s], ws);
4672
+ ws.cookie = cookie;
4673
+ ws.HandleEvent = function (source, event, ids, id) {
4674
+ obj.parent.RemoveAllEventDispatch(this);
4675
+ if ((event.approved === true) && (event.userid == this.cookie.u)) {
4676
+ // Create a login cookie
4677
+ const loginCookie = obj.parent.encodeCookie({ a: 'pushAuth', u: event.userid, d: event.domain }, obj.parent.loginCookieEncryptionKey);
4678
+ try { ws.send(JSON.stringify({ approved: true, token: loginCookie })); } catch (ex) { }
4679
+ } else {
4680
+ // Reject the login
4681
+ try { ws.send(JSON.stringify({ approved: false })); } catch (ex) { }
4682
+ }
4683
+ }
4684
+
4685
+ // We do not accept any data on this connection.
4686
+ ws.on('message', function (data) { this.close(); });
4687
+
4688
+ // If error, do nothing.
4689
+ ws.on('error', function (err) { });
4690
+
4691
+ // If closed, unsubscribe
4692
+ ws.on('close', function (req) { obj.parent.RemoveAllEventDispatch(this); });
4693
+
4694
+ // Perform push notification to device
4695
+ try {
4696
+ const deviceCookie = parent.encodeCookie({ a: 'checkAuth', c: cookie.c, u: cookie.u, n: cookie.n, s: cookie.s });
4697
+ var code = Buffer.from(cookie.c, 'base64').toString();
4698
+ var payload = { notification: { title: (domain.title ? domain.title : 'MeshCentral'), body: "Authentication - " + code }, data: { url: '2fa://auth?code=' + cookie.c + '&c=' + deviceCookie } };
4699
+ var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute
4700
+ parent.firebase.sendToDevice(user.otpdev, payload, options, function (id, err, errdesc) {
4701
+ if (err == null) {
4702
+ try { ws.send(JSON.stringify({ sent: true, code: code })); } catch (ex) { }
4703
+ } else {
4704
+ try { ws.send(JSON.stringify({ sent: false })); } catch (ex) { }
4705
+ }
4706
+ });
4707
+ } catch (ex) { console.log(ex); }
4708
+ }
4709
+
4710
+ // Get the total size of all files in a folder and all sub-folders. (TODO: try to make all async version)
4711
+ function readTotalFileSize(path) {
4712
+ var r = 0, dir;
4713
+ try { dir = obj.fs.readdirSync(path); } catch (e) { return 0; }
4714
+ for (var i in dir) {
4715
+ var stat = obj.fs.statSync(path + '/' + dir[i]);
4716
+ if ((stat.mode & 0x004000) == 0) { r += stat.size; } else { r += readTotalFileSize(path + '/' + dir[i]); }
4717
+ }
4718
+ return r;
4719
+ }
4720
+
4721
+ // Delete a folder and all sub items. (TODO: try to make all async version)
4722
+ function deleteFolderRec(path) {
4723
+ if (obj.fs.existsSync(path) == false) return;
4724
+ try {
4725
+ obj.fs.readdirSync(path).forEach(function (file, index) {
4726
+ var pathx = path + '/' + file;
4727
+ if (obj.fs.lstatSync(pathx).isDirectory()) { deleteFolderRec(pathx); } else { obj.fs.unlinkSync(pathx); }
4728
+ });
4729
+ obj.fs.rmdirSync(path);
4730
+ } catch (ex) { }
4731
+ }
4732
+
4733
+ // Handle Intel AMT events
4734
+ // To subscribe, add "http://server:port/amtevents.ashx" to Intel AMT subscriptions.
4735
+ obj.handleAmtEventRequest = function (req, res) {
4736
+ const domain = getDomain(req);
4737
+ try {
4738
+ if (req.headers.authorization) {
4739
+ var authstr = req.headers.authorization;
4740
+ if (authstr.substring(0, 7) == 'Digest ') {
4741
+ var auth = obj.common.parseNameValueList(obj.common.quoteSplit(authstr.substring(7)));
4742
+ if ((req.url === auth.uri) && (obj.httpAuthRealm === auth.realm) && (auth.opaque === obj.crypto.createHmac('SHA384', obj.httpAuthRandom).update(auth.nonce).digest('hex'))) {
4743
+
4744
+ // Read the data, we need to get the arg field
4745
+ var eventData = '';
4746
+ req.on('data', function (chunk) { eventData += chunk; });
4747
+ req.on('end', function () {
4748
+
4749
+ // Completed event read, let get the argument that must contain the nodeid
4750
+ var i = eventData.indexOf('<m:arg xmlns:m="http://x.com">');
4751
+ if (i > 0) {
4752
+ var nodeid = eventData.substring(i + 30, i + 30 + 64);
4753
+ if (nodeid.length == 64) {
4754
+ var nodekey = 'node/' + domain.id + '/' + nodeid;
4755
+
4756
+ // See if this node exists in the database
4757
+ obj.db.Get(nodekey, function (err, nodes) {
4758
+ if (nodes.length == 1) {
4759
+ // Yes, the node exists, compute Intel AMT digest password
4760
+ var node = nodes[0];
4761
+ 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');
4762
+
4763
+ // Check the MD5 hash
4764
+ if (auth.response === obj.common.ComputeDigesthash(auth.username, amtpass, auth.realm, 'POST', auth.uri, auth.qop, auth.nonce, auth.nc, auth.cnonce)) {
4765
+
4766
+ // This is an authenticated Intel AMT event, update the host address
4767
+ var amthost = req.clientIp;
4768
+ if (amthost.substring(0, 7) === '::ffff:') { amthost = amthost.substring(7); }
4769
+ if (node.host != amthost) {
4770
+ // Get the mesh for this device
4771
+ var mesh = obj.meshes[node.meshid];
4772
+ if (mesh) {
4773
+ // Update the database
4774
+ var oldname = node.host;
4775
+ node.host = amthost;
4776
+ obj.db.Set(obj.cleanDevice(node));
4777
+
4778
+ // Event the node change
4779
+ 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 };
4780
+
4781
+ // Remove the Intel AMT password before eventing this.
4782
+ event.node = node;
4783
+ if (event.node.intelamt && event.node.intelamt.pass) {
4784
+ event.node = Object.assign({}, event.node); // Shallow clone
4785
+ event.node.intelamt = Object.assign({}, event.node.intelamt); // Shallow clone
4786
+ delete event.node.intelamt.pass;
4787
+ }
4788
+
4789
+ 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.
4790
+ obj.parent.DispatchEvent(['*', node.meshid], obj, event);
4791
+ }
4792
+ }
4793
+
4794
+ if (parent.amtEventHandler) { parent.amtEventHandler.handleAmtEvent(eventData, nodeid, amthost); }
4795
+ //res.send('OK');
4796
+
4797
+ return;
4798
+ }
4799
+ }
4800
+ });
4801
+ }
4802
+ }
4803
+ });
4804
+ }
4805
+ }
4806
+ }
4807
+ } catch (e) { console.log(e); }
4808
+
4809
+ // Send authentication response
4810
+ obj.crypto.randomBytes(48, function (err, buf) {
4811
+ var nonce = buf.toString('hex'), opaque = obj.crypto.createHmac('SHA384', obj.httpAuthRandom).update(nonce).digest('hex');
4812
+ res.set({ 'WWW-Authenticate': 'Digest realm="' + obj.httpAuthRealm + '", qop="auth,auth-int", nonce="' + nonce + '", opaque="' + opaque + '"' });
4813
+ res.sendStatus(401);
4814
+ });
4815
+ };
4816
+
4817
+ // Handle a server backup request
4818
+ function handleBackupRequest(req, res) {
4819
+ const domain = checkUserIpAddress(req, res);
4820
+ if (domain == null) { return; }
4821
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
4822
+ if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
4823
+ if ((domain.myserver === false) || ((domain.myserver != null) && (domain.myserver.backup !== true))) { res.sendStatus(401); return; }
4824
+
4825
+ var user = obj.users[req.session.userid];
4826
+ if ((user == null) || ((user.siteadmin & 1) == 0)) { res.sendStatus(401); return; } // Check if we have server backup rights
4827
+
4828
+ // Require modules
4829
+ const archive = require('archiver')('zip', { level: 9 }); // Sets the compression method to maximum.
4830
+
4831
+ // Good practice to catch this error explicitly
4832
+ archive.on('error', function (err) { throw err; });
4833
+
4834
+ // Set the archive name
4835
+ res.attachment((domain.title ? domain.title : 'MeshCentral') + '-Backup-' + new Date().toLocaleDateString().replace('/', '-').replace('/', '-') + '.zip');
4836
+
4837
+ // Pipe archive data to the file
4838
+ archive.pipe(res);
4839
+
4840
+ // Append files from a glob pattern
4841
+ archive.directory(obj.parent.datapath, false);
4842
+
4843
+ // Finalize the archive (ie we are done appending files but streams have to finish yet)
4844
+ archive.finalize();
4845
+ }
4846
+
4847
+ // Handle a server restore request
4848
+ function handleRestoreRequest(req, res) {
4849
+ const domain = checkUserIpAddress(req, res);
4850
+ if (domain == null) { return; }
4851
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
4852
+ if ((domain.myserver === false) || ((domain.myserver != null) && (domain.myserver.restore !== true))) { res.sendStatus(401); return; }
4853
+
4854
+ var authUserid = null;
4855
+ if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
4856
+ const multiparty = require('multiparty');
4857
+ const form = new multiparty.Form();
4858
+ form.parse(req, function (err, fields, files) {
4859
+ // If an authentication cookie is embedded in the form, use that.
4860
+ if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
4861
+ var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
4862
+ if ((loginCookie != null) && (loginCookie.ip != null) && checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
4863
+ if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
4864
+ }
4865
+ if (authUserid == null) { res.sendStatus(401); return; }
4866
+
4867
+ // Get the user
4868
+ const user = obj.users[req.session.userid];
4869
+ if ((user == null) || ((user.siteadmin & 4) == 0)) { res.sendStatus(401); return; } // Check if we have server restore rights
4870
+
4871
+ res.set('Content-Type', 'text/html');
4872
+ res.end('<html><body>Server must be restarted, <a href="' + domain.url + '">click here to login</a>.</body></html>');
4873
+ parent.Stop(files.datafile[0].path);
4874
+ });
4875
+ }
4876
+
4877
+ // Handle a request to download a mesh agent
4878
+ obj.handleMeshAgentRequest = function (req, res) {
4879
+ var domain = getDomain(req, res);
4880
+ if (domain == null) { parent.debug('web', 'handleRootRequest: invalid domain.'); try { res.sendStatus(404); } catch (ex) { } return; }
4881
+
4882
+ // If required, check if this user has rights to do this
4883
+ if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true)) && (req.session.userid == null)) { res.sendStatus(401); return; }
4884
+
4885
+ if ((req.query.meshinstall != null) && (req.query.id != null)) {
4886
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { try { res.sendStatus(404); } catch (ex) { } return; } // Check 3FA URL key
4887
+
4888
+ // Send meshagent with included self installer for a specific platform back
4889
+ // Start by getting the .msh for this request
4890
+ var meshsettings = getMshFromRequest(req, res, domain);
4891
+ if (meshsettings == null) { try { res.sendStatus(401); } catch (ex) { } return; }
4892
+
4893
+ // Get the interactive install script, this only works for non-Windows agents
4894
+ var agentid = parseInt(req.query.meshinstall);
4895
+ var argentInfo = obj.parent.meshAgentBinaries[agentid];
4896
+ if (domain.meshAgentBinaries && domain.meshAgentBinaries[agentid]) { argentInfo = domain.meshAgentBinaries[agentid]; }
4897
+ var scriptInfo = obj.parent.meshAgentInstallScripts[6];
4898
+ if ((argentInfo == null) || (scriptInfo == null) || (argentInfo.platform == 'win32')) { try { res.sendStatus(404); } catch (ex) { } return; }
4899
+
4900
+ // Change the .msh file into JSON format and merge it into the install script
4901
+ var tokens, msh = {}, meshsettingslines = meshsettings.split('\r').join('').split('\n');
4902
+ for (var i in meshsettingslines) { tokens = meshsettingslines[i].split('='); if (tokens.length == 2) { msh[tokens[0]] = tokens[1]; } }
4903
+ var js = scriptInfo.data.replace('var msh = {};', 'var msh = ' + JSON.stringify(msh) + ';');
4904
+
4905
+ // Get the agent filename
4906
+ var meshagentFilename = 'meshagent';
4907
+ if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) { meshagentFilename = domain.agentcustomization.filename; }
4908
+
4909
+ setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename, null, 'meshagent');
4910
+ if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
4911
+ res.statusCode = 200;
4912
+ obj.parent.exeHandler.streamExeWithJavaScript({ platform: argentInfo.platform, sourceFileName: argentInfo.path, destinationStream: res, js: Buffer.from(js, 'utf8'), peinfo: argentInfo.pe });
4913
+ } else if (req.query.id != null) {
4914
+ // Send a specific mesh agent back
4915
+ var argentInfo = obj.parent.meshAgentBinaries[req.query.id];
4916
+ if (domain.meshAgentBinaries && domain.meshAgentBinaries[req.query.id]) { argentInfo = domain.meshAgentBinaries[req.query.id]; }
4917
+ if (argentInfo == null) { try { res.sendStatus(404); } catch (ex) { } return; }
4918
+
4919
+ // Download PDB debug files, only allowed for administrator or accounts with agent dump access
4920
+ if (req.query.pdb == 1) {
4921
+ if ((req.session == null) || (req.session.userid == null)) { try { res.sendStatus(404); } catch (ex) { } return; }
4922
+ var user = obj.users[req.session.userid];
4923
+ if (user == null) { try { res.sendStatus(404); } catch (ex) { } return; }
4924
+ if ((user != null) && ((user.siteadmin == 0xFFFFFFFF) || ((Array.isArray(obj.parent.config.settings.agentcoredumpusers)) && (obj.parent.config.settings.agentcoredumpusers.indexOf(user._id) >= 0)))) {
4925
+ if (argentInfo.id == 3) {
4926
+ setContentDispositionHeader(res, 'application/octet-stream', 'MeshService.pdb', null, 'MeshService.pdb');
4927
+ if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
4928
+ try { res.sendFile(argentInfo.path.split('MeshService-signed.exe').join('MeshService.pdb')); } catch (ex) { }
4929
+ return;
4930
+ }
4931
+ if (argentInfo.id == 4) {
4932
+ setContentDispositionHeader(res, 'application/octet-stream', 'MeshService64.pdb', null, 'MeshService64.pdb');
4933
+ if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
4934
+ try { res.sendFile(argentInfo.path.split('MeshService64-signed.exe').join('MeshService64.pdb')); } catch (ex) { }
4935
+ return;
4936
+ }
4937
+ }
4938
+ try { res.sendStatus(404); } catch (ex) { }
4939
+ return;
4940
+ }
4941
+
4942
+ if ((req.query.meshid == null) || (argentInfo.platform != 'win32')) {
4943
+ // Get the agent filename
4944
+ var meshagentFilename = argentInfo.rname;
4945
+ if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) { meshagentFilename = domain.agentcustomization.filename; }
4946
+ if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
4947
+ if (req.query.zip == 1) { if (argentInfo.zdata != null) { setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename + '.zip', null, 'meshagent.zip'); res.send(argentInfo.zdata); } else { try { res.sendStatus(404); } catch (ex) { } } return; } // Send compressed agent
4948
+ setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename, null, 'meshagent');
4949
+ if (argentInfo.data == null) { res.sendFile(argentInfo.path); } else { res.send(argentInfo.data); }
4950
+ return;
4951
+ } else {
4952
+ // Check if the meshid is a time limited, encrypted cookie
4953
+ var meshcookie = obj.parent.decodeCookie(req.query.meshid, obj.parent.invitationLinkEncryptionKey);
4954
+ if ((meshcookie != null) && (meshcookie.m != null)) { req.query.meshid = meshcookie.m; }
4955
+
4956
+ // We are going to embed the .msh file into the Windows executable (signed or not).
4957
+ // First, fetch the mesh object to build the .msh file
4958
+ var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.meshid];
4959
+ if (mesh == null) { try { res.sendStatus(401); } catch (ex) { } return; }
4960
+
4961
+ // If required, check if this user has rights to do this
4962
+ if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true))) {
4963
+ if ((domain.id != mesh.domain) || ((obj.GetMeshRights(req.session.userid, mesh) & 1) == 0)) { try { res.sendStatus(401); } catch (ex) { } return; }
4964
+ }
4965
+
4966
+ var meshidhex = Buffer.from(req.query.meshid.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
4967
+ var serveridhex = Buffer.from(obj.agentCertificateHashBase64.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
4968
+ var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port if specified
4969
+ if (obj.args.agentport != null) { httpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
4970
+ if (obj.args.agentaliasport != null) { httpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
4971
+
4972
+ // Prepare a mesh agent file name using the device group name.
4973
+ var meshfilename = mesh.name
4974
+ meshfilename = meshfilename.split('\\').join('').split('/').join('').split(':').join('').split('*').join('').split('?').join('').split('"').join('').split('<').join('').split('>').join('').split('|').join('').split(' ').join('').split('\'').join('');
4975
+ if (argentInfo.rname.endsWith('.exe')) { meshfilename = argentInfo.rname.substring(0, argentInfo.rname.length - 4) + '-' + meshfilename + '.exe'; } else { meshfilename = argentInfo.rname + '-' + meshfilename; }
4976
+
4977
+ // Customize the mesh agent file name
4978
+ if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) {
4979
+ meshfilename = meshfilename.split('meshagent').join(domain.agentcustomization.filename).split('MeshAgent').join(domain.agentcustomization.filename);
4980
+ }
4981
+
4982
+ // Get the agent connection server name
4983
+ var serverName = obj.getWebServerName(domain);
4984
+ if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
4985
+
4986
+ // Build the agent connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
4987
+ var xdomain = (domain.dns == null) ? domain.id : '';
4988
+ if (xdomain != '') xdomain += '/';
4989
+ var meshsettings = '\r\nMeshName=' + mesh.name + '\r\nMeshType=' + mesh.mtype + '\r\nMeshID=0x' + meshidhex + '\r\nServerID=' + serveridhex + '\r\n';
4990
+ if (obj.args.lanonly != true) { meshsettings += 'MeshServer=wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'agent.ashx\r\n'; } else {
4991
+ meshsettings += 'MeshServer=local\r\n';
4992
+ 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'; }
4993
+ }
4994
+ if ((req.query.tag != null) && (typeof req.query.tag == 'string') && (obj.common.isAlphaNumeric(req.query.tag) == true)) { meshsettings += 'Tag=' + req.query.tag + '\r\n'; }
4995
+ if ((req.query.installflags != null) && (req.query.installflags != 0) && (parseInt(req.query.installflags) == req.query.installflags)) { meshsettings += 'InstallFlags=' + parseInt(req.query.installflags) + '\r\n'; }
4996
+ if (req.query.id == '10006') { // Assistant settings and customizations
4997
+ if ((req.query.ac != null)) { meshsettings += 'AutoConnect=' + req.query.ac + '\r\n'; } // Set MeshCentral Assistant flags if needed. 0x01 = Always Connected, 0x02 = Not System Tray
4998
+ if (obj.args.assistantconfig) { for (var i in obj.args.assistantconfig) { meshsettings += obj.args.assistantconfig[i] + '\r\n'; } }
4999
+ if (domain.assistantconfig) { for (var i in domain.assistantconfig) { meshsettings += domain.assistantconfig[i] + '\r\n'; } }
This file is too large to show in full.
webserver-old.js
new
+8205
@@ -0,0 +1,8205 @@
1
+/**
2
+* @description MeshCentral web server
3
+* @author Ylian Saint-Hilaire
4
+* @copyright Intel Corporation 2018-2022
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, doneFunc) {
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
+ obj.uaparser = require('./ua-parser');
56
+ const constants = (obj.crypto.constants ? obj.crypto.constants : require('constants')); // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead.
57
+
58
+ // Setup WebAuthn / FIDO2
59
+ obj.webauthn = require('./webauthn.js').CreateWebAuthnModule();
60
+
61
+ // Variables
62
+ obj.args = args;
63
+ obj.parent = parent;
64
+ obj.filespath = parent.filespath;
65
+ obj.db = db;
66
+ obj.app = obj.express();
67
+ if (obj.args.agentport) { obj.agentapp = obj.express(); }
68
+ if (args.compression !== false) { obj.app.use(require('compression')()); }
69
+ obj.app.disable('x-powered-by');
70
+ obj.tlsServer = null;
71
+ obj.tcpServer = null;
72
+ obj.certificates = certificates;
73
+ obj.users = {}; // UserID --> User
74
+ obj.meshes = {}; // MeshID --> Mesh (also called device group)
75
+ obj.userGroups = {}; // UGrpID --> User Group
76
+ obj.userAllowedIp = args.userallowedip; // List of allowed IP addresses for users
77
+ obj.agentAllowedIp = args.agentallowedip; // List of allowed IP addresses for agents
78
+ obj.agentBlockedIp = args.agentblockedip; // List of blocked IP addresses for agents
79
+ obj.tlsSniCredentials = null;
80
+ obj.dnsDomains = {};
81
+ obj.relaySessionCount = 0;
82
+ obj.relaySessionErrorCount = 0;
83
+ obj.blockedUsers = 0;
84
+ obj.blockedAgents = 0;
85
+ obj.renderPages = null;
86
+ obj.renderLanguages = [];
87
+ obj.destroyedSessions = {};
88
+
89
+ // Mesh Rights
90
+ const MESHRIGHT_EDITMESH = 0x00000001;
91
+ const MESHRIGHT_MANAGEUSERS = 0x00000002;
92
+ const MESHRIGHT_MANAGECOMPUTERS = 0x00000004;
93
+ const MESHRIGHT_REMOTECONTROL = 0x00000008;
94
+ const MESHRIGHT_AGENTCONSOLE = 0x00000010;
95
+ const MESHRIGHT_SERVERFILES = 0x00000020;
96
+ const MESHRIGHT_WAKEDEVICE = 0x00000040;
97
+ const MESHRIGHT_SETNOTES = 0x00000080;
98
+ const MESHRIGHT_REMOTEVIEWONLY = 0x00000100;
99
+ const MESHRIGHT_NOTERMINAL = 0x00000200;
100
+ const MESHRIGHT_NOFILES = 0x00000400;
101
+ const MESHRIGHT_NOAMT = 0x00000800;
102
+ const MESHRIGHT_DESKLIMITEDINPUT = 0x00001000;
103
+ const MESHRIGHT_LIMITEVENTS = 0x00002000;
104
+ const MESHRIGHT_CHATNOTIFY = 0x00004000;
105
+ const MESHRIGHT_UNINSTALL = 0x00008000;
106
+ const MESHRIGHT_NODESKTOP = 0x00010000;
107
+ const MESHRIGHT_REMOTECOMMAND = 0x00020000;
108
+ const MESHRIGHT_RESETOFF = 0x00040000;
109
+ const MESHRIGHT_GUESTSHARING = 0x00080000;
110
+ const MESHRIGHT_ADMIN = 0xFFFFFFFF;
111
+
112
+ // Site rights
113
+ const SITERIGHT_SERVERBACKUP = 0x00000001;
114
+ const SITERIGHT_MANAGEUSERS = 0x00000002;
115
+ const SITERIGHT_SERVERRESTORE = 0x00000004;
116
+ const SITERIGHT_FILEACCESS = 0x00000008;
117
+ const SITERIGHT_SERVERUPDATE = 0x00000010;
118
+ const SITERIGHT_LOCKED = 0x00000020;
119
+ const SITERIGHT_NONEWGROUPS = 0x00000040;
120
+ const SITERIGHT_NOMESHCMD = 0x00000080;
121
+ const SITERIGHT_USERGROUPS = 0x00000100;
122
+ const SITERIGHT_RECORDINGS = 0x00000200;
123
+ const SITERIGHT_LOCKSETTINGS = 0x00000400;
124
+ const SITERIGHT_ALLEVENTS = 0x00000800;
125
+ const SITERIGHT_NONEWDEVICES = 0x00001000;
126
+ const SITERIGHT_ADMIN = 0xFFFFFFFF;
127
+
128
+ // Setup SSPI authentication if needed
129
+ if ((obj.parent.platform == 'win32') && (obj.args.nousers != true) && (obj.parent.config != null) && (obj.parent.config.domains != null)) {
130
+ 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: false, offerBasic: false }); } }
131
+ }
132
+
133
+ // Perform hash on web certificate and agent certificate
134
+ obj.webCertificateHash = parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.web.cert);
135
+ obj.webCertificateHashs = { '': obj.webCertificateHash };
136
+ obj.webCertificateHashBase64 = Buffer.from(obj.webCertificateHash, 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
137
+ obj.webCertificateFullHash = parent.certificateOperations.getCertHashBinary(obj.certificates.web.cert);
138
+ obj.webCertificateFullHashs = { '': obj.webCertificateFullHash };
139
+ obj.webCertificateExpire = { '': Date.parse(parent.certificateOperations.forge.pki.certificateFromPem(parent.certificates.web.cert).validity.notAfter) };
140
+ obj.agentCertificateHashHex = parent.certificateOperations.getPublicKeyHash(obj.certificates.agent.cert);
141
+ obj.agentCertificateHashBase64 = Buffer.from(obj.agentCertificateHashHex, 'hex').toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
142
+ obj.agentCertificateAsn1 = parent.certificateOperations.forge.asn1.toDer(parent.certificateOperations.forge.pki.certificateToAsn1(parent.certificateOperations.forge.pki.certificateFromPem(parent.certificates.agent.cert))).getBytes();
143
+ obj.defaultWebCertificateHash = parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.webdefault.cert);
144
+ obj.defaultWebCertificateFullHash = parent.certificateOperations.getCertHashBinary(obj.certificates.webdefault.cert);
145
+
146
+ // Compute the hash of all of the web certificates for each domain
147
+ for (var i in obj.parent.config.domains) {
148
+ if (obj.parent.config.domains[i].certhash != null) {
149
+ // If the web certificate hash is provided, use it.
150
+ obj.webCertificateHashs[i] = obj.webCertificateFullHashs[i] = Buffer.from(obj.parent.config.domains[i].certhash, 'hex').toString('binary');
151
+ if (obj.parent.config.domains[i].certkeyhash != null) { obj.webCertificateHashs[i] = Buffer.from(obj.parent.config.domains[i].certkeyhash, 'hex').toString('binary'); }
152
+ delete obj.webCertificateExpire[i]; // Expire time is not provided
153
+ } else if ((obj.parent.config.domains[i].dns != null) && (obj.parent.config.domains[i].certs != null)) {
154
+ // If the domain has a different DNS name, use a different certificate hash.
155
+ // Hash the full certificate
156
+ obj.webCertificateFullHashs[i] = parent.certificateOperations.getCertHashBinary(obj.parent.config.domains[i].certs.cert);
157
+ obj.webCertificateExpire[i] = Date.parse(parent.certificateOperations.forge.pki.certificateFromPem(obj.parent.config.domains[i].certs.cert).validity.notAfter);
158
+ try {
159
+ // Decode a RSA certificate and hash the public key.
160
+ obj.webCertificateHashs[i] = parent.certificateOperations.getPublicKeyHashBinary(obj.parent.config.domains[i].certs.cert);
161
+ } catch (ex) {
162
+ // This may be a ECDSA certificate, hash the entire cert.
163
+ obj.webCertificateHashs[i] = obj.webCertificateFullHashs[i];
164
+ }
165
+ } else if ((obj.parent.config.domains[i].dns != null) && (obj.certificates.dns[i] != null)) {
166
+ // If this domain has a DNS and a matching DNS cert, use it. This case works for wildcard certs.
167
+ obj.webCertificateFullHashs[i] = parent.certificateOperations.getCertHashBinary(obj.certificates.dns[i].cert);
168
+ obj.webCertificateHashs[i] = parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.dns[i].cert);
169
+ obj.webCertificateExpire[i] = Date.parse(parent.certificateOperations.forge.pki.certificateFromPem(obj.certificates.dns[i].cert).validity.notAfter);
170
+ } else if (i != '') {
171
+ // For any other domain, use the default cert.
172
+ obj.webCertificateFullHashs[i] = obj.webCertificateFullHashs[''];
173
+ obj.webCertificateHashs[i] = obj.webCertificateHashs[''];
174
+ obj.webCertificateExpire[i] = obj.webCertificateExpire[''];
175
+ }
176
+ }
177
+
178
+ // If we are running the legacy swarm server, compute the hash for that certificate
179
+ if (parent.certificates.swarmserver != null) {
180
+ obj.swarmCertificateAsn1 = parent.certificateOperations.forge.asn1.toDer(parent.certificateOperations.forge.pki.certificateToAsn1(parent.certificateOperations.forge.pki.certificateFromPem(parent.certificates.swarmserver.cert))).getBytes();
181
+ 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' });
182
+ 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' });
183
+ }
184
+
185
+ // Main lists
186
+ obj.wsagents = {}; // NodeId --> Agent
187
+ obj.wsagentsWithBadWebCerts = {}; // NodeId --> Agent
188
+ obj.wsagentsDisconnections = {};
189
+ obj.wsagentsDisconnectionsTimer = null;
190
+ obj.duplicateAgentsLog = {};
191
+ obj.wssessions = {}; // UserId --> Array Of Sessions
192
+ obj.wssessions2 = {}; // "UserId + SessionRnd" --> Session (Note that the SessionId is the UserId + / + SessionRnd)
193
+ obj.wsPeerSessions = {}; // ServerId --> Array Of "UserId + SessionRnd"
194
+ obj.wsPeerSessions2 = {}; // "UserId + SessionRnd" --> ServerId
195
+ obj.wsPeerSessions3 = {}; // ServerId --> UserId --> [ SessionId ]
196
+ obj.sessionsCount = {}; // Merged session counters, used when doing server peering. UserId --> SessionCount
197
+ obj.wsrelays = {}; // Id -> Relay
198
+ obj.desktoprelays = {}; // Id -> Desktop Multiplexor Relay
199
+ obj.wsPeerRelays = {}; // Id -> { ServerId, Time }
200
+ var tlsSessionStore = {}; // Store TLS session information for quick resume.
201
+ var tlsSessionStoreCount = 0; // Number of cached TLS session information in store.
202
+
203
+ // Setup randoms
204
+ obj.crypto.randomBytes(48, function (err, buf) { obj.httpAuthRandom = buf; });
205
+ obj.crypto.randomBytes(16, function (err, buf) { obj.httpAuthRealm = buf.toString('hex'); });
206
+ obj.crypto.randomBytes(48, function (err, buf) { obj.relayRandom = buf; });
207
+
208
+ // Get non-english web pages and emails
209
+ getRenderList();
210
+ getEmailLanguageList();
211
+
212
+ // Setup DNS domain TLS SNI credentials
213
+ {
214
+ var dnscount = 0;
215
+ obj.tlsSniCredentials = {};
216
+ 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++; } }
217
+ 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; }
218
+ }
219
+ function TlsSniCallback(name, cb) {
220
+ var c = obj.tlsSniCredentials[name];
221
+ if (c != null) {
222
+ cb(null, c);
223
+ } else {
224
+ cb(null, obj.tlsSniCredentials['']);
225
+ }
226
+ }
227
+
228
+ function EscapeHtml(x) { if (typeof x == 'string') return x.replace(/&/g, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/'/g, '''); if (typeof x == 'boolean') return x; if (typeof x == 'number') return x; }
229
+ //function EscapeHtmlBreaks(x) { if (typeof x == "string") return x.replace(/&/g, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/'/g, ''').replace(/\r/g, '<br />').replace(/\n/g, '').replace(/\t/g, ' '); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
230
+ // Fetch all users from the database, keep this in memory
231
+ obj.db.GetAllType('user', function (err, docs) {
232
+ obj.common.unEscapeAllLinksFieldName(docs);
233
+ var domainUserCount = {}, i = 0;
234
+ for (i in parent.config.domains) { domainUserCount[i] = 0; }
235
+ for (i in docs) { var u = obj.users[docs[i]._id] = docs[i]; domainUserCount[u.domain]++; }
236
+ for (i in parent.config.domains) {
237
+ if ((parent.config.domains[i].share == null) && (domainUserCount[i] == 0)) {
238
+ // If newaccounts is set to no new accounts, but no accounts exists, temporarly allow account creation.
239
+ //if ((parent.config.domains[i].newaccounts === 0) || (parent.config.domains[i].newaccounts === false)) { parent.config.domains[i].newaccounts = 2; }
240
+ console.log('Server ' + ((i == '') ? '' : (i + ' ')) + 'has no users, next new account will be site administrator.');
241
+ }
242
+ }
243
+
244
+ // Fetch all device groups (meshes) from the database, keep this in memory
245
+ // As we load things in memory, we will also be doing some cleaning up.
246
+ // We will not save any clean up in the database right now, instead it will be saved next time there is a change.
247
+ obj.db.GetAllType('mesh', function (err, docs) {
248
+ obj.common.unEscapeAllLinksFieldName(docs);
249
+ for (var i in docs) { obj.meshes[docs[i]._id] = docs[i]; } // Get all meshes, including deleted ones.
250
+
251
+ // Fetch all user groups from the database, keep this in memory
252
+ obj.db.GetAllType('ugrp', function (err, docs) {
253
+ obj.common.unEscapeAllLinksFieldName(docs);
254
+
255
+ // Perform user group link cleanup
256
+ for (var i in docs) {
257
+ const ugrp = docs[i];
258
+ if (ugrp.links != null) {
259
+ for (var j in ugrp.links) {
260
+ if (j.startsWith('user/') && (obj.users[j] == null)) { delete ugrp.links[j]; } // User group has a link to a user that does not exist
261
+ 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
262
+ }
263
+ }
264
+ obj.userGroups[docs[i]._id] = docs[i]; // Get all user groups
265
+ }
266
+
267
+ // Perform device group link cleanup
268
+ for (var i in obj.meshes) {
269
+ const mesh = obj.meshes[i];
270
+ if (mesh.links != null) {
271
+ for (var j in mesh.links) {
272
+ 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
273
+ 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
274
+ }
275
+ }
276
+ }
277
+
278
+ // Perform user link cleanup
279
+ for (var i in obj.users) {
280
+ const user = obj.users[i];
281
+ if (user.links != null) {
282
+ for (var j in user.links) {
283
+ if (j.startsWith('ugrp/') && (obj.userGroups[j] == null)) { delete user.links[j]; } // User has a link to a user group that does not exist
284
+ 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
285
+ //else if (j.startsWith('node/') && (obj.nodes[j] == null)) { delete user.links[j]; } // TODO
286
+ }
287
+ //if (Object.keys(user.links).length == 0) { delete user.links; }
288
+ }
289
+ }
290
+
291
+ // We loaded the users, device groups and user group state, start the server
292
+ serverStart();
293
+ });
294
+ });
295
+ });
296
+
297
+ // Clean up a device, used before saving it in the database
298
+ obj.cleanDevice = function (device) {
299
+ // Check device links, if a link points to an unknown user, remove it.
300
+ if (device.links != null) {
301
+ for (var j in device.links) {
302
+ if ((obj.users[j] == null) && (obj.userGroups[j] == null)) {
303
+ delete device.links[j];
304
+ if (Object.keys(device.links).length == 0) { delete device.links; }
305
+ }
306
+ }
307
+ }
308
+ return device;
309
+ }
310
+
311
+ // Return statistics about this web server
312
+ obj.getStats = function () {
313
+ return {
314
+ users: Object.keys(obj.users).length,
315
+ meshes: Object.keys(obj.meshes).length,
316
+ dnsDomains: Object.keys(obj.dnsDomains).length,
317
+ relaySessionCount: obj.relaySessionCount,
318
+ relaySessionErrorCount: obj.relaySessionErrorCount,
319
+ wsagents: Object.keys(obj.wsagents).length,
320
+ wsagentsDisconnections: Object.keys(obj.wsagentsDisconnections).length,
321
+ wsagentsDisconnectionsTimer: Object.keys(obj.wsagentsDisconnectionsTimer).length,
322
+ wssessions: Object.keys(obj.wssessions).length,
323
+ wssessions2: Object.keys(obj.wssessions2).length,
324
+ wsPeerSessions: Object.keys(obj.wsPeerSessions).length,
325
+ wsPeerSessions2: Object.keys(obj.wsPeerSessions2).length,
326
+ wsPeerSessions3: Object.keys(obj.wsPeerSessions3).length,
327
+ sessionsCount: Object.keys(obj.sessionsCount).length,
328
+ wsrelays: Object.keys(obj.wsrelays).length,
329
+ wsPeerRelays: Object.keys(obj.wsPeerRelays).length,
330
+ tlsSessionStore: Object.keys(tlsSessionStore).length,
331
+ blockedUsers: obj.blockedUsers,
332
+ blockedAgents: obj.blockedAgents
333
+ };
334
+ }
335
+
336
+ // Agent counters
337
+ obj.agentStats = {
338
+ createMeshAgentCount: 0,
339
+ agentClose: 0,
340
+ agentBinaryUpdate: 0,
341
+ agentMeshCoreBinaryUpdate: 0,
342
+ coreIsStableCount: 0,
343
+ verifiedAgentConnectionCount: 0,
344
+ clearingCoreCount: 0,
345
+ updatingCoreCount: 0,
346
+ recoveryCoreIsStableCount: 0,
347
+ meshDoesNotExistCount: 0,
348
+ invalidPkcsSignatureCount: 0,
349
+ invalidRsaSignatureCount: 0,
350
+ invalidJsonCount: 0,
351
+ unknownAgentActionCount: 0,
352
+ agentBadWebCertHashCount: 0,
353
+ agentBadSignature1Count: 0,
354
+ agentBadSignature2Count: 0,
355
+ agentMaxSessionHoldCount: 0,
356
+ invalidDomainMeshCount: 0,
357
+ invalidMeshTypeCount: 0,
358
+ invalidDomainMesh2Count: 0,
359
+ invalidMeshType2Count: 0,
360
+ duplicateAgentCount: 0,
361
+ maxDomainDevicesReached: 0,
362
+ agentInTrouble: 0,
363
+ agentInBigTrouble: 0
364
+ }
365
+ obj.getAgentStats = function () { return obj.agentStats; }
366
+
367
+ // Traffic counters
368
+ obj.trafficStats = {
369
+ httpRequestCount: 0,
370
+ httpWebSocketCount: 0,
371
+ httpIn: 0,
372
+ httpOut: 0,
373
+ relayCount: {},
374
+ relayIn: {},
375
+ relayOut: {},
376
+ localRelayCount: {},
377
+ localRelayIn: {},
378
+ localRelayOut: {},
379
+ AgentCtrlIn: 0,
380
+ AgentCtrlOut: 0,
381
+ LMSIn: 0,
382
+ LMSOut: 0,
383
+ CIRAIn: 0,
384
+ CIRAOut: 0
385
+ }
386
+ obj.trafficStats.time = Date.now();
387
+ obj.getTrafficStats = function () { return obj.trafficStats; }
388
+ obj.getTrafficDelta = function (oldTraffic) { // Return the difference between the old and new data along with the delta time.
389
+ const data = obj.common.Clone(obj.trafficStats);
390
+ data.time = Date.now();
391
+ const delta = calcDelta(oldTraffic ? oldTraffic : {}, data);
392
+ if (oldTraffic && oldTraffic.time) { delta.delta = (data.time - oldTraffic.time); }
393
+ delta.time = data.time;
394
+ return { current: data, delta: delta }
395
+ }
396
+ function calcDelta(oldData, newData) { // Recursive function that computes the difference of all numbers
397
+ const r = {};
398
+ for (var i in newData) {
399
+ if (typeof newData[i] == 'object') { r[i] = calcDelta(oldData[i] ? oldData[i] : {}, newData[i]); }
400
+ if (typeof newData[i] == 'number') { if (typeof oldData[i] == 'number') { r[i] = (newData[i] - oldData[i]); } else { r[i] = newData[i]; } }
401
+ }
402
+ return r;
403
+ }
404
+
405
+ // Keep a record of the last agent issues.
406
+ obj.getAgentIssues = function () { return obj.agentIssues; }
407
+ obj.setAgentIssue = function (agent, issue) { obj.agentIssues.push([new Date().toLocaleString(), agent.remoteaddrport, issue]); while (obj.setAgentIssue.length > 50) { obj.agentIssues.shift(); } }
408
+ obj.agentIssues = [];
409
+
410
+ // Authenticate the user
411
+ obj.authenticate = function (name, pass, domain, fn) {
412
+ if ((typeof (name) != 'string') || (typeof (pass) != 'string') || (typeof (domain) != 'object')) { fn(new Error('invalid fields')); return; }
413
+ if (name.startsWith('~t:')) {
414
+ // Login token, try to fetch the token from the database
415
+ obj.db.Get('logintoken-' + name, function (err, docs) {
416
+ if (err != null) { fn(err); return; }
417
+ if ((docs == null) || (docs.length != 1)) { fn(new Error('login token not found')); return; }
418
+ const loginToken = docs[0];
419
+ if ((loginToken.expire != 0) && (loginToken.expire < Date.now())) { fn(new Error('login token expired')); return; }
420
+
421
+ // Default strong password hashing (pbkdf2 SHA384)
422
+ require('./pass').hash(pass, loginToken.salt, function (err, hash, tag) {
423
+ if (err) return fn(err);
424
+ if (hash == loginToken.hash) {
425
+ // Login username and password are valid.
426
+ var user = obj.users[loginToken.userid];
427
+ if (!user) { fn(new Error('cannot find user')); return; }
428
+ if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
429
+
430
+ // Succesful login token authentication
431
+ var loginOptions = { tokenName: loginToken.name, tokenUser: loginToken.tokenUser };
432
+ if (loginToken.expire != 0) { loginOptions.expire = loginToken.expire; }
433
+ return fn(null, user._id, null, loginOptions);
434
+ }
435
+ fn(new Error('invalid password'));
436
+ }, 0);
437
+ });
438
+ } else if (domain.auth == 'ldap') {
439
+ if (domain.ldapoptions.url == 'test') {
440
+ // Fake LDAP login
441
+ var xxuser = domain.ldapoptions[name.toLowerCase()];
442
+ if (xxuser == null) {
443
+ fn(new Error('invalid password'));
444
+ return;
445
+ } else {
446
+ var username = xxuser['displayName'];
447
+ if (domain.ldapusername) { username = xxuser[domain.ldapusername]; }
448
+ var shortname = null;
449
+ if (domain.ldapuserbinarykey) {
450
+ // Use a binary key as the userid
451
+ if (xxuser[domain.ldapuserbinarykey]) { shortname = Buffer.from(xxuser[domain.ldapuserbinarykey], 'binary').toString('hex'); }
452
+ } else if (domain.ldapuserkey) {
453
+ // Use a string key as the userid
454
+ if (xxuser[domain.ldapuserkey]) { shortname = xxuser[domain.ldapuserkey]; }
455
+ } else {
456
+ // Use the default key as the userid
457
+ if (xxuser.objectSid) { shortname = Buffer.from(xxuser.objectSid, 'binary').toString('hex').toLowerCase(); }
458
+ else if (xxuser.objectGUID) { shortname = Buffer.from(xxuser.objectGUID, 'binary').toString('hex').toLowerCase(); }
459
+ else if (xxuser.name) { shortname = xxuser.name; }
460
+ else if (xxuser.cn) { shortname = xxuser.cn; }
461
+ }
462
+ if (shortname == null) { fn(new Error('no user identifier')); return; }
463
+ if (username == null) { username = shortname; }
464
+ var userid = 'user/' + domain.id + '/' + shortname;
465
+ var user = obj.users[userid];
466
+ var email = null;
467
+ if (domain.ldapuseremail) {
468
+ email = xxuser[domain.ldapuseremail];
469
+ } else if (xxuser.mail) { // use default
470
+ email = xxuser.mail;
471
+ }
472
+ if ('[object Array]' == Object.prototype.toString.call(email)) {
473
+ // mail may be multivalued in ldap in which case, answer is an array. Use the 1st value.
474
+ email = email[0];
475
+ }
476
+ if (email) { email = email.toLowerCase(); } // it seems some code otherwhere also lowercase the emailaddress. be compatible.
477
+
478
+ if (user == null) {
479
+ // Create a new user
480
+ var user = { type: 'user', _id: userid, name: username, creation: Math.floor(Date.now() / 1000), login: Math.floor(Date.now() / 1000), access: Math.floor(Date.now() / 1000), domain: domain.id };
481
+ if (email) { user['email'] = email; user['emailVerified'] = true; }
482
+ if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; }
483
+ if (obj.common.validateStrArray(domain.newaccountrealms)) { user.groups = domain.newaccountrealms; }
484
+ var usercount = 0;
485
+ for (var i in obj.users) { if (obj.users[i].domain == domain.id) { usercount++; } }
486
+ if (usercount == 0) { user.siteadmin = 4294967295; /*if (domain.newaccounts === 2) { delete domain.newaccounts; }*/ } // If this is the first user, give the account site admin.
487
+
488
+ // Auto-join any user groups
489
+ if (typeof domain.newaccountsusergroups == 'object') {
490
+ for (var i in domain.newaccountsusergroups) {
491
+ var ugrpid = domain.newaccountsusergroups[i];
492
+ if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
493
+ var ugroup = obj.userGroups[ugrpid];
494
+ if (ugroup != null) {
495
+ // Add group to the user
496
+ if (user.links == null) { user.links = {}; }
497
+ user.links[ugroup._id] = { rights: 1 };
498
+
499
+ // Add user to the group
500
+ ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
501
+ db.Set(ugroup);
502
+
503
+ // Notify user group change
504
+ var event = { etype: 'ugrp', ugrpid: ugroup._id, name: ugroup.name, desc: ugroup.desc, action: 'usergroupchange', links: ugroup.links, msgid: 71, msgArgs: [user.name, ugroup.name], msg: 'Added user ' + user.name + ' to user group ' + ugroup.name, addUserDomain: domain.id };
505
+ 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.
506
+ parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
507
+ }
508
+ }
509
+ }
510
+
511
+ obj.users[user._id] = user;
512
+ obj.db.SetUser(user);
513
+ var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountcreate', msgid: 128, msgArgs: [user.name], msg: 'Account created, name is ' + user.name, domain: domain.id };
514
+ 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.
515
+ obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
516
+ return fn(null, user._id);
517
+ } else {
518
+ // This is an existing user
519
+ // If the display username has changes, update it.
520
+ if (user.name != username) {
521
+ user.name = username;
522
+ obj.db.SetUser(user);
523
+ var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msgid: 127, msgArgs: [user.name], msg: 'Changed account display name to ' + user.name, domain: domain.id };
524
+ 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.
525
+ parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
526
+ }
527
+ // Check if user email has changed
528
+ var emailreason = null;
529
+ if (user.email && !email) { // email unset in ldap => unset
530
+ delete user.email;
531
+ delete user.emailVerified;
532
+ emailreason = 'Unset email (no more email in LDAP)'
533
+ } else if (user.email != email) { // update email
534
+ user['email'] = email;
535
+ user['emailVerified'] = true;
536
+ emailreason = 'Set account email to ' + email + '. Sync with LDAP.';
537
+ }
538
+ if (emailreason) {
539
+ obj.db.SetUser(user);
540
+ var event = { etype: 'user', userid: userid, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: emailreason, domain: domain.id };
541
+ 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.
542
+ parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
543
+ }
544
+ // If user is locker out, block here.
545
+ if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
546
+ return fn(null, user._id);
547
+ }
548
+ }
549
+ } else {
550
+ // LDAP login
551
+ var LdapAuth = require('ldapauth-fork');
552
+ var ldap = new LdapAuth(domain.ldapoptions);
553
+ ldap.on('error', function (err) { console.log('ldap error: ', err); });
554
+ ldap.authenticate(name, pass, function (err, xxuser) {
555
+ try { ldap.close(); } catch (ex) { console.log(ex); } // Close the LDAP object
556
+ if (err) { fn(new Error('invalid password')); return; }
557
+ var shortname = null;
558
+ var email = null;
559
+ if (domain.ldapuseremail) {
560
+ email = xxuser[domain.ldapuseremail];
561
+ } else if (xxuser.mail) {
562
+ email = xxuser.mail;
563
+ }
564
+ if ('[object Array]' == Object.prototype.toString.call(email)) {
565
+ // mail may be multivalued in ldap in which case, answer would be an array. Use the 1st one.
566
+ email = email[0];
567
+ }
568
+ if (email) { email = email.toLowerCase(); } // it seems some code otherwhere also lowercase the emailaddress. be compatible.
569
+ var username = xxuser['displayName'];
570
+ if (domain.ldapusername) { username = xxuser[domain.ldapusername]; }
571
+ if (domain.ldapuserbinarykey) {
572
+ // Use a binary key as the userid
573
+ if (xxuser[domain.ldapuserbinarykey]) { shortname = Buffer.from(xxuser[domain.ldapuserbinarykey], 'binary').toString('hex').toLowerCase(); }
574
+ } else if (domain.ldapuserkey) {
575
+ // Use a string key as the userid
576
+ if (xxuser[domain.ldapuserkey]) { shortname = xxuser[domain.ldapuserkey]; }
577
+ } else {
578
+ // Use the default key as the userid
579
+ if (xxuser.objectSid) { shortname = Buffer.from(xxuser.objectSid, 'binary').toString('hex').toLowerCase(); }
580
+ else if (xxuser.objectGUID) { shortname = Buffer.from(xxuser.objectGUID, 'binary').toString('hex').toLowerCase(); }
581
+ else if (xxuser.name) { shortname = xxuser.name; }
582
+ else if (xxuser.cn) { shortname = xxuser.cn; }
583
+ }
584
+ if (shortname == null) { fn(new Error('no user identifier')); return; }
585
+ if (username == null) { username = shortname; }
586
+ var userid = 'user/' + domain.id + '/' + shortname;
587
+ var user = obj.users[userid];
588
+
589
+ if (user == null) {
590
+ // This user does not exist, create a new account.
591
+ var user = { type: 'user', _id: userid, name: username, creation: Math.floor(Date.now() / 1000), login: Math.floor(Date.now() / 1000), access: Math.floor(Date.now() / 1000), domain: domain.id };
592
+ if (email) { user['email'] = email; user['emailVerified'] = true; }
593
+ if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; }
594
+ if (obj.common.validateStrArray(domain.newaccountrealms)) { user.groups = domain.newaccountrealms; }
595
+ var usercount = 0;
596
+ for (var i in obj.users) { if (obj.users[i].domain == domain.id) { usercount++; } }
597
+ if (usercount == 0) { user.siteadmin = 4294967295; /*if (domain.newaccounts === 2) { delete domain.newaccounts; }*/ } // If this is the first user, give the account site admin.
598
+
599
+ // Auto-join any user groups
600
+ if (typeof domain.newaccountsusergroups == 'object') {
601
+ for (var i in domain.newaccountsusergroups) {
602
+ var ugrpid = domain.newaccountsusergroups[i];
603
+ if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
604
+ var ugroup = obj.userGroups[ugrpid];
605
+ if (ugroup != null) {
606
+ // Add group to the user
607
+ if (user.links == null) { user.links = {}; }
608
+ user.links[ugroup._id] = { rights: 1 };
609
+
610
+ // Add user to the group
611
+ ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
612
+ db.Set(ugroup);
613
+
614
+ // Notify user group change
615
+ var event = { etype: 'ugrp', ugrpid: ugroup._id, name: ugroup.name, desc: ugroup.desc, action: 'usergroupchange', links: ugroup.links, msgid: 71, msgArgs: [user.name, ugroup.name], msg: 'Added user ' + user.name + ' to user group ' + ugroup.name, addUserDomain: domain.id };
616
+ 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.
617
+ parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
618
+ }
619
+ }
620
+ }
621
+
622
+ obj.users[user._id] = user;
623
+ obj.db.SetUser(user);
624
+ var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountcreate', msgid: 128, msgArgs: [user.name], msg: 'Account created, name is ' + user.name, domain: domain.id };
625
+ 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.
626
+ obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
627
+ return fn(null, user._id);
628
+ } else {
629
+ // This is an existing user
630
+ // If the display username has changes, update it.
631
+ if (user.name != username) {
632
+ user.name = username;
633
+ obj.db.SetUser(user);
634
+ var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msgid: 127, msgArgs: [user.name], msg: 'Changed account display name to ' + user.name, domain: domain.id };
635
+ 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.
636
+ parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
637
+ }
638
+ // Check if user email has changed
639
+ var emailreason = null;
640
+ if (user.email && !email) { // email unset in ldap => unset
641
+ delete user.email;
642
+ delete user.emailVerified;
643
+ emailreason = 'Unset email (no more email in LDAP)'
644
+ } else if (user.email != email) { // update email
645
+ user['email'] = email;
646
+ user['emailVerified'] = true;
647
+ emailreason = 'Set account email to ' + email + '. Sync with LDAP.';
648
+ }
649
+ if (emailreason) {
650
+ obj.db.SetUser(user);
651
+ var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: emailreason, domain: domain.id };
652
+ 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.
653
+ parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
654
+ }
655
+ // If user is locker out, block here.
656
+ if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
657
+ return fn(null, user._id);
658
+ }
659
+ });
660
+ }
661
+ } else {
662
+ // Regular login
663
+ var user = obj.users['user/' + domain.id + '/' + name.toLowerCase()];
664
+ // Query the db for the given username
665
+ if (!user) { fn(new Error('cannot find user')); return; }
666
+ // Apply the same algorithm to the POSTed password, applying the hash against the pass / salt, if there is a match we found the user
667
+ if (user.salt == null) {
668
+ fn(new Error('invalid password'));
669
+ } else {
670
+ if (user.passtype != null) {
671
+ // IIS default clear or weak password hashing (SHA-1)
672
+ require('./pass').iishash(user.passtype, pass, user.salt, function (err, hash) {
673
+ if (err) return fn(err);
674
+ if (hash == user.hash) {
675
+ // Update the password to the stronger format.
676
+ 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);
677
+ if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
678
+ return fn(null, user._id);
679
+ }
680
+ fn(new Error('invalid password'), null, user.passhint);
681
+ });
682
+ } else {
683
+ // Default strong password hashing (pbkdf2 SHA384)
684
+ require('./pass').hash(pass, user.salt, function (err, hash, tag) {
685
+ if (err) return fn(err);
686
+ if (hash == user.hash) {
687
+ if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
688
+ return fn(null, user._id);
689
+ }
690
+ fn(new Error('invalid password'), null, user.passhint);
691
+ }, 0);
692
+ }
693
+ }
694
+ }
695
+ };
696
+
697
+ /*
698
+ obj.restrict = function (req, res, next) {
699
+ console.log('restrict', req.url);
700
+ var domain = getDomain(req);
701
+ if (req.session.userid) {
702
+ next();
703
+ } else {
704
+ req.session.messageid = 111; // Access denied.
705
+ res.redirect(domain.url + 'login');
706
+ }
707
+ };
708
+ */
709
+
710
+ // Check if the source IP address is in the IP list, return false if not.
711
+ function checkIpAddressEx(req, res, ipList, closeIfThis, redirectUrl) {
712
+ try {
713
+ if (req.connection) {
714
+ // HTTP(S) request
715
+ if (req.clientIp) { for (var i = 0; i < ipList.length; i++) { if (require('ipcheck').match(req.clientIp, ipList[i])) { if (closeIfThis === true) { if (typeof redirectUrl == 'string') { res.redirect(redirectUrl); } else { res.sendStatus(401); } } return true; } } }
716
+ if (closeIfThis === false) { if (typeof redirectUrl == 'string') { res.redirect(redirectUrl); } else { res.sendStatus(401); } }
717
+ } else {
718
+ // WebSocket request
719
+ 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; } } }
720
+ if (closeIfThis === false) { try { req.close(); } catch (e) { } }
721
+ }
722
+ } catch (e) { console.log(e); } // Should never happen
723
+ return false;
724
+ }
725
+
726
+ // Check if the source IP address is allowed, return domain if allowed
727
+ // If there is a fail and null is returned, the request or connection is closed already.
728
+ function checkUserIpAddress(req, res) {
729
+ if ((parent.config.settings.userblockedip != null) && (checkIpAddressEx(req, res, parent.config.settings.userblockedip, true, parent.config.settings.ipblockeduserredirect) == true)) { obj.blockedUsers++; return null; }
730
+ if ((parent.config.settings.userallowedip != null) && (checkIpAddressEx(req, res, parent.config.settings.userallowedip, false, parent.config.settings.ipblockeduserredirect) == false)) { obj.blockedUsers++; return null; }
731
+ const domain = (req.url ? getDomain(req) : getDomain(res));
732
+ if (domain == null) { parent.debug('web', 'handleRootRequest: invalid domain.'); try { res.sendStatus(404); } catch (ex) { } return; }
733
+ if ((domain.userblockedip != null) && (checkIpAddressEx(req, res, domain.userblockedip, true, domain.ipblockeduserredirect) == true)) { obj.blockedUsers++; return null; }
734
+ if ((domain.userallowedip != null) && (checkIpAddressEx(req, res, domain.userallowedip, false, domain.ipblockeduserredirect) == false)) { obj.blockedUsers++; return null; }
735
+ return domain;
736
+ }
737
+
738
+ // Check if the source IP address is allowed, return domain if allowed
739
+ // If there is a fail and null is returned, the request or connection is closed already.
740
+ function checkAgentIpAddress(req, res) {
741
+ if ((parent.config.settings.agentblockedip != null) && (checkIpAddressEx(req, res, parent.config.settings.agentblockedip, null) == true)) { obj.blockedAgents++; return null; }
742
+ if ((parent.config.settings.agentallowedip != null) && (checkIpAddressEx(req, res, parent.config.settings.agentallowedip, null) == false)) { obj.blockedAgents++; return null; }
743
+ const domain = (req.url ? getDomain(req) : getDomain(res));
744
+ if ((domain.agentblockedip != null) && (checkIpAddressEx(req, res, domain.agentblockedip, null) == true)) { obj.blockedAgents++; return null; }
745
+ if ((domain.agentallowedip != null) && (checkIpAddressEx(req, res, domain.agentallowedip, null) == false)) { obj.blockedAgents++; return null; }
746
+ return domain;
747
+ }
748
+
749
+ // Return the current domain of the request
750
+ // Request or connection says open regardless of the response
751
+ function getDomain(req) {
752
+ if (req.xdomain != null) { return req.xdomain; } // Domain already set for this request, return it.
753
+ 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.
754
+ var x = req.url.split('/');
755
+ if (x.length < 2) return parent.config.domains[''];
756
+ var y = parent.config.domains[x[1].toLowerCase()];
757
+ if ((y != null) && (y.dns == null)) { return parent.config.domains[x[1].toLowerCase()]; }
758
+ return parent.config.domains[''];
759
+ }
760
+
761
+ function handleLogoutRequest(req, res) {
762
+ const domain = checkUserIpAddress(req, res);
763
+ if (domain == null) { return; }
764
+ if (domain.auth == 'sspi') { parent.debug('web', 'handleLogoutRequest: failed checks.'); res.sendStatus(404); return; }
765
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
766
+
767
+ res.set({ 'Cache-Control': 'no-store' });
768
+ // Destroy the user's session to log them out will be re-created next request
769
+ var userid = req.session.userid;
770
+ if (req.session.userid) {
771
+ var user = obj.users[req.session.userid];
772
+ 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 }); }
773
+ if (req.session.x) { clearDestroyedSessions(); obj.destroyedSessions[req.session.userid + '/' + req.session.x] = Date.now(); } // Destroy this session
774
+ }
775
+ req.session = null;
776
+ parent.debug('web', 'handleLogoutRequest: success.');
777
+
778
+ // If this user was logged in using an authentication strategy and there is a logout URL, use it.
779
+ if ((userid != null) && (domain.authstrategies != null)) {
780
+ const u = userid.split('/')[2];
781
+ if (u.startsWith('~twitter:') && (domain.authstrategies.twitter != null) && (typeof domain.authstrategies.twitter.logouturl == 'string')) { res.redirect(domain.authstrategies.twitter.logouturl); return; }
782
+ if (u.startsWith('~google:') && (domain.authstrategies.google != null) && (typeof domain.authstrategies.google.logouturl == 'string')) { res.redirect(domain.authstrategies.google.logouturl); return; }
783
+ if (u.startsWith('~github:') && (domain.authstrategies.github != null) && (typeof domain.authstrategies.github.logouturl == 'string')) { res.redirect(domain.authstrategies.github.logouturl); return; }
784
+ if (u.startsWith('~reddit:') && (domain.authstrategies.reddit != null) && (typeof domain.authstrategies.reddit.logouturl == 'string')) { res.redirect(domain.authstrategies.reddit.logouturl); return; }
785
+ if (u.startsWith('~azure:') && (domain.authstrategies.azure != null) && (typeof domain.authstrategies.azure.logouturl == 'string')) { res.redirect(domain.authstrategies.azure.logouturl); return; }
786
+ if (u.startsWith('~oidc:') && (domain.authstrategies.oidc != null) && (typeof domain.authstrategies.oidc.logouturl == 'string')) { res.redirect(domain.authstrategies.oidc.logouturl); return; }
787
+ if (u.startsWith('~jumpcloud:') && (domain.authstrategies.jumpcloud != null) && (typeof domain.authstrategies.jumpcloud.logouturl == 'string')) { res.redirect(domain.authstrategies.jumpcloud.logouturl); return; }
788
+ if (u.startsWith('~saml:') && (domain.authstrategies.saml != null) && (typeof domain.authstrategies.saml.logouturl == 'string')) { res.redirect(domain.authstrategies.saml.logouturl); return; }
789
+ if (u.startsWith('~intel:') && (domain.authstrategies.intel != null) && (typeof domain.authstrategies.intel.logouturl == 'string')) { res.redirect(domain.authstrategies.intel.logouturl); return; }
790
+ }
791
+
792
+ // This is the default logout redirect to the login page
793
+ if (req.query.key != null) { res.redirect(domain.url + '?key=' + req.query.key); } else { res.redirect(domain.url); }
794
+ }
795
+
796
+ // Return an object with 2FA type if 2-step auth can be skipped
797
+ function checkUserOneTimePasswordSkip(domain, user, req, loginOptions) {
798
+ if (parent.config.settings.no2factorauth == true) return null;
799
+
800
+ // If this login occured using a login token, no 2FA needed.
801
+ if ((loginOptions != null) && (typeof loginOptions.tokenName === 'string')) { return { twoFactorType: 'tokenlogin' }; }
802
+
803
+ // Check if we can skip 2nd factor auth because of the source IP address
804
+ if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
805
+ for (var i in domain.passwordrequirements.skip2factor) { if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) { return { twoFactorType: 'ipaddr' }; } }
806
+ }
807
+
808
+ // Check if a 2nd factor cookie is present
809
+ if (typeof req.headers.cookie == 'string') {
810
+ const cookies = req.headers.cookie.split('; ');
811
+ for (var i in cookies) {
812
+ if (cookies[i].startsWith('twofactor=')) {
813
+ 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.
814
+ if ((twoFactorCookie != null) && ((obj.args.cookieipcheck === false) || (twoFactorCookie.ip == null) || (twoFactorCookie.ip === req.clientIp)) && (twoFactorCookie.userid == user._id)) { return { twoFactorType: 'cookie' }; }
815
+ }
816
+ }
817
+ }
818
+
819
+ return null;
820
+ }
821
+
822
+ // Return true if this user has 2-step auth active
823
+ function checkUserOneTimePasswordRequired(domain, user, req, loginOptions) {
824
+ // If this login occured using a login token, no 2FA needed.
825
+ if ((loginOptions != null) && (typeof loginOptions.tokenName === 'string')) { return false; }
826
+
827
+ // Check if we can skip 2nd factor auth because of the source IP address
828
+ if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
829
+ for (var i in domain.passwordrequirements.skip2factor) { if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) return false; }
830
+ }
831
+
832
+ // Check if a 2nd factor cookie is present
833
+ if (typeof req.headers.cookie == 'string') {
834
+ const cookies = req.headers.cookie.split('; ');
835
+ for (var i in cookies) {
836
+ if (cookies[i].startsWith('twofactor=')) {
837
+ 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.
838
+ if ((twoFactorCookie != null) && ((obj.args.cookieipcheck === false) || (twoFactorCookie.ip == null) || (twoFactorCookie.ip === req.clientIp)) && (twoFactorCookie.userid == user._id)) { return false; }
839
+ }
840
+ }
841
+ }
842
+
843
+ // See if SMS 2FA is available
844
+ var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
845
+
846
+ // Check if a 2nd factor is present
847
+ return ((parent.config.settings.no2factorauth !== true) && (sms2fa || (user.otpsecret != null) || ((user.email != null) && (user.emailVerified == true) && (domain.mailserver != null) && (user.otpekey != null)) || ((user.otphkeys != null) && (user.otphkeys.length > 0))));
848
+ }
849
+
850
+ // Check the 2-step auth token
851
+ function checkUserOneTimePassword(req, domain, user, token, hwtoken, func) {
852
+ parent.debug('web', 'checkUserOneTimePassword()');
853
+ const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.nousers !== true) && (parent.config.settings.no2factorauth !== true));
854
+ if (twoStepLoginSupported == false) { parent.debug('web', 'checkUserOneTimePassword: not supported.'); func(true); return; };
855
+
856
+ // Check if we can use OTP tokens with email
857
+ var otpemail = (domain.mailserver != null);
858
+ if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; }
859
+ var otpsms = (parent.smsserver != null);
860
+ if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; }
861
+
862
+ // Check 2FA login cookie
863
+ if ((token != null) && (token.startsWith('cookie='))) {
864
+ 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.
865
+ if ((twoFactorCookie != null) && ((obj.args.cookieipcheck === false) || (twoFactorCookie.ip == null) || (twoFactorCookie.ip === req.clientIp)) && (twoFactorCookie.userid == user._id)) { func(true, { twoFactorType: 'cookie' }); return; }
866
+ }
867
+
868
+ // Check email key
869
+ if ((otpemail) && (user.otpekey != null) && (user.otpekey.d != null) && (user.otpekey.k === token)) {
870
+ var deltaTime = (Date.now() - user.otpekey.d);
871
+ if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the email token (10000 * 60 * 5).
872
+ user.otpekey = {};
873
+ obj.db.SetUser(user);
874
+ parent.debug('web', 'checkUserOneTimePassword: success (email).');
875
+ func(true, { twoFactorType: 'email' });
876
+ return;
877
+ }
878
+ }
879
+
880
+ // Check sms key
881
+ if ((otpsms) && (user.phone != null) && (user.otpsms != null) && (user.otpsms.d != null) && (user.otpsms.k === token)) {
882
+ var deltaTime = (Date.now() - user.otpsms.d);
883
+ if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the SMS token (10000 * 60 * 5).
884
+ delete user.otpsms;
885
+ obj.db.SetUser(user);
886
+ parent.debug('web', 'checkUserOneTimePassword: success (SMS).');
887
+ func(true, { twoFactorType: 'sms' });
888
+ return;
889
+ }
890
+ }
891
+
892
+ // Check hardware key
893
+ if (user.otphkeys && (user.otphkeys.length > 0) && (typeof (hwtoken) == 'string') && (hwtoken.length > 0)) {
894
+ var authResponse = null;
895
+ try { authResponse = JSON.parse(hwtoken); } catch (ex) { }
896
+ if ((authResponse != null) && (authResponse.clientDataJSON)) {
897
+ // Get all WebAuthn keys
898
+ var webAuthnKeys = [];
899
+ for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 3) { webAuthnKeys.push(user.otphkeys[i]); } }
900
+ if (webAuthnKeys.length > 0) {
901
+ // Decode authentication response
902
+ var clientAssertionResponse = { response: {} };
903
+ clientAssertionResponse.id = authResponse.id;
904
+ clientAssertionResponse.rawId = Buffer.from(authResponse.id, 'base64');
905
+ clientAssertionResponse.response.authenticatorData = Buffer.from(authResponse.authenticatorData, 'base64');
906
+ clientAssertionResponse.response.clientDataJSON = Buffer.from(authResponse.clientDataJSON, 'base64');
907
+ clientAssertionResponse.response.signature = Buffer.from(authResponse.signature, 'base64');
908
+ clientAssertionResponse.response.userHandle = Buffer.from(authResponse.userHandle, 'base64');
909
+
910
+ // Look for the key with clientAssertionResponse.id
911
+ var webAuthnKey = null;
912
+ for (var i = 0; i < webAuthnKeys.length; i++) { if (webAuthnKeys[i].keyId == clientAssertionResponse.id) { webAuthnKey = webAuthnKeys[i]; } }
913
+
914
+ // If we found a valid key to use, let's validate the response
915
+ if (webAuthnKey != null) {
916
+ // Figure out the origin
917
+ var httpport = ((args.aliasport != null) ? args.aliasport : args.port);
918
+ var origin = 'https://' + (domain.dns ? domain.dns : parent.certificates.CommonName);
919
+ if (httpport != 443) { origin += ':' + httpport; }
920
+
921
+ var assertionExpectations = {
922
+ challenge: req.session.u2f,
923
+ origin: origin,
924
+ factor: 'either',
925
+ fmt: 'fido-u2f',
926
+ publicKey: webAuthnKey.publicKey,
927
+ prevCounter: webAuthnKey.counter,
928
+ userHandle: Buffer.from(user._id, 'binary').toString('base64')
929
+ };
930
+
931
+ var webauthnResponse = null;
932
+ try { webauthnResponse = obj.webauthn.verifyAuthenticatorAssertionResponse(clientAssertionResponse.response, assertionExpectations); } catch (ex) { parent.debug('web', 'checkUserOneTimePassword: exception ' + ex); console.log(ex); }
933
+ if ((webauthnResponse != null) && (webauthnResponse.verified === true)) {
934
+ // Update the hardware key counter and accept the 2nd factor
935
+ webAuthnKey.counter = webauthnResponse.counter;
936
+ obj.db.SetUser(user);
937
+ parent.debug('web', 'checkUserOneTimePassword: success (hardware).');
938
+ func(true, { twoFactorType: 'fido' });
939
+ } else {
940
+ parent.debug('web', 'checkUserOneTimePassword: fail (hardware).');
941
+ func(false);
942
+ }
943
+ return;
944
+ }
945
+ }
946
+ }
947
+ }
948
+
949
+ // Check Google Authenticator
950
+ const otplib = require('otplib')
951
+ otplib.authenticator.options = { window: 2 }; // Set +/- 1 minute window
952
+ if (user.otpsecret && (typeof (token) == 'string') && (token.length == 6) && (otplib.authenticator.check(token, user.otpsecret) == true)) {
953
+ parent.debug('web', 'checkUserOneTimePassword: success (authenticator).');
954
+ func(true, { twoFactorType: 'otp' });
955
+ return;
956
+ };
957
+
958
+ // Check written down keys
959
+ if ((user.otpkeys != null) && (user.otpkeys.keys != null) && (typeof (token) == 'string') && (token.length == 8)) {
960
+ var tokenNumber = parseInt(token);
961
+ for (var i = 0; i < user.otpkeys.keys.length; i++) {
962
+ if ((tokenNumber === user.otpkeys.keys[i].p) && (user.otpkeys.keys[i].u === true)) {
963
+ parent.debug('web', 'checkUserOneTimePassword: success (one-time).');
964
+ user.otpkeys.keys[i].u = false; func(true, { twoFactorType: 'backup' }); return;
965
+ }
966
+ }
967
+ }
968
+
969
+ // Check OTP hardware key (Yubikey OTP)
970
+ 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)) {
971
+ var keyId = token.substring(0, 12);
972
+
973
+ // Find a matching OTP key
974
+ var match = false;
975
+ for (var i = 0; i < user.otphkeys.length; i++) { if ((user.otphkeys[i].type === 2) && (user.otphkeys[i].keyid === keyId)) { match = true; } }
976
+
977
+ // If we have a match, check the OTP
978
+ if (match === true) {
979
+ var yubikeyotp = require('yubikeyotp');
980
+ var request = { otp: token, id: domain.yubikey.id, key: domain.yubikey.secret, timestamp: true }
981
+ if (domain.yubikey.proxy) { request.requestParams = { proxy: domain.yubikey.proxy }; }
982
+ yubikeyotp.verifyOTP(request, function (err, results) {
983
+ if ((results != null) && (results.status == 'OK')) {
984
+ parent.debug('web', 'checkUserOneTimePassword: success (Yubikey).');
985
+ func(true, { twoFactorType: 'hwotp' });
986
+ } else {
987
+ parent.debug('web', 'checkUserOneTimePassword: fail (Yubikey).');
988
+ func(false);
989
+ }
990
+ });
991
+ return;
992
+ }
993
+ }
994
+
995
+ parent.debug('web', 'checkUserOneTimePassword: fail (2).');
996
+ func(false);
997
+ }
998
+
999
+ // Return a U2F hardware key challenge
1000
+ function getHardwareKeyChallenge(req, domain, user, func) {
1001
+ delete req.session.u2f;
1002
+ if (user.otphkeys && (user.otphkeys.length > 0)) {
1003
+ // Get all WebAuthn keys
1004
+ var webAuthnKeys = [];
1005
+ for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 3) { webAuthnKeys.push(user.otphkeys[i]); } }
1006
+ if (webAuthnKeys.length > 0) {
1007
+ // Generate a Webauthn challenge, this is really easy, no need to call any modules to do this.
1008
+ var authnOptions = { type: 'webAuthn', keyIds: [], timeout: 60000, challenge: obj.crypto.randomBytes(64).toString('base64') };
1009
+ for (var i = 0; i < webAuthnKeys.length; i++) { authnOptions.keyIds.push(webAuthnKeys[i].keyId); }
1010
+ req.session.u2f = authnOptions.challenge;
1011
+ parent.debug('web', 'getHardwareKeyChallenge: success');
1012
+ func(JSON.stringify(authnOptions));
1013
+ return;
1014
+ }
1015
+ }
1016
+ parent.debug('web', 'getHardwareKeyChallenge: fail');
1017
+ func('');
1018
+ }
1019
+
1020
+ // Redirect a root request to a different page
1021
+ function handleRootRedirect(req, res, direct) {
1022
+ const domain = checkUserIpAddress(req, res);
1023
+ if (domain == null) { return; }
1024
+ res.redirect(domain.rootredirect + getQueryPortion(req));
1025
+ }
1026
+
1027
+ function handleLoginRequest(req, res, direct) {
1028
+ const domain = checkUserIpAddress(req, res);
1029
+ if (domain == null) { return; }
1030
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1031
+
1032
+ // Check if this is a banned ip address
1033
+ if (obj.checkAllowLogin(req) == false) {
1034
+ // Wait and redirect the user
1035
+ setTimeout(function () {
1036
+ req.session.messageid = 114; // IP address blocked, try again later.
1037
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1038
+ }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
1039
+ return;
1040
+ }
1041
+
1042
+ // Normally, use the body username/password. If this is a token, use the username/password in the session.
1043
+ var xusername = req.body.username, xpassword = req.body.password;
1044
+ if ((xusername == null) && (xpassword == null) && (req.body.token != null)) { xusername = req.session.tuser; xpassword = req.session.tpass; }
1045
+
1046
+ // Authenticate the user
1047
+ obj.authenticate(xusername, xpassword, domain, function (err, userid, passhint, loginOptions) {
1048
+ if (userid) {
1049
+ var user = obj.users[userid];
1050
+
1051
+ // Check if we are in maintenance mode
1052
+ if ((parent.config.settings.maintenancemode != null) && (user.siteadmin != 4294967295)) {
1053
+ req.session.messageid = 115; // Server under maintenance
1054
+ req.session.loginmode = 1;
1055
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1056
+ return;
1057
+ }
1058
+
1059
+ var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.email != null) && (user.emailVerified == true) && (user.otpekey != null));
1060
+ var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
1061
+ var push2fa = ((parent.firebase != null) && (user.otpdev != null));
1062
+
1063
+ // Check if two factor can be skipped
1064
+ const twoFactorSkip = checkUserOneTimePasswordSkip(domain, user, req, loginOptions);
1065
+
1066
+ // Check if this user has 2-step login active
1067
+ if ((twoFactorSkip == null) && (req.session.loginmode != 6) && checkUserOneTimePasswordRequired(domain, user, req, loginOptions)) {
1068
+ if ((req.body.hwtoken == '**timeout**')) {
1069
+ delete req.session; // Clear the session
1070
+ res.redirect(domain.url + getQueryPortion(req));
1071
+ return;
1072
+ }
1073
+
1074
+ if ((req.body.hwtoken == '**email**') && email2fa) {
1075
+ user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
1076
+ obj.db.SetUser(user);
1077
+ parent.debug('web', 'Sending 2FA email to: ' + user.email);
1078
+ domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key);
1079
+ req.session.messageid = 2; // "Email sent" message
1080
+ req.session.loginmode = 4;
1081
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1082
+ return;
1083
+ }
1084
+
1085
+ if ((req.body.hwtoken == '**sms**') && sms2fa) {
1086
+ // Cause a token to be sent to the user's phone number
1087
+ user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
1088
+ obj.db.SetUser(user);
1089
+ parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
1090
+ parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
1091
+ // Ask for a login token & confirm sms was sent
1092
+ req.session.messageid = 4; // "SMS sent" message
1093
+ req.session.loginmode = 4;
1094
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1095
+ return;
1096
+ }
1097
+
1098
+ // Handle device push notification 2FA request
1099
+ // We create a browser cookie, send it back and when the browser connects it's web socket, it will trigger the push notification.
1100
+ if ((req.body.hwtoken == '**push**') && push2fa && ((domain.passwordrequirements == null) || (domain.passwordrequirements.push2factor != false))) {
1101
+ const logincodeb64 = Buffer.from(obj.common.zeroPad(getRandomSixDigitInteger(), 6)).toString('base64');
1102
+ const sessioncode = obj.crypto.randomBytes(24).toString('base64');
1103
+
1104
+ // Create a browser cookie so the browser can connect using websocket and wait for device accept/reject.
1105
+ const browserCookie = parent.encodeCookie({ a: 'waitAuth', c: logincodeb64, u: user._id, n: user.otpdev, s: sessioncode, d: domain.id });
1106
+
1107
+ // Get the HTTPS port
1108
+ var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port if specified
1109
+
1110
+ // Get the agent connection server name
1111
+ var serverName = obj.getWebServerName(domain);
1112
+ if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
1113
+
1114
+ // Build the connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
1115
+ var xdomain = (domain.dns == null) ? domain.id : '';
1116
+ if (xdomain != '') xdomain += '/';
1117
+ var url = 'wss://' + serverName + ':' + httpsPort + '/' + xdomain + '2fahold.ashx?c=' + browserCookie;
1118
+
1119
+ // Request that the login page wait for device auth
1120
+ req.session.messageid = 5; // "Sending notification..." message
1121
+ req.session.passhint = url;
1122
+ req.session.loginmode = 8;
1123
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1124
+ return;
1125
+ }
1126
+
1127
+ checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result, authData) {
1128
+ if (result == false) {
1129
+ var randomWaitTime = 0;
1130
+
1131
+ // Check if 2FA is allowed for this IP address
1132
+ if (obj.checkAllow2Fa(req) == false) {
1133
+ // Wait and redirect the user
1134
+ setTimeout(function () {
1135
+ req.session.messageid = 114; // IP address blocked, try again later.
1136
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1137
+ }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
1138
+ return;
1139
+ }
1140
+
1141
+ // 2-step auth is required, but the token is not present or not valid.
1142
+ if ((req.body.token != null) || (req.body.hwtoken != null)) {
1143
+ randomWaitTime = 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095); // This is a fail, wait a random time. 2 to 6 seconds.
1144
+ req.session.messageid = 108; // Invalid token, try again.
1145
+ if (obj.parent.authlog) { obj.parent.authLog('https', 'Failed 2FA for ' + xusername + ' from ' + cleanRemoteAddr(req.clientIp) + ' port ' + req.port); }
1146
+ parent.debug('web', 'handleLoginRequest: invalid 2FA token');
1147
+ const ua = getUserAgentInfo(req);
1148
+ 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, msgid: 108, msgArgs: [req.clientIp, ua.browserStr, ua.osStr] });
1149
+ obj.setbad2Fa(req);
1150
+ } else {
1151
+ parent.debug('web', 'handleLoginRequest: 2FA token required');
1152
+ }
1153
+
1154
+ // Wait and redirect the user
1155
+ setTimeout(function () {
1156
+ req.session.loginmode = 4;
1157
+ if ((user.email != null) && (user.emailVerified == true) && (domain.mailserver != null) && (user.otpekey != null)) { req.session.temail = 1; }
1158
+ if ((user.phone != null) && (parent.smsserver != null)) { req.session.tsms = 1; }
1159
+ if ((user.otpdev != null) && (parent.firebase != null)) { req.session.tpush = 1; }
1160
+ req.session.tuserid = userid;
1161
+ req.session.tuser = xusername;
1162
+ req.session.tpass = xpassword;
1163
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1164
+ }, randomWaitTime);
1165
+ } else {
1166
+ // Check if we need to remember this device
1167
+ if ((req.body.remembertoken === 'on') && ((domain.twofactorcookiedurationdays == null) || (domain.twofactorcookiedurationdays > 0))) {
1168
+ var maxCookieAge = domain.twofactorcookiedurationdays;
1169
+ if (typeof maxCookieAge != 'number') { maxCookieAge = 30; }
1170
+ const twoFactorCookie = obj.parent.encodeCookie({ userid: user._id, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey);
1171
+ res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: ((parent.config.settings.cookieipcheck === false) ? 'none' : 'strict'), secure: true });
1172
+ }
1173
+
1174
+ // Check if email address needs to be confirmed
1175
+ const emailcheck = ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap'))
1176
+ if (emailcheck && (user.emailVerified !== true)) {
1177
+ parent.debug('web', 'Redirecting using ' + user.name + ' to email check login page');
1178
+ req.session.messageid = 3; // "Email verification required" message
1179
+ req.session.loginmode = 7;
1180
+ req.session.passhint = user.email;
1181
+ req.session.cuserid = userid;
1182
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1183
+ return;
1184
+ }
1185
+
1186
+ // Login successful
1187
+ if (obj.parent.authlog) { obj.parent.authLog('https', 'Accepted password for ' + xusername + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); }
1188
+ parent.debug('web', 'handleLoginRequest: successful 2FA login');
1189
+ if (authData != null) { if (loginOptions == null) { loginOptions = {}; } loginOptions.twoFactorType = authData.twoFactorType; }
1190
+ completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions);
1191
+ }
1192
+ });
1193
+ return;
1194
+ }
1195
+
1196
+ // Check if email address needs to be confirmed
1197
+ const emailcheck = ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap'))
1198
+ if (emailcheck && (user.emailVerified !== true)) {
1199
+ parent.debug('web', 'Redirecting using ' + user.name + ' to email check login page');
1200
+ req.session.messageid = 3; // "Email verification required" message
1201
+ req.session.loginmode = 7;
1202
+ req.session.passhint = user.email;
1203
+ req.session.cuserid = userid;
1204
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1205
+ return;
1206
+ }
1207
+
1208
+ // Login successful
1209
+ if (obj.parent.authlog) { obj.parent.authLog('https', 'Accepted password for ' + xusername + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); }
1210
+ parent.debug('web', 'handleLoginRequest: successful login');
1211
+ if (twoFactorSkip != null) { if (loginOptions == null) { loginOptions = {}; } loginOptions.twoFactorType = twoFactorSkip.twoFactorType; }
1212
+ completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions);
1213
+ } else {
1214
+ // Login failed, log the error
1215
+ if (obj.parent.authlog) { obj.parent.authLog('https', 'Failed password for ' + xusername + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); }
1216
+
1217
+ // Wait a random delay
1218
+ setTimeout(function () {
1219
+ // If the account is locked, display that.
1220
+ if (typeof xusername == 'string') {
1221
+ var xuserid = 'user/' + domain.id + '/' + xusername.toLowerCase();
1222
+ if (err == 'locked') {
1223
+ parent.debug('web', 'handleLoginRequest: login failed, locked account');
1224
+ req.session.messageid = 110; // Account locked.
1225
+ const ua = getUserAgentInfo(req);
1226
+ 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, msgid: 109, msgArgs: [req.clientIp, ua.browserStr, ua.osStr] });
1227
+ obj.setbadLogin(req);
1228
+ } else {
1229
+ parent.debug('web', 'handleLoginRequest: login failed, bad username and password');
1230
+ req.session.messageid = 112; // Login failed, check username and password.
1231
+ const ua = getUserAgentInfo(req);
1232
+ obj.parent.DispatchEvent(['*', 'server-users', xuserid], obj, { action: 'authfail', userid: xuserid, username: xusername, domain: domain.id, msg: 'Invalid user login attempt from ' + req.clientIp, msgid: 110, msgArgs: [req.clientIp, ua.browserStr, ua.osStr] });
1233
+ obj.setbadLogin(req);
1234
+ }
1235
+ }
1236
+
1237
+ // Clean up login mode and display password hint if present.
1238
+ delete req.session.loginmode;
1239
+ if ((passhint != null) && (passhint.length > 0)) {
1240
+ req.session.passhint = passhint;
1241
+ } else {
1242
+ delete req.session.passhint;
1243
+ }
1244
+
1245
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1246
+ }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095)); // Wait for 2 to ~6 seconds.
1247
+ }
1248
+ });
1249
+ }
1250
+
1251
+ function completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions) {
1252
+ // Check if we need to change the password
1253
+ 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))))) {
1254
+ // Request a password change
1255
+ parent.debug('web', 'handleLoginRequest: login ok, password change requested');
1256
+ req.session.loginmode = 6;
1257
+ req.session.messageid = 113; // Password change requested.
1258
+ req.session.resettokenuserid = userid;
1259
+ req.session.resettokenusername = xusername;
1260
+ req.session.resettokenpassword = xpassword;
1261
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1262
+ return;
1263
+ }
1264
+
1265
+ // Save login time
1266
+ user.pastlogin = user.login;
1267
+ user.login = user.access = Math.floor(Date.now() / 1000);
1268
+ obj.db.SetUser(user);
1269
+
1270
+ // Notify account login
1271
+ const targets = ['*', 'server-users', user._id];
1272
+ if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
1273
+ const ua = getUserAgentInfo(req);
1274
+ const loginEvent = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'login', msgid: 107, msgArgs: [req.clientIp, ua.browserStr, ua.osStr], msg: 'Account login', domain: domain.id, ip: req.clientIp, userAgent: req.headers['user-agent'] };
1275
+ if (loginOptions != null) {
1276
+ if ((loginOptions.tokenName != null) && (loginOptions.tokenUser != null)) { loginEvent.tokenName = loginOptions.tokenName; loginEvent.tokenUser = loginOptions.tokenUser; } // If a login token was used, add it to the event.
1277
+ if (loginOptions.twoFactorType != null) { loginEvent.twoFactorType = loginOptions.twoFactorType; }
1278
+ }
1279
+ obj.parent.DispatchEvent(targets, obj, loginEvent);
1280
+
1281
+ // Regenerate session when signing in to prevent fixation
1282
+ //req.session.regenerate(function () {
1283
+ // Store the user's primary key in the session store to be retrieved, or in this case the entire user object
1284
+ delete req.session.u2f;
1285
+ delete req.session.loginmode;
1286
+ delete req.session.tuserid;
1287
+ delete req.session.tuser;
1288
+ delete req.session.tpass;
1289
+ delete req.session.temail;
1290
+ delete req.session.tsms;
1291
+ delete req.session.tpush;
1292
+ delete req.session.messageid;
1293
+ delete req.session.passhint;
1294
+ delete req.session.cuserid;
1295
+ delete req.session.expire;
1296
+ delete req.session.currentNode;
1297
+ req.session.userid = userid;
1298
+ req.session.ip = req.clientIp;
1299
+ setSessionRandom(req);
1300
+
1301
+ // If a login token was used, add this information and expire time to the session.
1302
+ if ((loginOptions != null) && (loginOptions.tokenName != null) && (loginOptions.tokenUser != null)) {
1303
+ req.session.loginToken = loginOptions.tokenUser;
1304
+ if (loginOptions.expire != null) { req.session.expire = loginOptions.expire; }
1305
+ }
1306
+
1307
+ if (req.body.viewmode) { req.session.viewmode = req.body.viewmode; }
1308
+ if (req.body.host) {
1309
+ // TODO: This is a terrible search!!! FIX THIS.
1310
+ /*
1311
+ obj.db.GetAllType('node', function (err, docs) {
1312
+ for (var i = 0; i < docs.length; i++) {
1313
+ if (docs[i].name == req.body.host) {
1314
+ req.session.currentNode = docs[i]._id;
1315
+ break;
1316
+ }
1317
+ }
1318
+ console.log("CurrentNode: " + req.session.currentNode);
1319
+ // This redirect happens after finding node is completed
1320
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1321
+ });
1322
+ */
1323
+ parent.debug('web', 'handleLoginRequest: login ok (1)');
1324
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } // Temporary
1325
+ } else {
1326
+ parent.debug('web', 'handleLoginRequest: login ok (2)');
1327
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1328
+ }
1329
+ //});
1330
+ }
1331
+
1332
+ function handleCreateAccountRequest(req, res, direct) {
1333
+ const domain = checkUserIpAddress(req, res);
1334
+ if (domain == null) { return; }
1335
+ if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handleCreateAccountRequest: failed checks.'); res.sendStatus(404); return; }
1336
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1337
+ if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
1338
+
1339
+ // Check if we are in maintenance mode
1340
+ if (parent.config.settings.maintenancemode != null) {
1341
+ req.session.messageid = 115; // Server under maintenance
1342
+ req.session.loginmode = 1;
1343
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1344
+ return;
1345
+ }
1346
+
1347
+ // Always lowercase the email address
1348
+ if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
1349
+
1350
+ // If the email is the username, set this here.
1351
+ if (domain.usernameisemail) { req.body.username = req.body.email; }
1352
+
1353
+ // Accounts that start with ~ are not allowed
1354
+ if ((typeof req.body.username != 'string') || (req.body.username.length < 1) || (req.body.username[0] == '~')) {
1355
+ parent.debug('web', 'handleCreateAccountRequest: unable to create account (0)');
1356
+ req.session.loginmode = 2;
1357
+ req.session.messageid = 100; // Unable to create account.
1358
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1359
+ return;
1360
+ }
1361
+
1362
+ // Count the number of users in this domain
1363
+ var domainUserCount = 0;
1364
+ for (var i in obj.users) { if (obj.users[i].domain == domain.id) { domainUserCount++; } }
1365
+
1366
+ // Check if we are allowed to create new users using the login screen
1367
+ if ((domain.newaccounts !== 1) && (domain.newaccounts !== true) && (domainUserCount > 0)) {
1368
+ parent.debug('web', 'handleCreateAccountRequest: domainUserCount > 1.');
1369
+ res.sendStatus(401);
1370
+ return;
1371
+ }
1372
+
1373
+ // Check if this request is for an allows email domain
1374
+ if ((domain.newaccountemaildomains != null) && Array.isArray(domain.newaccountemaildomains)) {
1375
+ var i = -1;
1376
+ if (typeof req.body.email == 'string') { i = req.body.email.indexOf('@'); }
1377
+ if (i == -1) {
1378
+ parent.debug('web', 'handleCreateAccountRequest: unable to create account (1)');
1379
+ req.session.loginmode = 2;
1380
+ req.session.messageid = 100; // Unable to create account.
1381
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1382
+ return;
1383
+ }
1384
+ var emailok = false, emaildomain = req.body.email.substring(i + 1).toLowerCase();
1385
+ for (var i in domain.newaccountemaildomains) { if (emaildomain == domain.newaccountemaildomains[i].toLowerCase()) { emailok = true; } }
1386
+ if (emailok == false) {
1387
+ parent.debug('web', 'handleCreateAccountRequest: unable to create account (2)');
1388
+ req.session.loginmode = 2;
1389
+ req.session.messageid = 100; // Unable to create account.
1390
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1391
+ return;
1392
+ }
1393
+ }
1394
+
1395
+ // Check if we exceed the maximum number of user accounts
1396
+ obj.db.isMaxType(domain.limits.maxuseraccounts, 'user', domain.id, function (maxExceed) {
1397
+ if (maxExceed) {
1398
+ parent.debug('web', 'handleCreateAccountRequest: account limit reached');
1399
+ req.session.loginmode = 2;
1400
+ req.session.messageid = 101; // Account limit reached.
1401
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1402
+ } else {
1403
+ 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)) {
1404
+ parent.debug('web', 'handleCreateAccountRequest: unable to create account (3)');
1405
+ req.session.loginmode = 2;
1406
+ req.session.messageid = 100; // Unable to create account.
1407
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1408
+ } else {
1409
+ // Check if this email was already verified
1410
+ obj.db.GetUserWithVerifiedEmail(domain.id, req.body.email, function (err, docs) {
1411
+ if ((docs != null) && (docs.length > 0)) {
1412
+ parent.debug('web', 'handleCreateAccountRequest: Existing account with this email address');
1413
+ req.session.loginmode = 2;
1414
+ req.session.messageid = 102; // Existing account with this email address.
1415
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1416
+ } else {
1417
+ // Check if there is domain.newAccountToken, check if supplied token is valid
1418
+ if ((domain.newaccountspass != null) && (domain.newaccountspass != '') && (req.body.anewaccountpass != domain.newaccountspass)) {
1419
+ parent.debug('web', 'handleCreateAccountRequest: Invalid account creation token');
1420
+ req.session.loginmode = 2;
1421
+ req.session.messageid = 103; // Invalid account creation token.
1422
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1423
+ return;
1424
+ }
1425
+ // Check if user exists
1426
+ if (obj.users['user/' + domain.id + '/' + req.body.username.toLowerCase()]) {
1427
+ parent.debug('web', 'handleCreateAccountRequest: Username already exists');
1428
+ req.session.loginmode = 2;
1429
+ req.session.messageid = 104; // Username already exists.
1430
+ } else {
1431
+ 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), access: Math.floor(Date.now() / 1000), domain: domain.id };
1432
+ if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; }
1433
+ if (obj.common.validateStrArray(domain.newaccountrealms)) { user.groups = domain.newaccountrealms; }
1434
+ 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; }
1435
+ if (domainUserCount == 0) { user.siteadmin = 4294967295; /*if (domain.newaccounts === 2) { delete domain.newaccounts; }*/ } // If this is the first user, give the account site admin.
1436
+
1437
+ // Auto-join any user groups
1438
+ if (typeof domain.newaccountsusergroups == 'object') {
1439
+ for (var i in domain.newaccountsusergroups) {
1440
+ var ugrpid = domain.newaccountsusergroups[i];
1441
+ if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
1442
+ var ugroup = obj.userGroups[ugrpid];
1443
+ if (ugroup != null) {
1444
+ // Add group to the user
1445
+ if (user.links == null) { user.links = {}; }
1446
+ user.links[ugroup._id] = { rights: 1 };
1447
+
1448
+ // Add user to the group
1449
+ ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
1450
+ db.Set(ugroup);
1451
+
1452
+ // Notify user group change
1453
+ 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 };
1454
+ 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.
1455
+ parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
1456
+ }
1457
+ }
1458
+ }
1459
+
1460
+ obj.users[user._id] = user;
1461
+ req.session.userid = user._id;
1462
+ req.session.ip = req.clientIp; // Bind this session to the IP address of the request
1463
+ setSessionRandom(req);
1464
+ // Create a user, generate a salt and hash the password
1465
+ require('./pass').hash(req.body.password1, function (err, salt, hash, tag) {
1466
+ if (err) throw err;
1467
+ user.salt = salt;
1468
+ user.hash = hash;
1469
+ delete user.passtype;
1470
+ obj.db.SetUser(user);
1471
+
1472
+ // Send the verification email
1473
+ if ((domain.mailserver != null) && (domain.auth != 'sspi') && (domain.auth != 'ldap') && (obj.common.validateEmail(user.email, 1, 256) == true)) { domain.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key); }
1474
+ }, 0);
1475
+ 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 };
1476
+ 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.
1477
+ obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
1478
+ }
1479
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1480
+ }
1481
+ });
1482
+ }
1483
+ }
1484
+ });
1485
+ }
1486
+
1487
+ // Called to process an account password reset
1488
+ function handleResetPasswordRequest(req, res, direct) {
1489
+ const domain = checkUserIpAddress(req, res);
1490
+ if (domain == null) { return; }
1491
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1492
+ if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
1493
+
1494
+ // Check everything is ok
1495
+ const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false));
1496
+ if ((allowAccountReset === false) || (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')) {
1497
+ parent.debug('web', 'handleResetPasswordRequest: checks failed');
1498
+ delete req.session.u2f;
1499
+ delete req.session.loginmode;
1500
+ delete req.session.tuserid;
1501
+ delete req.session.tuser;
1502
+ delete req.session.tpass;
1503
+ delete req.session.resettokenuserid;
1504
+ delete req.session.resettokenusername;
1505
+ delete req.session.resettokenpassword;
1506
+ delete req.session.temail;
1507
+ delete req.session.tsms;
1508
+ delete req.session.tpush;
1509
+ delete req.session.messageid;
1510
+ delete req.session.passhint;
1511
+ delete req.session.cuserid;
1512
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1513
+ return;
1514
+ }
1515
+
1516
+ // Authenticate the user
1517
+ obj.authenticate(req.session.resettokenusername, req.session.resettokenpassword, domain, function (err, userid, passhint, loginOptions) {
1518
+ if (userid) {
1519
+ // Login
1520
+ var user = obj.users[userid];
1521
+
1522
+ // If we have password requirements, check this here.
1523
+ if (!obj.common.checkPasswordRequirements(req.body.rpassword1, domain.passwordrequirements)) {
1524
+ parent.debug('web', 'handleResetPasswordRequest: password rejected, use a different one (1)');
1525
+ req.session.loginmode = 6;
1526
+ req.session.messageid = 105; // Password rejected, use a different one.
1527
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1528
+ return;
1529
+ }
1530
+
1531
+ // Check if the password is the same as a previous one
1532
+ obj.checkOldUserPasswords(domain, user, req.body.rpassword1, function (result) {
1533
+ if (result != 0) {
1534
+ // This is the same password as an older one, request a password change again
1535
+ parent.debug('web', 'handleResetPasswordRequest: password rejected, use a different one (2)');
1536
+ req.session.loginmode = 6;
1537
+ req.session.messageid = 105; // Password rejected, use a different one.
1538
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1539
+ } else {
1540
+ // Update the password, use a different salt.
1541
+ require('./pass').hash(req.body.rpassword1, function (err, salt, hash, tag) {
1542
+ const nowSeconds = Math.floor(Date.now() / 1000);
1543
+ if (err) { parent.debug('web', 'handleResetPasswordRequest: hash error.'); throw err; }
1544
+
1545
+ if (domain.passwordrequirements != null) {
1546
+ // Save password hint if this feature is enabled
1547
+ 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; }
1548
+
1549
+ // Save previous password if this feature is enabled
1550
+ if ((typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
1551
+ if (user.oldpasswords == null) { user.oldpasswords = []; }
1552
+ user.oldpasswords.push({ salt: user.salt, hash: user.hash, start: user.passchange, end: nowSeconds });
1553
+ const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
1554
+ if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
1555
+ }
1556
+ }
1557
+
1558
+ user.salt = salt;
1559
+ user.hash = hash;
1560
+ user.passchange = user.access = nowSeconds;
1561
+ delete user.passtype;
1562
+ obj.db.SetUser(user);
1563
+
1564
+ // Event the account change
1565
+ var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'User password reset', domain: domain.id };
1566
+ 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.
1567
+ obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
1568
+
1569
+ // Login successful
1570
+ parent.debug('web', 'handleResetPasswordRequest: success');
1571
+ req.session.userid = userid;
1572
+ req.session.ip = req.clientIp; // Bind this session to the IP address of the request
1573
+ setSessionRandom(req);
1574
+ completeLoginRequest(req, res, domain, obj.users[userid], userid, req.session.tuser, req.session.tpass, direct, loginOptions);
1575
+ }, 0);
1576
+ }
1577
+ }, 0);
1578
+ } else {
1579
+ // Failed, error out.
1580
+ parent.debug('web', 'handleResetPasswordRequest: failed authenticate()');
1581
+ delete req.session.u2f;
1582
+ delete req.session.loginmode;
1583
+ delete req.session.tuserid;
1584
+ delete req.session.tuser;
1585
+ delete req.session.tpass;
1586
+ delete req.session.resettokenuserid;
1587
+ delete req.session.resettokenusername;
1588
+ delete req.session.resettokenpassword;
1589
+ delete req.session.temail;
1590
+ delete req.session.tsms;
1591
+ delete req.session.tpush;
1592
+ delete req.session.messageid;
1593
+ delete req.session.passhint;
1594
+ delete req.session.cuserid;
1595
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1596
+ return;
1597
+ }
1598
+ });
1599
+ }
1600
+
1601
+ // Called to process an account reset request
1602
+ function handleResetAccountRequest(req, res, direct) {
1603
+ const domain = checkUserIpAddress(req, res);
1604
+ if (domain == null) { return; }
1605
+ const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false));
1606
+ if ((allowAccountReset === false) || (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; }
1607
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1608
+ if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
1609
+
1610
+ // Always lowercase the email address
1611
+ if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
1612
+
1613
+ // Get the email from the body or session.
1614
+ var email = req.body.email;
1615
+ if ((email == null) || (email == '')) { email = req.session.temail; }
1616
+
1617
+ // Check the email string format
1618
+ if (!email || checkEmail(email) == false) {
1619
+ parent.debug('web', 'handleResetAccountRequest: Invalid email');
1620
+ req.session.loginmode = 3;
1621
+ req.session.messageid = 106; // Invalid email.
1622
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1623
+ } else {
1624
+ obj.db.GetUserWithVerifiedEmail(domain.id, email, function (err, docs) {
1625
+ // Remove all accounts that start with ~ since they are special accounts.
1626
+ var cleanDocs = [];
1627
+ if ((err == null) && (docs.length > 0)) {
1628
+ for (var i in docs) {
1629
+ const user = docs[i];
1630
+ const locked = ((user.siteadmin != null) && (user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)); // No password recovery for locked accounts
1631
+ const specialAccount = (user._id.split('/')[2].startsWith('~')); // No password recovery for special accounts
1632
+ if ((specialAccount == false) && (locked == false)) { cleanDocs.push(user); }
1633
+ }
1634
+ }
1635
+ docs = cleanDocs;
1636
+
1637
+ // Check if we have any account that match this email address
1638
+ if ((err != null) || (docs.length == 0)) {
1639
+ parent.debug('web', 'handleResetAccountRequest: Account not found');
1640
+ req.session.loginmode = 3;
1641
+ 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.
1642
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1643
+ } else {
1644
+ // 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.
1645
+ var responseSent = false;
1646
+ for (var i in docs) {
1647
+ var user = docs[i];
1648
+ if (checkUserOneTimePasswordRequired(domain, user, req) == true) {
1649
+ // Second factor setup, request it now.
1650
+ checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result, authData) {
1651
+ if (result == false) {
1652
+ if (i == 0) {
1653
+
1654
+ // Check if 2FA is allowed for this IP address
1655
+ if (obj.checkAllow2Fa(req) == false) {
1656
+ // Wait and redirect the user
1657
+ setTimeout(function () {
1658
+ req.session.messageid = 114; // IP address blocked, try again later.
1659
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1660
+ }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
1661
+ return;
1662
+ }
1663
+
1664
+ // 2-step auth is required, but the token is not present or not valid.
1665
+ parent.debug('web', 'handleResetAccountRequest: Invalid 2FA token, try again');
1666
+ if ((req.body.token != null) || (req.body.hwtoken != null)) {
1667
+ var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
1668
+ if ((req.body.hwtoken == '**sms**') && sms2fa) {
1669
+ // Cause a token to be sent to the user's phone number
1670
+ user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
1671
+ obj.db.SetUser(user);
1672
+ parent.debug('web', 'Sending 2FA SMS for password recovery to: ' + user.phone);
1673
+ parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
1674
+ req.session.messageid = 4; // SMS sent.
1675
+ } else {
1676
+ req.session.messageid = 108; // Invalid token, try again.
1677
+ const ua = getUserAgentInfo(req);
1678
+ 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, msgid: 108, msgArgs: [req.clientIp, ua.browserStr, ua.osStr] });
1679
+ obj.setbad2Fa(req);
1680
+ }
1681
+ }
1682
+ req.session.loginmode = 5;
1683
+ req.session.temail = email;
1684
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1685
+ }
1686
+ } else {
1687
+ // Send email to perform recovery.
1688
+ delete req.session.temail;
1689
+ if (domain.mailserver != null) {
1690
+ domain.mailserver.sendAccountResetMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
1691
+ if (i == 0) {
1692
+ parent.debug('web', 'handleResetAccountRequest: Hold on, reset mail sent.');
1693
+ req.session.loginmode = 1;
1694
+ req.session.messageid = 1; // If valid, reset mail sent.
1695
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1696
+ }
1697
+ } else {
1698
+ if (i == 0) {
1699
+ parent.debug('web', 'handleResetAccountRequest: Unable to sent email.');
1700
+ req.session.loginmode = 3;
1701
+ req.session.messageid = 109; // Unable to sent email.
1702
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1703
+ }
1704
+ }
1705
+ }
1706
+ });
1707
+ } else {
1708
+ // No second factor, send email to perform recovery.
1709
+ if (domain.mailserver != null) {
1710
+ domain.mailserver.sendAccountResetMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
1711
+ if (i == 0) {
1712
+ parent.debug('web', 'handleResetAccountRequest: Hold on, reset mail sent.');
1713
+ req.session.loginmode = 1;
1714
+ req.session.messageid = 1; // If valid, reset mail sent.
1715
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1716
+ }
1717
+ } else {
1718
+ if (i == 0) {
1719
+ parent.debug('web', 'handleResetAccountRequest: Unable to sent email.');
1720
+ req.session.loginmode = 3;
1721
+ req.session.messageid = 109; // Unable to sent email.
1722
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1723
+ }
1724
+ }
1725
+ }
1726
+ }
1727
+ }
1728
+ });
1729
+ }
1730
+ }
1731
+
1732
+ // Handle account email change and email verification request
1733
+ function handleCheckAccountEmailRequest(req, res, direct) {
1734
+ const domain = checkUserIpAddress(req, res);
1735
+ if (domain == null) { return; }
1736
+ if ((domain.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; }
1737
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1738
+ if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
1739
+
1740
+ // Always lowercase the email address
1741
+ if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
1742
+
1743
+ // Get the email from the body or session.
1744
+ var email = req.body.email;
1745
+ if ((email == null) || (email == '')) { email = req.session.temail; }
1746
+
1747
+ // Check if this request is for an allows email domain
1748
+ if ((domain.newaccountemaildomains != null) && Array.isArray(domain.newaccountemaildomains)) {
1749
+ var i = -1;
1750
+ if (typeof req.body.email == 'string') { i = req.body.email.indexOf('@'); }
1751
+ if (i == -1) {
1752
+ parent.debug('web', 'handleCreateAccountRequest: unable to create account (1)');
1753
+ req.session.loginmode = 7;
1754
+ req.session.messageid = 106; // Invalid email.
1755
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1756
+ return;
1757
+ }
1758
+ var emailok = false, emaildomain = req.body.email.substring(i + 1).toLowerCase();
1759
+ for (var i in domain.newaccountemaildomains) { if (emaildomain == domain.newaccountemaildomains[i].toLowerCase()) { emailok = true; } }
1760
+ if (emailok == false) {
1761
+ parent.debug('web', 'handleCreateAccountRequest: unable to create account (2)');
1762
+ req.session.loginmode = 7;
1763
+ req.session.messageid = 106; // Invalid email.
1764
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1765
+ return;
1766
+ }
1767
+ }
1768
+
1769
+ // Check the email string format
1770
+ if (!email || checkEmail(email) == false) {
1771
+ parent.debug('web', 'handleCheckAccountEmailRequest: Invalid email');
1772
+ req.session.loginmode = 7;
1773
+ req.session.messageid = 106; // Invalid email.
1774
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1775
+ } else {
1776
+ // Check is email already exists
1777
+ obj.db.GetUserWithVerifiedEmail(domain.id, email, function (err, docs) {
1778
+ if ((err != null) || ((docs.length > 0) && (docs.find(function (u) { return (u._id === req.session.cuserid); }) < 0))) {
1779
+ // Email already exitst
1780
+ req.session.messageid = 102; // Existing account with this email address.
1781
+ } else {
1782
+ // Update the user and notify of user email address change
1783
+ var user = obj.users[req.session.cuserid];
1784
+ if (user.email != email) {
1785
+ user.email = email;
1786
+ db.SetUser(user);
1787
+ var targets = ['*', 'server-users', user._id];
1788
+ if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
1789
+ var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Account changed: ' + user.name, domain: domain.id };
1790
+ 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.
1791
+ parent.DispatchEvent(targets, obj, event);
1792
+ }
1793
+
1794
+ // Send the verification email
1795
+ domain.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
1796
+
1797
+ // Send the response
1798
+ req.session.messageid = 2; // Email sent.
1799
+ }
1800
+ req.session.loginmode = 7;
1801
+ delete req.session.cuserid;
1802
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1803
+ });
1804
+ }
1805
+ }
1806
+
1807
+ // Called to process a web based email verification request
1808
+ function handleCheckMailRequest(req, res) {
1809
+ const domain = checkUserIpAddress(req, res);
1810
+ if (domain == null) { return; }
1811
+ if ((domain.auth == 'sspi') || (domain.auth == 'ldap') || (domain.mailserver == null)) { parent.debug('web', 'handleCheckMailRequest: failed checks.'); res.sendStatus(404); return; }
1812
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1813
+
1814
+ if (req.query.c != null) {
1815
+ var cookie = obj.parent.decodeCookie(req.query.c, domain.mailserver.mailCookieEncryptionKey, 30);
1816
+ if ((cookie != null) && (cookie.u != null) && (cookie.u.startsWith('user/')) && (cookie.e != null)) {
1817
+ var idsplit = cookie.u.split('/');
1818
+ if ((idsplit.length != 3) || (idsplit[1] != domain.id)) {
1819
+ parent.debug('web', 'handleCheckMailRequest: Invalid domain.');
1820
+ 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));
1821
+ } else {
1822
+ obj.db.Get(cookie.u, function (err, docs) {
1823
+ if (docs.length == 0) {
1824
+ parent.debug('web', 'handleCheckMailRequest: Invalid username.');
1825
+ 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));
1826
+ } else {
1827
+ var user = docs[0];
1828
+ if (user.email != cookie.e) {
1829
+ parent.debug('web', 'handleCheckMailRequest: Invalid e-mail.');
1830
+ 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));
1831
+ } else {
1832
+ if (cookie.a == 1) {
1833
+ // Account email verification
1834
+ if (user.emailVerified == true) {
1835
+ parent.debug('web', 'handleCheckMailRequest: email already verified.');
1836
+ 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));
1837
+ } else {
1838
+ obj.db.GetUserWithVerifiedEmail(domain.id, user.email, function (err, docs) {
1839
+ if ((docs.length > 0) && (docs.find(function (u) { return (u._id === user._id); }) < 0)) {
1840
+ parent.debug('web', 'handleCheckMailRequest: email already in use.');
1841
+ 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));
1842
+ } else {
1843
+ parent.debug('web', 'handleCheckMailRequest: email verification success.');
1844
+
1845
+ // Set the verified flag
1846
+ obj.users[user._id].emailVerified = true;
1847
+ user.emailVerified = true;
1848
+ obj.db.SetUser(user);
1849
+
1850
+ // Event the change
1851
+ 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 };
1852
+ 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.
1853
+ obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
1854
+
1855
+ // Send the confirmation page
1856
+ 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));
1857
+
1858
+ // Send a notification
1859
+ obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: 'Email verified', value: user.email, nolog: 1, id: Math.random() });
1860
+
1861
+ // Send to authlog
1862
+ if (obj.parent.authlog) { obj.parent.authLog('https', 'Verified email address ' + user.email + ' for user ' + user.name); }
1863
+ }
1864
+ });
1865
+ }
1866
+ } else if (cookie.a == 2) {
1867
+ // Account reset
1868
+ if (user.emailVerified != true) {
1869
+ parent.debug('web', 'handleCheckMailRequest: email not verified.');
1870
+ 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));
1871
+ } else {
1872
+ if (req.query.confirm == 1) {
1873
+ // Set a temporary password
1874
+ obj.crypto.randomBytes(16, function (err, buf) {
1875
+ var newpass = buf.toString('base64').split('=').join('').split('/').join('').split('+').join('');
1876
+ require('./pass').hash(newpass, function (err, salt, hash, tag) {
1877
+ if (err) throw err;
1878
+
1879
+ // Change the password
1880
+ var userinfo = obj.users[user._id];
1881
+ userinfo.salt = salt;
1882
+ userinfo.hash = hash;
1883
+ delete userinfo.passtype;
1884
+ userinfo.passchange = userinfo.access = Math.floor(Date.now() / 1000);
1885
+ delete userinfo.passhint;
1886
+ obj.db.SetUser(userinfo);
1887
+
1888
+ // Event the change
1889
+ 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 };
1890
+ 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.
1891
+ obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
1892
+
1893
+ // Send the new password
1894
+ 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));
1895
+ parent.debug('web', 'handleCheckMailRequest: send temporary password.');
1896
+
1897
+ // Send to authlog
1898
+ if (obj.parent.authlog) { obj.parent.authLog('https', 'Performed account reset for user ' + user.name); }
1899
+ }, 0);
1900
+ });
1901
+ } else {
1902
+ // Display a link for the user to confirm password reset
1903
+ // 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.
1904
+ 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));
1905
+ }
1906
+ }
1907
+ } else {
1908
+ 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));
1909
+ }
1910
+ }
1911
+ }
1912
+ });
1913
+ }
1914
+ } else {
1915
+ 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));
1916
+ }
1917
+ }
1918
+ }
1919
+
1920
+ // Called to process an agent invite GET/POST request
1921
+ function handleInviteRequest(req, res) {
1922
+ const domain = getDomain(req);
1923
+ if (domain == null) { parent.debug('web', 'handleInviteRequest: failed checks.'); res.sendStatus(404); return; }
1924
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1925
+ if ((req.body.inviteCode == null) || (req.body.inviteCode == '')) { render(req, res, getRenderPage('invite', req, domain), getRenderArgs({ messageid: 0 }, req, domain)); return; } // No invitation code
1926
+
1927
+ // Each for a device group that has this invite code.
1928
+ for (var i in obj.meshes) {
1929
+ if ((obj.meshes[i].domain == domain.id) && (obj.meshes[i].deleted == null) && (obj.meshes[i].invite != null) && (obj.meshes[i].invite.codes.indexOf(req.body.inviteCode) >= 0)) {
1930
+ // Send invitation link, valid for 1 minute.
1931
+ res.redirect(domain.url + 'agentinvite?c=' + parent.encodeCookie({ a: 4, mid: i, f: obj.meshes[i].invite.flags, ag: obj.meshes[i].invite.ag, expire: 1 }, parent.invitationLinkEncryptionKey) + (req.query.key ? ('&key=' + req.query.key) : '') + (req.query.hide ? ('&hide=' + req.query.hide) : ''));
1932
+ return;
1933
+ }
1934
+ }
1935
+
1936
+ render(req, res, getRenderPage('invite', req, domain), getRenderArgs({ messageid: 100 }, req, domain)); // Bad invitation code
1937
+ }
1938
+
1939
+ // Called to render the MSTSC (RDP) or SSH web page
1940
+ function handleMSTSCRequest(req, res, page) {
1941
+ const domain = getDomain(req);
1942
+ if (domain == null) { parent.debug('web', 'handleMSTSCRequest: failed checks.'); res.sendStatus(404); return; }
1943
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1944
+
1945
+ // Check if we are in maintenance mode
1946
+ if ((parent.config.settings.maintenancemode != null) && (req.query.loginscreen !== '1')) {
1947
+ 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));
1948
+ return;
1949
+ }
1950
+
1951
+ // Set features we want to send to this page
1952
+ var features = 0;
1953
+ if (domain.allowsavingdevicecredentials === false) { features |= 1; }
1954
+
1955
+ if (req.query.ws != null) {
1956
+ // This is a query with a websocket relay cookie, check that the cookie is valid and use it.
1957
+ var rcookie = parent.decodeCookie(req.query.ws, parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
1958
+ if ((rcookie != null) && (rcookie.domainid == domain.id) && (rcookie.nodeid != null) && (rcookie.tcpport != null)) {
1959
+
1960
+ // Fetch the node from the database
1961
+ obj.db.Get(rcookie.nodeid, function (err, nodes) {
1962
+ if ((err != null) || (nodes.length != 1)) { res.sendStatus(404); return; }
1963
+ const node = nodes[0];
1964
+
1965
+ // Check if we have RDP credentials for this device
1966
+ var serverCredentials = false;
1967
+ if (domain.allowsavingdevicecredentials !== false) {
1968
+ if (page == 'ssh') {
1969
+ serverCredentials = ((typeof node.rdp == 'object') && (typeof node.rdp.d == 'string') && (typeof node.rdp.u == 'string') && (typeof node.rdp.p == 'string'))
1970
+ } else {
1971
+ serverCredentials = ((typeof node.ssh == 'object') && (typeof node.ssh.u == 'string'))
1972
+ }
1973
+ }
1974
+
1975
+ // Render the page
1976
+ render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: req.query.ws, name: encodeURIComponent(req.query.name).replace(/'/g, '%27'), serverCredentials: serverCredentials, features: features }, req, domain));
1977
+ });
1978
+ return;
1979
+ }
1980
+ }
1981
+
1982
+ // Get the logged in user if present
1983
+ var user = null;
1984
+
1985
+ // If there is a login token, use that
1986
+ if (req.query.login != null) {
1987
+ var ucookie = parent.decodeCookie(req.query.login, parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
1988
+ if ((ucookie != null) && (ucookie.a === 3) && (typeof ucookie.u == 'string')) { user = obj.users[ucookie.u]; }
1989
+ }
1990
+
1991
+ // If no token, see if we have an active session
1992
+ if ((user == null) && (req.session.userid != null)) { user = obj.users[req.session.userid]; }
1993
+
1994
+ // If still no user, see if we have a default user
1995
+ if ((user == null) && (obj.args.user)) { user = obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]; }
1996
+
1997
+ // No user login, exit now
1998
+ if (user == null) { res.sendStatus(401); return; }
1999
+
2000
+ // Check the nodeid
2001
+ if (req.query.node != null) {
2002
+ var nodeidsplit = req.query.node.split('/');
2003
+ if (nodeidsplit.length == 1) {
2004
+ req.query.node = 'node/' + domain.id + '/' + nodeidsplit[0]; // Format the nodeid correctly
2005
+ } else if (nodeidsplit.length == 3) {
2006
+ if ((nodeidsplit[0] != 'node') || (nodeidsplit[1] != domain.id)) { req.query.node = null; } // Check the nodeid format
2007
+ } else {
2008
+ req.query.node = null; // Bad nodeid
2009
+ }
2010
+ }
2011
+
2012
+ // If there is no nodeid, exit now
2013
+ if (req.query.node == null) { render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: '', name: '', features: features }, req, domain)); return; }
2014
+
2015
+ // Fetch the node from the database
2016
+ obj.db.Get(req.query.node, function (err, nodes) {
2017
+ if ((err != null) || (nodes.length != 1)) { res.sendStatus(404); return; }
2018
+ const node = nodes[0];
2019
+
2020
+ // Check access rights, must have remote control rights
2021
+ if ((obj.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0) { res.sendStatus(401); return; }
2022
+
2023
+ // Figure out the target port
2024
+ var port = 0, serverCredentials = false;
2025
+ if (page == 'ssh') {
2026
+ // SSH port
2027
+ port = 22;
2028
+ if (typeof node.sshport == 'number') { port = node.sshport; }
2029
+
2030
+ // Check if we have SSH credentials for this device
2031
+ if (domain.allowsavingdevicecredentials !== false) { serverCredentials = ((typeof node.ssh == 'object') && (typeof node.ssh.u == 'string')); }
2032
+ } else {
2033
+ // RDP port
2034
+ port = 3389;
2035
+ if (typeof node.rdpport == 'number') { port = node.rdpport; }
2036
+
2037
+ // Check if we have RDP credentials for this device
2038
+ if (domain.allowsavingdevicecredentials !== false) { serverCredentials = ((typeof node.rdp == 'object') && (typeof node.rdp.d == 'string') && (typeof node.rdp.u == 'string') && (typeof node.rdp.p == 'string')); }
2039
+ }
2040
+ 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; } }
2041
+
2042
+ // Generate a cookie and respond
2043
+ var cookie = parent.encodeCookie({ userid: user._id, domainid: user.domain, nodeid: node._id, tcpport: port }, parent.loginCookieEncryptionKey);
2044
+ render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: cookie, name: encodeURIComponent(node.name).replace(/'/g, '%27'), serverCredentials: serverCredentials, features: features }, req, domain));
2045
+ });
2046
+ }
2047
+
2048
+ // Called to handle push-only requests
2049
+ function handleFirebasePushOnlyRelayRequest(req, res) {
2050
+ parent.debug('email', 'handleFirebasePushOnlyRelayRequest');
2051
+ if ((req.body == null) || (req.body.msg == null) || (obj.parent.firebase == null)) { res.sendStatus(404); return; }
2052
+ if (obj.parent.config.firebase.pushrelayserver == null) { res.sendStatus(404); return; }
2053
+ if ((typeof obj.parent.config.firebase.pushrelayserver == 'string') && (req.query.key != obj.parent.config.firebase.pushrelayserver)) { res.sendStatus(404); return; }
2054
+ var data = null;
2055
+ try { data = JSON.parse(req.body.msg) } catch (ex) { res.sendStatus(404); return; }
2056
+ if (typeof data != 'object') { res.sendStatus(404); return; }
2057
+ if (typeof data.pmt != 'string') { res.sendStatus(404); return; }
2058
+ if (typeof data.payload != 'object') { res.sendStatus(404); return; }
2059
+ if (typeof data.payload.notification != 'object') { res.sendStatus(404); return; }
2060
+ if (typeof data.payload.notification.title != 'string') { res.sendStatus(404); return; }
2061
+ if (typeof data.payload.notification.body != 'string') { res.sendStatus(404); return; }
2062
+ if (typeof data.options != 'object') { res.sendStatus(404); return; }
2063
+ if ((data.options.priority != 'Normal') && (data.options.priority != 'High')) { res.sendStatus(404); return; }
2064
+ if ((typeof data.options.timeToLive != 'number') || (data.options.timeToLive < 1)) { res.sendStatus(404); return; }
2065
+ parent.debug('email', 'handleFirebasePushOnlyRelayRequest - ok');
2066
+ obj.parent.firebase.sendToDevice({ pmt: data.pmt }, data.payload, data.options, function (id, err, errdesc) {
2067
+ if (err == null) { res.sendStatus(200); } else { res.sendStatus(500); }
2068
+ });
2069
+ }
2070
+
2071
+ // Called to handle two-way push notification relay request
2072
+ function handleFirebaseRelayRequest(ws, req) {
2073
+ parent.debug('email', 'handleFirebaseRelayRequest');
2074
+ if (obj.parent.firebase == null) { try { ws.close(); } catch (e) { } return; }
2075
+ if (obj.parent.firebase.setupRelay == null) { try { ws.close(); } catch (e) { } return; }
2076
+ if (obj.parent.config.firebase.relayserver == null) { try { ws.close(); } catch (e) { } return; }
2077
+ if ((typeof obj.parent.config.firebase.relayserver == 'string') && (req.query.key != obj.parent.config.firebase.relayserver)) { res.sendStatus(404); try { ws.close(); } catch (e) { } return; }
2078
+ obj.parent.firebase.setupRelay(ws);
2079
+ }
2080
+
2081
+ // Called to process an agent invite request
2082
+ function handleAgentInviteRequest(req, res) {
2083
+ const domain = getDomain(req);
2084
+ if ((domain == null) || ((req.query.m == null) && (req.query.c == null))) { parent.debug('web', 'handleAgentInviteRequest: failed checks.'); res.sendStatus(404); return; }
2085
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2086
+
2087
+ if (req.query.c != null) {
2088
+ // A cookie is specified in the query string, use that
2089
+ var cookie = obj.parent.decodeCookie(req.query.c, obj.parent.invitationLinkEncryptionKey);
2090
+ if (cookie == null) { res.sendStatus(404); return; }
2091
+ var mesh = obj.meshes[cookie.mid];
2092
+ if (mesh == null) { res.sendStatus(404); return; }
2093
+ var installflags = cookie.f;
2094
+ if (typeof installflags != 'number') { installflags = 0; }
2095
+ var showagents = cookie.ag;
2096
+ if (typeof showagents != 'number') { showagents = 0; }
2097
+ parent.debug('web', 'handleAgentInviteRequest using cookie.');
2098
+
2099
+ // Build the mobile agent URL, this is used to connect mobile devices
2100
+ var agentServerName = obj.getWebServerName(domain);
2101
+ if (typeof obj.args.agentaliasdns == 'string') { agentServerName = obj.args.agentaliasdns; }
2102
+ var xdomain = (domain.dns == null) ? domain.id : '';
2103
+ var agentHttpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
2104
+ if (obj.args.agentport != null) { agentHttpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
2105
+ if (obj.args.agentaliasport != null) { agentHttpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
2106
+ var magenturl = 'mc://' + agentServerName + ((agentHttpsPort != 443) ? (':' + agentHttpsPort) : '') + ((xdomain != '') ? ('/' + xdomain) : '') + ',' + obj.agentCertificateHashBase64 + ',' + mesh._id.split('/')[2];
2107
+
2108
+ var meshcookie = parent.encodeCookie({ m: mesh._id.split('/')[2] }, parent.invitationLinkEncryptionKey);
2109
+ 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, showagents: showagents, magenturl: magenturl }, req, domain));
2110
+ } else if (req.query.m != null) {
2111
+ // The MeshId is specified in the query string, use that
2112
+ var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.m.toLowerCase()];
2113
+ if (mesh == null) { res.sendStatus(404); return; }
2114
+ var installflags = 0;
2115
+ if (req.query.f) { installflags = parseInt(req.query.f); }
2116
+ if (typeof installflags != 'number') { installflags = 0; }
2117
+ var showagents = 0;
2118
+ if (req.query.f) { showagents = parseInt(req.query.ag); }
2119
+ if (typeof showagents != 'number') { showagents = 0; }
2120
+ parent.debug('web', 'handleAgentInviteRequest using meshid.');
2121
+
2122
+ // Build the mobile agent URL, this is used to connect mobile devices
2123
+ var agentServerName = obj.getWebServerName(domain);
2124
+ if (typeof obj.args.agentaliasdns == 'string') { agentServerName = obj.args.agentaliasdns; }
2125
+ var xdomain = (domain.dns == null) ? domain.id : '';
2126
+ var agentHttpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
2127
+ if (obj.args.agentport != null) { agentHttpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
2128
+ if (obj.args.agentaliasport != null) { agentHttpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
2129
+ var magenturl = 'mc://' + agentServerName + ((agentHttpsPort != 443) ? (':' + agentHttpsPort) : '') + ((xdomain != '') ? ('/' + xdomain) : '') + ',' + obj.agentCertificateHashBase64 + ',' + mesh._id.split('/')[2];
2130
+
2131
+ var meshcookie = parent.encodeCookie({ m: mesh._id.split('/')[2] }, parent.invitationLinkEncryptionKey);
2132
+ 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, showagents: showagents, magenturl: magenturl }, req, domain));
2133
+ }
2134
+ }
2135
+
2136
+ // Called to process an agent invite request
2137
+ function handleUserImageRequest(req, res) {
2138
+ const domain = getDomain(req);
2139
+ if (domain == null) { parent.debug('web', 'handleUserImageRequest: failed checks.'); res.sendStatus(404); return; }
2140
+ if ((req.session == null) || (req.session.userid == null)) { parent.debug('web', 'handleUserImageRequest: failed checks 2.'); res.sendStatus(404); return; }
2141
+ var imageUserId = req.session.userid;
2142
+ if ((req.query.id != null)) {
2143
+ var user = obj.users[req.session.userid];
2144
+ if ((user == null) || (user.siteadmin == null) && ((user.siteadmin & 2) == 0)) { res.sendStatus(404); return; }
2145
+ imageUserId = 'user/' + domain.id + '/' + req.query.id;
2146
+ }
2147
+ obj.db.Get('im' + imageUserId, function (err, docs) {
2148
+ if ((err != null) || (docs == null) || (docs.length != 1) || (typeof docs[0].image != 'string')) { res.sendStatus(404); return; }
2149
+ var imagebase64 = docs[0].image;
2150
+ if (imagebase64.startsWith('data:image/png;base64,')) {
2151
+ res.set('Content-Type', 'image/png');
2152
+ res.set({ 'Cache-Control': 'no-store' });
2153
+ res.send(Buffer.from(imagebase64.substring(22), 'base64'));
2154
+ } else if (imagebase64.startsWith('data:image/jpeg;base64,')) {
2155
+ res.set('Content-Type', 'image/jpeg');
2156
+ res.set({ 'Cache-Control': 'no-store' });
2157
+ res.send(Buffer.from(imagebase64.substring(23), 'base64'));
2158
+ } else {
2159
+ res.sendStatus(404);
2160
+ }
2161
+ });
2162
+ }
2163
+
2164
+ function handleDeleteAccountRequest(req, res, direct) {
2165
+ parent.debug('web', 'handleDeleteAccountRequest()');
2166
+ const domain = checkUserIpAddress(req, res);
2167
+ if (domain == null) { return; }
2168
+ if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handleDeleteAccountRequest: failed checks.'); res.sendStatus(404); return; }
2169
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2170
+ if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
2171
+
2172
+ var user = null;
2173
+ if (req.body.authcookie) {
2174
+ // If a authentication cookie is provided, decode it here
2175
+ var loginCookie = obj.parent.decodeCookie(req.body.authcookie, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
2176
+ if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { user = obj.users[loginCookie.userid]; }
2177
+ } else {
2178
+ // Check if the user is logged and we have all required parameters
2179
+ if (!req.session || !req.session.userid || !req.body.apassword1 || (req.body.apassword1 != req.body.apassword2) || (req.session.userid.split('/')[1] != domain.id)) {
2180
+ parent.debug('web', 'handleDeleteAccountRequest: required parameters not present.');
2181
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2182
+ return;
2183
+ } else {
2184
+ user = obj.users[req.session.userid];
2185
+ }
2186
+ }
2187
+ if (!user) { parent.debug('web', 'handleDeleteAccountRequest: user not found.'); res.sendStatus(404); return; }
2188
+ if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) { parent.debug('web', 'handleDeleteAccountRequest: account settings locked.'); res.sendStatus(404); return; }
2189
+
2190
+ // Check if the password is correct
2191
+ obj.authenticate(user._id.split('/')[2], req.body.apassword1, domain, function (err, userid, passhint, loginOptions) {
2192
+ var deluser = obj.users[userid];
2193
+ if ((userid != null) && (deluser != null)) {
2194
+ // Remove all links to this user
2195
+ if (deluser.links != null) {
2196
+ for (var i in deluser.links) {
2197
+ if (i.startsWith('mesh/')) {
2198
+ // Get the device group
2199
+ var mesh = obj.meshes[i];
2200
+ if (mesh) {
2201
+ // Remove user from the mesh
2202
+ if (mesh.links[deluser._id] != null) { delete mesh.links[deluser._id]; parent.db.Set(mesh); }
2203
+
2204
+ // Notify mesh change
2205
+ var change = 'Removed user ' + deluser.name + ' from group ' + mesh.name;
2206
+ 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 };
2207
+ 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.
2208
+ parent.DispatchEvent(['*', mesh._id, deluser._id, user._id], obj, event);
2209
+ }
2210
+ } else if (i.startsWith('node/')) {
2211
+ // Get the node and the rights for this node
2212
+ obj.GetNodeWithRights(domain, deluser, i, function (node, rights, visible) {
2213
+ if ((node == null) || (node.links == null) || (node.links[deluser._id] == null)) return;
2214
+
2215
+ // Remove the link and save the node to the database
2216
+ delete node.links[deluser._id];
2217
+ if (Object.keys(node.links).length == 0) { delete node.links; }
2218
+ db.Set(obj.cleanDevice(node));
2219
+
2220
+ // Event the node change
2221
+ 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) }
2222
+ 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.
2223
+ parent.DispatchEvent(['*', node.meshid, node._id], obj, event);
2224
+ });
2225
+ } else if (i.startsWith('ugrp/')) {
2226
+ // Get the device group
2227
+ var ugroup = obj.userGroups[i];
2228
+ if (ugroup) {
2229
+ // Remove user from the user group
2230
+ if (ugroup.links[deluser._id] != null) { delete ugroup.links[deluser._id]; parent.db.Set(ugroup); }
2231
+
2232
+ // Notify user group change
2233
+ var change = 'Removed user ' + deluser.name + ' from user group ' + ugroup.name;
2234
+ 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 };
2235
+ 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.
2236
+ parent.DispatchEvent(['*', ugroup._id, user._id, deluser._id], obj, event);
2237
+ }
2238
+ }
2239
+ }
2240
+ }
2241
+
2242
+ obj.db.Remove('ws' + deluser._id); // Remove user web state
2243
+ obj.db.Remove('nt' + deluser._id); // Remove notes for this user
2244
+ obj.db.Remove('ntp' + deluser._id); // Remove personal notes for this user
2245
+ obj.db.Remove('im' + deluser._id); // Remove image for this user
2246
+
2247
+ // Delete any login tokens
2248
+ parent.db.GetAllTypeNodeFiltered(['logintoken-' + deluser._id], domain.id, 'logintoken', null, function (err, docs) {
2249
+ if ((err == null) && (docs != null)) { for (var i = 0; i < docs.length; i++) { parent.db.Remove(docs[i]._id, function () { }); } }
2250
+ });
2251
+
2252
+ // Delete all files on the server for this account
2253
+ try {
2254
+ var deluserpath = obj.getServerRootFilePath(deluser);
2255
+ if (deluserpath != null) { obj.deleteFolderRec(deluserpath); }
2256
+ } catch (e) { }
2257
+
2258
+ // Remove the user
2259
+ obj.db.Remove(deluser._id);
2260
+ delete obj.users[deluser._id];
2261
+ req.session = null;
2262
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2263
+ obj.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', userid: deluser._id, username: deluser.name, action: 'accountremove', msg: 'Account removed', domain: domain.id });
2264
+ parent.debug('web', 'handleDeleteAccountRequest: removed user.');
2265
+ } else {
2266
+ parent.debug('web', 'handleDeleteAccountRequest: auth failed.');
2267
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2268
+ }
2269
+ });
2270
+ }
2271
+
2272
+ // Check a user's password
2273
+ obj.checkUserPassword = function (domain, user, password, func) {
2274
+ // Check the old password
2275
+ if (user.passtype != null) {
2276
+ // IIS default clear or weak password hashing (SHA-1)
2277
+ require('./pass').iishash(user.passtype, password, user.salt, function (err, hash) {
2278
+ if (err) { parent.debug('web', 'checkUserPassword: SHA-1 fail.'); return func(false); }
2279
+ if (hash == user.hash) {
2280
+ if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { parent.debug('web', 'checkUserPassword: SHA-1 locked.'); return func(false); } // Account is locked
2281
+ parent.debug('web', 'checkUserPassword: SHA-1 ok.');
2282
+ return func(true); // Allow password change
2283
+ }
2284
+ func(false);
2285
+ });
2286
+ } else {
2287
+ // Default strong password hashing (pbkdf2 SHA384)
2288
+ require('./pass').hash(password, user.salt, function (err, hash, tag) {
2289
+ if (err) { parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 fail.'); return func(false); }
2290
+ if (hash == user.hash) {
2291
+ if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 locked.'); return func(false); } // Account is locked
2292
+ parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 ok.');
2293
+ return func(true); // Allow password change
2294
+ }
2295
+ func(false);
2296
+ }, 0);
2297
+ }
2298
+ }
2299
+
2300
+ // Check a user's old passwords
2301
+ // Callback: 0=OK, 1=OldPass, 2=CommonPass
2302
+ obj.checkOldUserPasswords = function (domain, user, password, func) {
2303
+ // Check how many old passwords we need to check
2304
+ if ((domain.passwordrequirements != null) && (typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
2305
+ if (user.oldpasswords != null) {
2306
+ const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
2307
+ if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
2308
+ }
2309
+ } else {
2310
+ delete user.oldpasswords;
2311
+ }
2312
+
2313
+ // If there is no old passwords, exit now.
2314
+ var oldPassCount = 1;
2315
+ if (user.oldpasswords != null) { oldPassCount += user.oldpasswords.length; }
2316
+ var oldPassCheckState = { response: 0, count: oldPassCount, user: user, func: func };
2317
+
2318
+ // Test against common passwords if this feature is enabled
2319
+ // Example of common passwords: 123456789, password123
2320
+ if ((domain.passwordrequirements != null) && (domain.passwordrequirements.bancommonpasswords == true)) {
2321
+ oldPassCheckState.count++;
2322
+ require('wildleek')(password).then(function (wild) {
2323
+ if (wild == true) { oldPassCheckState.response = 2; }
2324
+ if (--oldPassCheckState.count == 0) { oldPassCheckState.func(oldPassCheckState.response); }
2325
+ });
2326
+ }
2327
+
2328
+ // Try current password
2329
+ require('./pass').hash(password, user.salt, function oldPassCheck(err, hash, tag) {
2330
+ if ((err == null) && (hash == tag.user.hash)) { tag.response = 1; }
2331
+ if (--tag.count == 0) { tag.func(tag.response); }
2332
+ }, oldPassCheckState);
2333
+
2334
+ // Try each old password
2335
+ if (user.oldpasswords != null) {
2336
+ for (var i in user.oldpasswords) {
2337
+ const oldpassword = user.oldpasswords[i];
2338
+ // Default strong password hashing (pbkdf2 SHA384)
2339
+ require('./pass').hash(password, oldpassword.salt, function oldPassCheck(err, hash, tag) {
2340
+ if ((err == null) && (hash == tag.oldPassword.hash)) { tag.state.response = 1; }
2341
+ if (--tag.state.count == 0) { tag.state.func(tag.state.response); }
2342
+ }, { oldPassword: oldpassword, state: oldPassCheckState });
2343
+ }
2344
+ }
2345
+ }
2346
+
2347
+ // Handle password changes
2348
+ function handlePasswordChangeRequest(req, res, direct) {
2349
+ const domain = checkUserIpAddress(req, res);
2350
+ if (domain == null) { return; }
2351
+ if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handlePasswordChangeRequest: failed checks (1).'); res.sendStatus(404); return; }
2352
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2353
+ if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
2354
+
2355
+ // Check if the user is logged and we have all required parameters
2356
+ if (!req.session || !req.session.userid || !req.body.apassword0 || !req.body.apassword1 || (req.body.apassword1 != req.body.apassword2) || (req.session.userid.split('/')[1] != domain.id)) {
2357
+ parent.debug('web', 'handlePasswordChangeRequest: failed checks (2).');
2358
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2359
+ return;
2360
+ }
2361
+
2362
+ // Get the current user
2363
+ var user = obj.users[req.session.userid];
2364
+ if (!user) {
2365
+ parent.debug('web', 'handlePasswordChangeRequest: user not found.');
2366
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2367
+ return;
2368
+ }
2369
+
2370
+ // Check account settings locked
2371
+ if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) {
2372
+ parent.debug('web', 'handlePasswordChangeRequest: account settings locked.');
2373
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2374
+ return;
2375
+ }
2376
+
2377
+ // Check old password
2378
+ obj.checkUserPassword(domain, user, req.body.apassword1, function (result) {
2379
+ if (result == true) {
2380
+ // Check if the new password is allowed, only do this if this feature is enabled.
2381
+ parent.checkOldUserPasswords(domain, user, command.newpass, function (result) {
2382
+ if (result == 1) {
2383
+ parent.debug('web', 'handlePasswordChangeRequest: old password reuse attempt.');
2384
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2385
+ } else if (result == 2) {
2386
+ parent.debug('web', 'handlePasswordChangeRequest: commonly used password use attempt.');
2387
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2388
+ } else {
2389
+ // Update the password
2390
+ require('./pass').hash(req.body.apassword1, function (err, salt, hash, tag) {
2391
+ const nowSeconds = Math.floor(Date.now() / 1000);
2392
+ if (err) { parent.debug('web', 'handlePasswordChangeRequest: hash error.'); throw err; }
2393
+ if (domain.passwordrequirements != null) {
2394
+ // Save password hint if this feature is enabled
2395
+ 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; }
2396
+
2397
+ // Save previous password if this feature is enabled
2398
+ if ((typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
2399
+ if (user.oldpasswords == null) { user.oldpasswords = []; }
2400
+ user.oldpasswords.push({ salt: user.salt, hash: user.hash, start: user.passchange, end: nowSeconds });
2401
+ const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
2402
+ if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
2403
+ }
2404
+ }
2405
+ user.salt = salt;
2406
+ user.hash = hash;
2407
+ user.passchange = user.access = nowSeconds;
2408
+ delete user.passtype;
2409
+
2410
+ obj.db.SetUser(user);
2411
+ req.session.viewmode = 2;
2412
+ if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2413
+ 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 });
2414
+ }, 0);
2415
+ }
2416
+ });
2417
+ }
2418
+ });
2419
+ }
2420
+
2421
+ // Called when a strategy login occured
2422
+ // This is called after a succesful Oauth to Twitter, Google, GitHub...
2423
+ function handleStrategyLogin(req, res) {
2424
+ const domain = checkUserIpAddress(req, res);
2425
+ if (domain == null) { return; }
2426
+ parent.debug('web', 'handleStrategyLogin: ' + JSON.stringify(req.user));
2427
+ if ((req.user != null) && (req.user.sid != null)) {
2428
+ const userid = 'user/' + domain.id + '/' + req.user.sid;
2429
+ var user = obj.users[userid];
2430
+ if (user == null) {
2431
+ var newAccountAllowed = false;
2432
+ var newAccountRealms = null;
2433
+
2434
+ if (domain.newaccounts === true) { newAccountAllowed = true; }
2435
+ if (obj.common.validateStrArray(domain.newaccountrealms)) { newAccountRealms = domain.newaccountrealms; }
2436
+
2437
+ if ((domain.authstrategies != null) && (domain.authstrategies[req.user.strategy] != null)) {
2438
+ if (domain.authstrategies[req.user.strategy].newaccounts === true) { newAccountAllowed = true; }
2439
+ if (obj.common.validateStrArray(domain.authstrategies[req.user.strategy].newaccountrealms)) { newAccountRealms = domain.authstrategies[req.user.strategy].newaccountrealms; }
2440
+ }
2441
+
2442
+ if (newAccountAllowed === true) {
2443
+ // Create the user
2444
+ parent.debug('web', 'handleStrategyLogin: creating new user: ' + userid);
2445
+ user = { type: 'user', _id: userid, name: req.user.name, email: req.user.email, creation: Math.floor(Date.now() / 1000), login: Math.floor(Date.now() / 1000), access: Math.floor(Date.now() / 1000), domain: domain.id };
2446
+ if (req.user.email != null) { user.email = req.user.email; user.emailVerified = true; }
2447
+ if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; } // New accounts automatically assigned server rights.
2448
+ 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.
2449
+ if (newAccountRealms) { user.groups = newAccountRealms; } // New accounts automatically part of some groups (Realms).
2450
+ obj.users[userid] = user;
2451
+
2452
+ // Auto-join any user groups
2453
+ var newaccountsusergroups = null;
2454
+ if (typeof domain.newaccountsusergroups == 'object') { newaccountsusergroups = domain.newaccountsusergroups; }
2455
+ if (typeof domain.authstrategies[req.user.strategy].newaccountsusergroups == 'object') { newaccountsusergroups = domain.authstrategies[req.user.strategy].newaccountsusergroups; }
2456
+ if (newaccountsusergroups) {
2457
+ for (var i in newaccountsusergroups) {
2458
+ var ugrpid = newaccountsusergroups[i];
2459
+ if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
2460
+ var ugroup = obj.userGroups[ugrpid];
2461
+ if (ugroup != null) {
2462
+ // Add group to the user
2463
+ if (user.links == null) { user.links = {}; }
2464
+ user.links[ugroup._id] = { rights: 1 };
2465
+
2466
+ // Add user to the group
2467
+ ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
2468
+ db.Set(ugroup);
2469
+
2470
+ // Notify user group change
2471
+ 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 };
2472
+ 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.
2473
+ parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
2474
+ }
2475
+ }
2476
+ }
2477
+
2478
+ // Save the user
2479
+ obj.db.SetUser(user);
2480
+
2481
+ // Event user creation
2482
+ var targets = ['*', 'server-users'];
2483
+ 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 };
2484
+ 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.
2485
+ parent.DispatchEvent(targets, obj, event);
2486
+
2487
+ req.session.userid = userid;
2488
+ setSessionRandom(req);
2489
+
2490
+ // Notify account login using SSO
2491
+ var targets = ['*', 'server-users', user._id];
2492
+ if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
2493
+ const ua = getUserAgentInfo(req);
2494
+ const loginEvent = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'login', msgid: 107, msgArgs: [req.clientIp, ua.browserStr, ua.osStr], msg: 'Account login', domain: domain.id, ip: req.clientIp, userAgent: req.headers['user-agent'], twoFactorType: 'sso' };
2495
+ obj.parent.DispatchEvent(targets, obj, loginEvent);
2496
+ } else {
2497
+ // New users not allowed
2498
+ parent.debug('web', 'handleStrategyLogin: Can\'t create new accounts');
2499
+ req.session.loginmode = 1;
2500
+ req.session.messageid = 100; // Unable to create account.
2501
+ res.redirect(domain.url + getQueryPortion(req));
2502
+ return;
2503
+ }
2504
+ } else {
2505
+ // Login success
2506
+ var userChange = false;
2507
+ if ((req.user.name != null) && (req.user.name != user.name)) { user.name = req.user.name; userChange = true; }
2508
+ if ((req.user.email != null) && (req.user.email != user.email)) { user.email = req.user.email; user.emailVerified = true; userChange = true; }
2509
+ if (userChange) {
2510
+ obj.db.SetUser(user);
2511
+
2512
+ // Event user change
2513
+ var targets = ['*', 'server-users'];
2514
+ var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Account changed', domain: domain.id };
2515
+ 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.
2516
+ parent.DispatchEvent(targets, obj, event);
2517
+ }
2518
+ parent.debug('web', 'handleStrategyLogin: succesful login: ' + userid);
2519
+ req.session.userid = userid;
2520
+ setSessionRandom(req);
2521
+
2522
+ // Notify account login using SSO
2523
+ var targets = ['*', 'server-users', user._id];
2524
+ if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
2525
+ const ua = getUserAgentInfo(req);
2526
+ const loginEvent = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'login', msgid: 107, msgArgs: [req.clientIp, ua.browserStr, ua.osStr], msg: 'Account login', domain: domain.id, ip: req.clientIp, userAgent: req.headers['user-agent'], twoFactorType: 'sso' };
2527
+ obj.parent.DispatchEvent(targets, obj, loginEvent);
2528
+ }
2529
+ }
2530
+ //res.redirect(domain.url); // This does not handle cookie correctly.
2531
+ res.set('Content-Type', 'text/html');
2532
+ res.end('<html><head><meta http-equiv="refresh" content=0;url="' + domain.url + '"></head><body></body></html>');
2533
+ }
2534
+
2535
+ // Indicates that any request to "/" should render "default" or "login" depending on login state
2536
+ function handleRootRequest(req, res, direct) {
2537
+ const domain = checkUserIpAddress(req, res);
2538
+ if (domain == null) { return; }
2539
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2540
+ if (!obj.args) { parent.debug('web', 'handleRootRequest: no obj.args.'); res.sendStatus(500); return; }
2541
+
2542
+ // If the session is expired, clear it.
2543
+ if ((req.session != null) && (typeof req.session.expire == 'number') && ((req.session.expire - Date.now()) <= 0)) { for (var i in req.session) { delete req.session[i]; } }
2544
+
2545
+ // Check if we are in maintenance mode
2546
+ if ((parent.config.settings.maintenancemode != null) && (req.query.loginscreen !== '1')) {
2547
+ parent.debug('web', 'handleLoginRequest: Server under maintenance.');
2548
+ 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));
2549
+ return;
2550
+ }
2551
+
2552
+ // If set and there is no user logged in, redirect the root page. Make sure not to redirect if /login is used
2553
+ if ((typeof domain.unknownuserrootredirect == 'string') && ((req.session == null) || (req.session.userid == null))) {
2554
+ var q = require('url').parse(req.url, true);
2555
+ if (!q.pathname.endsWith('/login')) { res.redirect(domain.unknownuserrootredirect + getQueryPortion(req)); return; }
2556
+ }
2557
+
2558
+ if ((domain.sspi != null) && ((req.query.login == null) || (obj.parent.loginCookieEncryptionKey == null))) {
2559
+ // Login using SSPI
2560
+ domain.sspi.authenticate(req, res, function (err) {
2561
+ if ((err != null) || (req.connection.user == null)) {
2562
+ if (obj.parent.authlog) { obj.parent.authLog('https', 'Failed SSPI-auth for ' + req.connection.user + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); }
2563
+ parent.debug('web', 'handleRootRequest: SSPI auth required.');
2564
+ res.end('Authentication Required...');
2565
+ } else {
2566
+ if (obj.parent.authlog) { obj.parent.authLog('https', 'Accepted SSPI-auth for ' + req.connection.user + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); }
2567
+ parent.debug('web', 'handleRootRequest: SSPI auth ok.');
2568
+ handleRootRequestEx(req, res, domain, direct);
2569
+ }
2570
+ });
2571
+ } else if (req.query.user && req.query.pass) {
2572
+ // User credentials are being passed in the URL. WARNING: Putting credentials in a URL is bad security... but people are requesting this option.
2573
+ obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid, passhint, loginOptions) {
2574
+ if (obj.parent.authlog) { obj.parent.authLog('https', 'Accepted password for ' + userid + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); }
2575
+ parent.debug('web', 'handleRootRequest: user/pass in URL auth ok.');
2576
+ req.session.userid = userid;
2577
+ delete req.session.currentNode;
2578
+ req.session.ip = req.clientIp; // Bind this session to the IP address of the request
2579
+ setSessionRandom(req);
2580
+ handleRootRequestEx(req, res, domain, direct);
2581
+ });
2582
+ } else if ((req.session != null) && (typeof req.session.loginToken == 'string')) {
2583
+ // Check if the loginToken is still valid
2584
+ obj.db.Get('logintoken-' + req.session.loginToken, function (err, docs) {
2585
+ if ((err != null) || (docs == null) || (docs.length != 1) || (docs[0].tokenUser != req.session.loginToken)) { for (var i in req.session) { delete req.session[i]; } }
2586
+ handleRootRequestEx(req, res, domain, direct); // Login using a different system
2587
+ });
2588
+ } else {
2589
+ // Login using a different system
2590
+ handleRootRequestEx(req, res, domain, direct);
2591
+ }
2592
+ }
2593
+
2594
+ function handleRootRequestEx(req, res, domain, direct) {
2595
+ var nologout = false, user = null;
2596
+ res.set({ 'Cache-Control': 'no-store' });
2597
+
2598
+ // Check if we have an incomplete domain name in the path
2599
+ if ((domain.id != '') && (domain.dns == null) && (req.url.split('/').length == 2)) {
2600
+ parent.debug('web', 'handleRootRequestEx: incomplete domain name in the path.');
2601
+ res.redirect(domain.url + getQueryPortion(req)); // BAD***
2602
+ return;
2603
+ }
2604
+
2605
+ if (obj.args.nousers == true) {
2606
+ // If in single user mode, setup things here.
2607
+ delete req.session.loginmode;
2608
+ req.session.userid = 'user/' + domain.id + '/~';
2609
+ delete req.session.currentNode;
2610
+ req.session.ip = req.clientIp; // Bind this session to the IP address of the request
2611
+ setSessionRandom(req);
2612
+ if (obj.users[req.session.userid] == null) {
2613
+ // Create the dummy user ~ with impossible password
2614
+ parent.debug('web', 'handleRootRequestEx: created dummy user in nouser mode.');
2615
+ obj.users[req.session.userid] = { type: 'user', _id: req.session.userid, name: '~', email: '~', domain: domain.id, siteadmin: 4294967295 };
2616
+ obj.db.SetUser(obj.users[req.session.userid]);
2617
+ }
2618
+ } else if (obj.args.user && obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]) {
2619
+ // If a default user is active, setup the session here.
2620
+ parent.debug('web', 'handleRootRequestEx: auth using default user.');
2621
+ delete req.session.loginmode;
2622
+ req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase();
2623
+ delete req.session.currentNode;
2624
+ req.session.ip = req.clientIp; // Bind this session to the IP address of the request
2625
+ setSessionRandom(req);
2626
+ } else if (req.query.login && (obj.parent.loginCookieEncryptionKey != null)) {
2627
+ var loginCookie = obj.parent.decodeCookie(req.query.login, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
2628
+ //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.
2629
+ if ((loginCookie != null) && (loginCookie.a == 3) && (loginCookie.u != null) && (loginCookie.u.split('/')[1] == domain.id)) {
2630
+ // If a login cookie was provided, setup the session here.
2631
+ parent.debug('web', 'handleRootRequestEx: cookie auth ok.');
2632
+ delete req.session.loginmode;
2633
+ req.session.userid = loginCookie.u;
2634
+ delete req.session.currentNode;
2635
+ req.session.ip = req.clientIp; // Bind this session to the IP address of the request
2636
+ setSessionRandom(req);
2637
+ } else {
2638
+ parent.debug('web', 'handleRootRequestEx: cookie auth failed.');
2639
+ }
2640
+ } else if (domain.sspi != null) {
2641
+ // SSPI login (Windows only)
2642
+ //console.log(req.connection.user, req.connection.userSid);
2643
+ if ((req.connection.user == null) || (req.connection.userSid == null)) {
2644
+ parent.debug('web', 'handleRootRequestEx: SSPI no user auth.');
2645
+ res.sendStatus(404); return;
2646
+ } else {
2647
+ nologout = true;
2648
+ req.session.userid = 'user/' + domain.id + '/' + req.connection.user.toLowerCase();
2649
+ req.session.usersid = req.connection.userSid;
2650
+ req.session.usersGroups = req.connection.userGroups;
2651
+ delete req.session.currentNode;
2652
+ req.session.ip = req.clientIp; // Bind this session to the IP address of the request
2653
+ setSessionRandom(req);
2654
+
2655
+ // Check if this user exists, create it if not.
2656
+ user = obj.users[req.session.userid];
2657
+ if ((user == null) || (user.sid != req.session.usersid)) {
2658
+ // Create the domain user
2659
+ 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), access: Math.floor(Date.now() / 1000) };
2660
+ if (domain.newaccountsrights) { user2.siteadmin = domain.newaccountsrights; }
2661
+ if (obj.common.validateStrArray(domain.newaccountrealms)) { user2.groups = domain.newaccountrealms; }
2662
+ for (var i in obj.users) { if (obj.users[i].domain == domain.id) { usercount++; } }
2663
+ if (usercount == 0) { user2.siteadmin = 4294967295; } // If this is the first user, give the account site admin.
2664
+
2665
+ // Auto-join any user groups
2666
+ if (typeof domain.newaccountsusergroups == 'object') {
2667
+ for (var i in domain.newaccountsusergroups) {
2668
+ var ugrpid = domain.newaccountsusergroups[i];
2669
+ if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
2670
+ var ugroup = obj.userGroups[ugrpid];
2671
+ if (ugroup != null) {
2672
+ // Add group to the user
2673
+ if (user2.links == null) { user2.links = {}; }
2674
+ user2.links[ugroup._id] = { rights: 1 };
2675
+
2676
+ // Add user to the group
2677
+ ugroup.links[user2._id] = { userid: user2._id, name: user2.name, rights: 1 };
2678
+ db.Set(ugroup);
2679
+
2680
+ // Notify user group change
2681
+ 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 };
2682
+ 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.
2683
+ parent.DispatchEvent(['*', ugroup._id, user2._id], obj, event);
2684
+ }
2685
+ }
2686
+ }
2687
+
2688
+ obj.users[req.session.userid] = user2;
2689
+ obj.db.SetUser(user2);
2690
+ 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 };
2691
+ 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.
2692
+ obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
2693
+ parent.debug('web', 'handleRootRequestEx: SSPI new domain user.');
2694
+ }
2695
+ }
2696
+ }
2697
+
2698
+ // Figure out the minimal password requirement
2699
+ var passRequirements = null;
2700
+ if (domain.passwordrequirements != null) {
2701
+ if (domain.passrequirementstr == null) {
2702
+ var passRequirements = {};
2703
+ if (typeof domain.passwordrequirements.min == 'number') { passRequirements.min = domain.passwordrequirements.min; }
2704
+ if (typeof domain.passwordrequirements.max == 'number') { passRequirements.max = domain.passwordrequirements.max; }
2705
+ if (typeof domain.passwordrequirements.upper == 'number') { passRequirements.upper = domain.passwordrequirements.upper; }
2706
+ if (typeof domain.passwordrequirements.lower == 'number') { passRequirements.lower = domain.passwordrequirements.lower; }
2707
+ if (typeof domain.passwordrequirements.numeric == 'number') { passRequirements.numeric = domain.passwordrequirements.numeric; }
2708
+ if (typeof domain.passwordrequirements.nonalpha == 'number') { passRequirements.nonalpha = domain.passwordrequirements.nonalpha; }
2709
+ domain.passwordrequirementsstr = encodeURIComponent(JSON.stringify(passRequirements));
2710
+ }
2711
+ passRequirements = domain.passwordrequirementsstr;
2712
+ }
2713
+
2714
+ // If a user exists and is logged in, serve the default app, otherwise server the login app.
2715
+ if (req.session && req.session.userid && obj.users[req.session.userid]) {
2716
+ const user = obj.users[req.session.userid];
2717
+
2718
+ // Check if we are in maintenance mode
2719
+ if ((parent.config.settings.maintenancemode != null) && (user.siteadmin != 4294967295)) {
2720
+ req.session.messageid = 115; // Server under maintenance
2721
+ req.session.loginmode = 1;
2722
+ res.redirect(domain.url);
2723
+ return;
2724
+ }
2725
+
2726
+ // If the request has a "meshmessengerid", redirect to MeshMessenger
2727
+ // This situation happens when you get a push notification for a chat session, but are not logged in.
2728
+ if (req.query.meshmessengerid != null) {
2729
+ res.redirect(domain.url + 'messenger?id=' + req.query.meshmessengerid + ((req.query.key != null) ? ('&key=' + req.query.key) : ''));
2730
+ return;
2731
+ }
2732
+
2733
+ const xdbGetFunc = function dbGetFunc(err, states) {
2734
+ if (dbGetFunc.req.session.userid.split('/')[1] != domain.id) { // Check if the session is for the correct domain
2735
+ parent.debug('web', 'handleRootRequestEx: incorrect domain.');
2736
+ dbGetFunc.req.session = null;
2737
+ dbGetFunc.res.redirect(domain.url + getQueryPortion(dbGetFunc.req)); // BAD***
2738
+ return;
2739
+ }
2740
+
2741
+ // Check if this is a locked account
2742
+ if ((dbGetFunc.user.siteadmin != null) && ((dbGetFunc.user.siteadmin & 32) != 0) && (dbGetFunc.user.siteadmin != 0xFFFFFFFF)) {
2743
+ // Locked account
2744
+ parent.debug('web', 'handleRootRequestEx: locked account.');
2745
+ delete dbGetFunc.req.session.userid;
2746
+ delete dbGetFunc.req.session.currentNode;
2747
+ delete dbGetFunc.req.session.passhint;
2748
+ delete dbGetFunc.req.session.cuserid;
2749
+ dbGetFunc.req.session.messageid = 110; // Account locked.
2750
+ dbGetFunc.res.redirect(domain.url + getQueryPortion(dbGetFunc.req)); // BAD***
2751
+ return;
2752
+ }
2753
+
2754
+ var viewmode = 1;
2755
+ if (dbGetFunc.req.session.viewmode) {
2756
+ viewmode = dbGetFunc.req.session.viewmode;
2757
+ delete dbGetFunc.req.session.viewmode;
2758
+ } else if (dbGetFunc.req.query.viewmode) {
2759
+ viewmode = dbGetFunc.req.query.viewmode;
2760
+ }
2761
+ var currentNode = '';
2762
+ if (dbGetFunc.req.session.currentNode) {
2763
+ currentNode = dbGetFunc.req.session.currentNode;
2764
+ delete dbGetFunc.req.session.currentNode;
2765
+ } else if (dbGetFunc.req.query.node) {
2766
+ currentNode = 'node/' + domain.id + '/' + dbGetFunc.req.query.node;
2767
+ }
2768
+ var logoutcontrols = {};
2769
+ if (obj.args.nousers != true) { logoutcontrols.name = user.name; }
2770
+
2771
+ // Give the web page a list of supported server features for this domain and user
2772
+ const allFeatures = obj.getDomainUserFeatures(domain, dbGetFunc.user, dbGetFunc.req);
2773
+
2774
+ // Create a authentication cookie
2775
+ const authCookie = obj.parent.encodeCookie({ userid: dbGetFunc.user._id, domainid: domain.id, ip: req.clientIp }, obj.parent.loginCookieEncryptionKey);
2776
+ const authRelayCookie = obj.parent.encodeCookie({ ruserid: dbGetFunc.user._id, domainid: domain.id }, obj.parent.loginCookieEncryptionKey);
2777
+
2778
+ // Send the main web application
2779
+ var extras = (dbGetFunc.req.query.key != null) ? ('&key=' + dbGetFunc.req.query.key) : '';
2780
+ 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
2781
+ var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
2782
+
2783
+ // Clean up the U2F challenge if needed
2784
+ if (dbGetFunc.req.session.u2f) { delete dbGetFunc.req.session.u2f; };
2785
+
2786
+ // Intel AMT Scanning options
2787
+ var amtscanoptions = '';
2788
+ if (typeof domain.amtscanoptions == 'string') { amtscanoptions = encodeURIComponent(domain.amtscanoptions); }
2789
+ else if (obj.common.validateStrArray(domain.amtscanoptions)) { domain.amtscanoptions = domain.amtscanoptions.join(','); amtscanoptions = encodeURIComponent(domain.amtscanoptions); }
2790
+
2791
+ // Fetch the web state
2792
+ parent.debug('web', 'handleRootRequestEx: success.');
2793
+
2794
+ var webstate = '';
2795
+ if ((err == null) && (states != null) && (Array.isArray(states)) && (states.length == 1) && (states[0].state != null)) { webstate = obj.filterUserWebState(states[0].state); }
2796
+ if ((webstate == '') && (typeof domain.defaultuserwebstate == 'object')) { webstate = JSON.stringify(domain.defaultuserwebstate); } // User has no web state, use defaults.
2797
+ if (typeof domain.forceduserwebstate == 'object') { // Forces initial user web state if present, use it.
2798
+ var webstate2 = {};
2799
+ try { if (webstate != '') { webstate2 = JSON.parse(webstate); } } catch (ex) { }
2800
+ for (var i in domain.forceduserwebstate) { webstate2[i] = domain.forceduserwebstate[i]; }
2801
+ webstate = JSON.stringify(webstate2);
2802
+ }
2803
+
2804
+ // Custom user interface
2805
+ var customui = '';
2806
+ if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); }
2807
+
2808
+ // Server features
2809
+ var serverFeatures = 127;
2810
+ if (domain.myserver === false) { serverFeatures = 0; } // 64 = Show "My Server" tab
2811
+ else if (typeof domain.myserver == 'object') {
2812
+ if (domain.myserver.backup !== true) { serverFeatures -= 1; } // Disallow simple server backups
2813
+ if (domain.myserver.restore !== true) { serverFeatures -= 2; } // Disallow simple server restore
2814
+ if (domain.myserver.upgrade !== true) { serverFeatures -= 4; } // Disallow server upgrade
2815
+ if (domain.myserver.errorlog !== true) { serverFeatures -= 8; } // Disallow show server crash log
2816
+ if (domain.myserver.console !== true) { serverFeatures -= 16; } // Disallow server console
2817
+ if (domain.myserver.trace !== true) { serverFeatures -= 32; } // Disallow server tracing
2818
+ }
2819
+ if (obj.db.databaseType != 1) { // If not using NeDB, we can't backup using the simple system.
2820
+ if ((serverFeatures & 1) != 0) { serverFeatures -= 1; } // Disallow server backups
2821
+ if ((serverFeatures & 2) != 0) { serverFeatures -= 2; } // Disallow simple server restore
2822
+ }
2823
+
2824
+ // Refresh the session
2825
+ render(dbGetFunc.req, dbGetFunc.res, getRenderPage('default', dbGetFunc.req, domain), getRenderArgs({
2826
+ authCookie: authCookie,
2827
+ authRelayCookie: authRelayCookie,
2828
+ viewmode: viewmode,
2829
+ currentNode: currentNode,
2830
+ logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27'),
2831
+ domain: domain.id,
2832
+ debuglevel: parent.debugLevel,
2833
+ serverDnsName: obj.getWebServerName(domain),
2834
+ serverRedirPort: args.redirport,
2835
+ serverPublicPort: httpsPort,
2836
+ serverfeatures: serverFeatures,
2837
+ features: allFeatures.features,
2838
+ features2: allFeatures.features2,
2839
+ sessiontime: (args.sessiontime) ? args.sessiontime : 60,
2840
+ mpspass: args.mpspass,
2841
+ passRequirements: passRequirements,
2842
+ customui: customui,
2843
+ webcerthash: Buffer.from(obj.webCertificateFullHashs[domain.id], 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'),
2844
+ footer: (domain.footer == null) ? '' : domain.footer,
2845
+ webstate: encodeURIComponent(webstate).replace(/'/g, '%27'),
2846
+ amtscanoptions: amtscanoptions,
2847
+ pluginHandler: (parent.pluginHandler == null) ? 'null' : parent.pluginHandler.prepExports()
2848
+ }, dbGetFunc.req, domain), user);
2849
+ }
2850
+ xdbGetFunc.req = req;
2851
+ xdbGetFunc.res = res;
2852
+ xdbGetFunc.user = user;
2853
+ obj.db.Get('ws' + user._id, xdbGetFunc);
2854
+ } else {
2855
+ // Send back the login application
2856
+ // If this is a 2 factor auth request, look for a hardware key challenge.
2857
+ // Normal login 2 factor request
2858
+ if (req.session && (req.session.loginmode == 4) && (req.session.tuserid)) {
2859
+ var user = obj.users[req.session.tuserid];
2860
+ if (user != null) {
2861
+ parent.debug('web', 'handleRootRequestEx: sending 2FA challenge.');
2862
+ getHardwareKeyChallenge(req, domain, user, function (hwchallenge) { handleRootRequestLogin(req, res, domain, hwchallenge, passRequirements); });
2863
+ return;
2864
+ }
2865
+ }
2866
+ // Password recovery 2 factor request
2867
+ if (req.session && (req.session.loginmode == 5) && (req.session.temail)) {
2868
+ obj.db.GetUserWithVerifiedEmail(domain.id, req.session.temail, function (err, docs) {
2869
+ if ((err != null) || (docs.length == 0)) {
2870
+ parent.debug('web', 'handleRootRequestEx: password recover 2FA fail.');
2871
+ req.session = null;
2872
+ res.redirect(domain.url + getQueryPortion(req)); // BAD***
2873
+ } else {
2874
+ var user = obj.users[docs[0]._id];
2875
+ if (user != null) {
2876
+ parent.debug('web', 'handleRootRequestEx: password recover 2FA challenge.');
2877
+ getHardwareKeyChallenge(req, domain, user, function (hwchallenge) { handleRootRequestLogin(req, res, domain, hwchallenge, passRequirements); });
2878
+ } else {
2879
+ parent.debug('web', 'handleRootRequestEx: password recover 2FA no user.');
2880
+ req.session = null;
2881
+ res.redirect(domain.url + getQueryPortion(req)); // BAD***
2882
+ }
2883
+ }
2884
+ });
2885
+ return;
2886
+ }
2887
+ handleRootRequestLogin(req, res, domain, '', passRequirements);
2888
+ }
2889
+ }
2890
+
2891
+ // Return a list of server supported features for a given domain and user
2892
+ obj.getDomainUserFeatures = function(domain, user, req) {
2893
+ var features = 0;
2894
+ var features2 = 0;
2895
+ if (obj.args.wanonly == true) { features += 0x00000001; } // WAN-only mode
2896
+ if (obj.args.lanonly == true) { features += 0x00000002; } // LAN-only mode
2897
+ if (obj.args.nousers == true) { features += 0x00000004; } // Single user mode
2898
+ if (domain.userQuota == -1) { features += 0x00000008; } // No server files mode
2899
+ if (obj.args.mpstlsoffload) { features += 0x00000010; } // No mutual-auth CIRA
2900
+ if ((parent.config.settings.allowframing != null) || (domain.allowframing != null)) { features += 0x00000020; } // Allow site within iframe
2901
+ if ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true)) { features += 0x00000040; } // Email invites
2902
+ if (obj.args.webrtc == true) { features += 0x00000080; } // Enable WebRTC (Default false for now)
2903
+ // 0x00000100 --> This feature flag is free for future use.
2904
+ if (obj.args.allowhighqualitydesktop !== false) { features += 0x00000200; } // Enable AllowHighQualityDesktop (Default true)
2905
+ if ((obj.args.lanonly == true) || (obj.args.mpsport == 0)) { features += 0x00000400; } // No CIRA
2906
+ if ((obj.parent.serverSelfWriteAllowed == true) && (user != null) && ((user.siteadmin & 0x00000010) != 0)) { features += 0x00000800; } // Server can self-write (Allows self-update)
2907
+ 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
2908
+ if (domain.agentnoproxy === true) { features += 0x00002000; } // Indicates that agents should be installed without using a HTTP proxy
2909
+ if ((parent.config.settings.no2factorauth !== true) && domain.yubikey && domain.yubikey.id && domain.yubikey.secret && (user._id.split('/')[2][0] != '~')) { features += 0x00004000; } // Indicates Yubikey support
2910
+ if (domain.geolocation == true) { features += 0x00008000; } // Enable geo-location features
2911
+ if ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true)) { features += 0x00010000; } // Enable password hints
2912
+ if (parent.config.settings.no2factorauth !== true) { features += 0x00020000; } // Enable WebAuthn/FIDO2 support
2913
+ if ((obj.args.nousers != true) && (domain.passwordrequirements != null) && (domain.passwordrequirements.force2factor === true) && (user._id.split('/')[2][0] != '~')) {
2914
+ // Check if we can skip 2nd factor auth because of the source IP address
2915
+ var skip2factor = false;
2916
+ if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
2917
+ for (var i in domain.passwordrequirements.skip2factor) {
2918
+ if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) { skip2factor = true; }
2919
+ }
2920
+ }
2921
+ if (skip2factor == false) { features += 0x00040000; } // Force 2-factor auth
2922
+ }
2923
+ 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.
2924
+ if (domain.amtacmactivation) { features += 0x00100000; } // Intel AMT ACM activation/upgrade is possible
2925
+ if (domain.usernameisemail) { features += 0x00200000; } // Username is email address
2926
+ if (parent.mqttbroker != null) { features += 0x00400000; } // This server supports MQTT channels
2927
+ if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null)) { features += 0x00800000; } // using email for 2FA is allowed
2928
+ if (domain.agentinvitecodes == true) { features += 0x01000000; } // Support for agent invite codes
2929
+ if (parent.smsserver != null) { features += 0x02000000; } // SMS messaging is supported
2930
+ if ((parent.smsserver != null) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false))) { features += 0x04000000; } // SMS 2FA is allowed
2931
+ if (domain.sessionrecording != null) { features += 0x08000000; } // Server recordings enabled
2932
+ if (domain.urlswitching === false) { features += 0x10000000; } // Disables the URL switching feature
2933
+ if (domain.novnc === false) { features += 0x20000000; } // Disables noVNC
2934
+ if (domain.mstsc !== true) { features += 0x40000000; } // Disables MSTSC.js
2935
+ if (obj.isTrustedCert(domain) == false) { features += 0x80000000; } // Indicate we are not using a trusted certificate
2936
+ if (obj.parent.amtManager != null) { features2 += 0x00000001; } // Indicates that the Intel AMT manager is active
2937
+ if (obj.parent.firebase != null) { features2 += 0x00000002; } // Indicates the server supports Firebase push messaging
2938
+ if ((obj.parent.firebase != null) && (obj.parent.firebase.pushOnly != true)) { features2 += 0x00000004; } // Indicates the server supports Firebase two-way push messaging
2939
+ if (obj.parent.webpush != null) { features2 += 0x00000008; } // Indicates web push is enabled
2940
+ if (((obj.args.noagentupdate == 1) || (obj.args.noagentupdate == true))) { features2 += 0x00000010; } // No agent update
2941
+ if (parent.amtProvisioningServer != null) { features2 += 0x00000020; } // Intel AMT LAN provisioning server
2942
+ if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.push2factor != false)) && (obj.parent.firebase != null)) { features2 += 0x00000040; } // Indicates device push notification 2FA is enabled
2943
+ if ((typeof domain.passwordrequirements != 'object') || ((domain.passwordrequirements.logintokens !== false) && ((Array.isArray(domain.passwordrequirements.logintokens) == false) || (domain.passwordrequirements.logintokens.indexOf(user._id) >= 0)))) { features2 += 0x00000080; } // Indicates login tokens are allowed
2944
+ if (req.session.loginToken != null) { features2 += 0x00000100; } // LoginToken mode, no account changes.
2945
+ if (domain.ssh == true) { features2 += 0x00000200; } // SSH is enabled
2946
+ if (domain.localsessionrecording === false) { features2 += 0x00000400; } // Disable local recording feature
2947
+ if (domain.clipboardget == false) { features2 += 0x00000800; } // Disable clipboard get
2948
+ if (domain.clipboardset == false) { features2 += 0x00001000; } // Disable clipboard set
2949
+ if ((typeof domain.desktop == 'object') && (domain.desktop.viewonly == true)) { features2 += 0x00002000; } // Indicates remote desktop is viewonly
2950
+ if (domain.mailserver != null) { features2 += 0x00004000; } // Indicates email server is active
2951
+ if (domain.devicesearchbarserverandclientname) { features2 += 0x00008000; } // Search bar will find both server name and client name
2952
+ if (domain.ipkvm) { features2 += 0x00010000; } // Indicates support for IP KVM device groups
2953
+ if ((domain.passwordrequirements) && (domain.passwordrequirements.otp2factor == false)) { features2 += 0x00020000; } // Indicates support for OTP 2FA is disabled
2954
+ if ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.backupcode2factor === false)) { features2 += 0x00040000; } // Indicates 2FA backup codes are disabled
2955
+ if ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.single2factorwarning === false)) { features2 += 0x00080000; } // Indicates no warning if a single 2FA is in use
2956
+ if (domain.nightmode === 1) { features2 += 0x00100000; } // Always night mode
2957
+ if (domain.nightmode === 2) { features2 += 0x00200000; } // Always day mode
2958
+ if (domain.allowsavingdevicecredentials == false) { features2 += 0x00400000; } // Do not allow device credentials to be saved on the server
2959
+ return { features: features, features2: features2 };
2960
+ }
2961
+
2962
+ function handleRootRequestLogin(req, res, domain, hardwareKeyChallenge, passRequirements) {
2963
+ parent.debug('web', 'handleRootRequestLogin()');
2964
+ var features = 0;
2965
+ 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
2966
+ if (domain.usernameisemail) { features += 0x00200000; } // Username is email address
2967
+ var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
2968
+ var loginmode = 0;
2969
+ 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.
2970
+
2971
+ // Format an error message if needed
2972
+ var passhint = null, msgid = 0;
2973
+ if (req.session != null) {
2974
+ msgid = req.session.messageid;
2975
+ if ((msgid == 5) || (loginmode == 7) || ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true))) { passhint = EscapeHtml(req.session.passhint); }
2976
+ delete req.session.messageid;
2977
+ delete req.session.passhint;
2978
+ }
2979
+ const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false));
2980
+ const emailcheck = (allowAccountReset && (domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap'))
2981
+
2982
+ // Check if we are allowed to create new users using the login screen
2983
+ var newAccountsAllowed = true;
2984
+ if ((domain.newaccounts !== 1) && (domain.newaccounts !== true)) { for (var i in obj.users) { if (obj.users[i].domain == domain.id) { newAccountsAllowed = false; break; } } }
2985
+ if (parent.config.settings.maintenancemode != null) { newAccountsAllowed = false; }
2986
+
2987
+ // Encrypt the hardware key challenge state if needed
2988
+ var hwstate = null;
2989
+ if (hardwareKeyChallenge) { hwstate = obj.parent.encodeCookie({ u: req.session.tuser, p: req.session.tpass, c: req.session.u2f }, obj.parent.loginCookieEncryptionKey) }
2990
+
2991
+ // Check if we can use OTP tokens with email. We can't use email for 2FA password recovery (loginmode 5).
2992
+ var otpemail = (loginmode != 5) && (domain.mailserver != null) && (req.session != null) && ((req.session.temail === 1) || (typeof req.session.temail == 'string'));
2993
+ if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; }
2994
+ var otpsms = (parent.smsserver != null) && (req.session != null) && (req.session.tsms === 1);
2995
+ if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; }
2996
+ var otppush = (parent.firebase != null) && (req.session != null) && (req.session.tpush === 1);
2997
+ if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.push2factor == false)) { otppush = false; }
2998
+ const autofido = ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.autofido2fa == true)); // See if FIDO should be automatically prompted if user account has it.
2999
+
3000
+ // See if we support two-factor trusted cookies
3001
+ var twoFactorCookieDays = 30;
3002
+ if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
3003
+
3004
+ // See what authentication strategies we have
3005
+ var authStrategies = [];
3006
+ if (typeof domain.authstrategies == 'object') {
3007
+ if (typeof domain.authstrategies.twitter == 'object') { authStrategies.push('twitter'); }
3008
+ if (typeof domain.authstrategies.google == 'object') { authStrategies.push('google'); }
3009
+ if (typeof domain.authstrategies.github == 'object') { authStrategies.push('github'); }
3010
+ if (typeof domain.authstrategies.reddit == 'object') { authStrategies.push('reddit'); }
3011
+ if (typeof domain.authstrategies.azure == 'object') { authStrategies.push('azure'); }
3012
+ if (typeof domain.authstrategies.oidc == 'object') { authStrategies.push('oidc'); }
3013
+ if (typeof domain.authstrategies.intel == 'object') { authStrategies.push('intel'); }
3014
+ if (typeof domain.authstrategies.jumpcloud == 'object') { authStrategies.push('jumpcloud'); }
3015
+ if (typeof domain.authstrategies.saml == 'object') { authStrategies.push('saml'); }
3016
+ }
3017
+
3018
+ // Custom user interface
3019
+ var customui = '';
3020
+ if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); }
3021
+
3022
+ // Get two-factor screen timeout
3023
+ var twoFactorTimeout = 300000; // Default is 5 minutes, 0 for no timeout.
3024
+ if ((typeof domain.passwordrequirements == 'object') && (typeof domain.passwordrequirements.twofactortimeout == 'number')) {
3025
+ twoFactorTimeout = domain.passwordrequirements.twofactortimeout * 1000;
3026
+ }
3027
+
3028
+ // Render the login page
3029
+ render(req, res,
3030
+ getRenderPage((domain.sitestyle == 2) ? 'login2' : 'login', req, domain),
3031
+ getRenderArgs({
3032
+ loginmode: loginmode,
3033
+ rootCertLink: getRootCertLink(domain),
3034
+ newAccount: newAccountsAllowed,
3035
+ newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1),
3036
+ serverDnsName: obj.getWebServerName(domain),
3037
+ serverPublicPort: httpsPort,
3038
+ passlogin: (typeof domain.showpasswordlogin == 'boolean') ? domain.showpasswordlogin : true,
3039
+ emailcheck: emailcheck,
3040
+ features: features,
3041
+ sessiontime: (args.sessiontime) ? args.sessiontime : 60,
3042
+ passRequirements: passRequirements,
3043
+ customui: customui,
3044
+ footer: (domain.loginfooter == null) ? '' : domain.loginfooter,
3045
+ hkey: encodeURIComponent(hardwareKeyChallenge).replace(/'/g, '%27'),
3046
+ messageid: msgid,
3047
+ passhint: passhint,
3048
+ welcometext: domain.welcometext ? encodeURIComponent(domain.welcometext).split('\'').join('\\\'') : null,
3049
+ welcomePictureFullScreen: ((typeof domain.welcomepicturefullscreen == 'boolean') ? domain.welcomepicturefullscreen : false),
3050
+ hwstate: hwstate,
3051
+ otpemail: otpemail,
3052
+ otpsms: otpsms,
3053
+ otppush: otppush,
3054
+ autofido: autofido,
3055
+ twoFactorCookieDays: twoFactorCookieDays,
3056
+ authStrategies: authStrategies.join(','),
3057
+ loginpicture: (typeof domain.loginpicture == 'string'),
3058
+ tokenTimeout: twoFactorTimeout // Two-factor authentication screen timeout in milliseconds
3059
+ }, req, domain, (domain.sitestyle == 2) ? 'login2' : 'login'));
3060
+ }
3061
+
3062
+ // Handle a post request on the root
3063
+ function handleRootPostRequest(req, res) {
3064
+ const domain = checkUserIpAddress(req, res);
3065
+ if (domain == null) { return; }
3066
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.end("Not Found"); return; } // Check 3FA URL key
3067
+ parent.debug('web', 'handleRootPostRequest, action: ' + req.body.action);
3068
+
3069
+ switch (req.body.action) {
3070
+ case 'login': { handleLoginRequest(req, res, true); break; }
3071
+ case 'tokenlogin': {
3072
+ if (req.body.hwstate) {
3073
+ var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 10);
3074
+ if (cookie != null) { req.session.tuser = cookie.u; req.session.tpass = cookie.p; req.session.u2f = cookie.c; }
3075
+ }
3076
+ handleLoginRequest(req, res, true); break;
3077
+ }
3078
+ case 'pushlogin': {
3079
+ if (req.body.hwstate) {
3080
+ var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 1);
3081
+ if ((cookie != null) && (typeof cookie.u == 'string') && (cookie.d == domain.id) && (cookie.a == 'pushAuth')) {
3082
+ // Push authentication is a success, login the user
3083
+ req.session = { userid: cookie.u };
3084
+
3085
+ // Check if we need to remember this device
3086
+ if ((req.body.remembertoken === 'on') && ((domain.twofactorcookiedurationdays == null) || (domain.twofactorcookiedurationdays > 0))) {
3087
+ var maxCookieAge = domain.twofactorcookiedurationdays;
3088
+ if (typeof maxCookieAge != 'number') { maxCookieAge = 30; }
3089
+ const twoFactorCookie = obj.parent.encodeCookie({ userid: cookie.u, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey);
3090
+ res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: ((parent.config.settings.cookieipcheck === false) ? 'none' : 'strict'), secure: true });
3091
+ }
3092
+
3093
+ handleRootRequestEx(req, res, domain);
3094
+ return;
3095
+ }
3096
+ }
3097
+ handleLoginRequest(req, res, true); break;
3098
+ }
3099
+ case 'changepassword': { handlePasswordChangeRequest(req, res, true); break; }
3100
+ case 'deleteaccount': { handleDeleteAccountRequest(req, res, true); break; }
3101
+ case 'createaccount': { handleCreateAccountRequest(req, res, true); break; }
3102
+ case 'resetpassword': { handleResetPasswordRequest(req, res, true); break; }
3103
+ case 'resetaccount': { handleResetAccountRequest(req, res, true); break; }
3104
+ case 'checkemail': { handleCheckAccountEmailRequest(req, res, true); break; }
3105
+ default: { handleLoginRequest(req, res, true); break; }
3106
+ }
3107
+ }
3108
+
3109
+ // Return true if it looks like we are using a real TLS certificate.
3110
+ obj.isTrustedCert = function (domain) {
3111
+ if ((domain != null) && (typeof domain.trustedcert == 'boolean')) return domain.trustedcert; // If the status of the cert specified, use that.
3112
+ if (typeof obj.args.trustedcert == 'boolean') return obj.args.trustedcert; // If the status of the cert specified, use that.
3113
+ if (obj.args.tlsoffload != null) return true; // We are using TLS offload, a real cert is likely used.
3114
+ 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.
3115
+ if (obj.certificates.WebIssuer.indexOf('MeshCentralRoot-') == 0) return false; // Our cert is issued by self-signed cert.
3116
+ if (obj.certificates.CommonName.indexOf('.') == -1) return false; // Our cert is named with a fake name
3117
+ return true; // This is a guess
3118
+ }
3119
+
3120
+ // Get the link to the root certificate if needed
3121
+ function getRootCertLink(domain) {
3122
+ // Check if the HTTPS certificate is issued from MeshCentralRoot, if so, add download link to root certificate.
3123
+ if (obj.isTrustedCert(domain) == false) {
3124
+ // Get the domain suffix
3125
+ var xdomain = (domain.dns == null) ? domain.id : '';
3126
+ if (xdomain != '') xdomain += '/';
3127
+ return '<a href=/' + xdomain + 'MeshServerRootCert.cer title="Download the root certificate for this server">Root Certificate</a>';
3128
+ }
3129
+ return '';
3130
+ }
3131
+
3132
+ // Serve the xterm page
3133
+ function handleXTermRequest(req, res) {
3134
+ const domain = checkUserIpAddress(req, res);
3135
+ if (domain == null) { return; }
3136
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
3137
+
3138
+ parent.debug('web', 'handleXTermRequest: sending xterm');
3139
+ res.set({ 'Cache-Control': 'no-store' });
3140
+ if (req.session && req.session.userid) {
3141
+ if (req.session.userid.split('/')[1] != domain.id) { res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain
3142
+ var user = obj.users[req.session.userid];
3143
+ if ((user == null) || (req.query.nodeid == null)) { res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the user exists
3144
+
3145
+ // Check permissions
3146
+ obj.GetNodeWithRights(domain, user, req.query.nodeid, function (node, rights, visible) {
3147
+ if ((node == null) || ((rights & 8) == 0) || ((rights != 0xFFFFFFFF) && ((rights & 512) != 0))) { res.redirect(domain.url + getQueryPortion(req)); return; }
3148
+
3149
+ var logoutcontrols = { name: user.name };
3150
+ var extras = (req.query.key != null) ? ('&key=' + req.query.key) : '';
3151
+ 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
3152
+
3153
+ // Create a authentication cookie
3154
+ const authCookie = obj.parent.encodeCookie({ userid: user._id, domainid: domain.id, ip: req.clientIp }, obj.parent.loginCookieEncryptionKey);
3155
+ const authRelayCookie = obj.parent.encodeCookie({ ruserid: user._id, domainid: domain.id }, obj.parent.loginCookieEncryptionKey);
3156
+ var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
3157
+ 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));
3158
+ });
3159
+ } else {
3160
+ res.redirect(domain.url + getQueryPortion(req));
3161
+ return;
3162
+ }
3163
+ }
3164
+
3165
+ // Render the terms of service.
3166
+ function handleTermsRequest(req, res) {
3167
+ const domain = checkUserIpAddress(req, res);
3168
+ if (domain == null) { return; }
3169
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
3170
+
3171
+ // See if term.txt was loaded from the database
3172
+ if ((parent.configurationFiles != null) && (parent.configurationFiles['terms.txt'] != null)) {
3173
+ // Send the terms from the database
3174
+ res.set({ 'Cache-Control': 'no-store' });
3175
+ if (req.session && req.session.userid) {
3176
+ if (req.session.userid.split('/')[1] != domain.id) { req.session = null; res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain
3177
+ var user = obj.users[req.session.userid];
3178
+ var logoutcontrols = { name: user.name };
3179
+ var extras = (req.query.key != null) ? ('&key=' + req.query.key) : '';
3180
+ 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
3181
+ 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));
3182
+ } else {
3183
+ render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(parent.configurationFiles['terms.txt'].toString()).split('\'').join('\\\''), logoutControls: encodeURIComponent('{}') }, req, domain));
3184
+ }
3185
+ } else {
3186
+ // See if there is a terms.txt file in meshcentral-data
3187
+ var p = obj.path.join(obj.parent.datapath, 'terms.txt');
3188
+ if (obj.fs.existsSync(p)) {
3189
+ obj.fs.readFile(p, 'utf8', function (err, data) {
3190
+ if (err != null) { parent.debug('web', 'handleTermsRequest: no terms.txt'); res.sendStatus(404); return; }
3191
+
3192
+ // Send the terms from terms.txt
3193
+ res.set({ 'Cache-Control': 'no-store' });
3194
+ if (req.session && req.session.userid) {
3195
+ if (req.session.userid.split('/')[1] != domain.id) { req.session = null; res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain
3196
+ var user = obj.users[req.session.userid];
3197
+ var logoutcontrols = { name: user.name };
3198
+ var extras = (req.query.key != null) ? ('&key=' + req.query.key) : '';
3199
+ 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
3200
+ render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(data).split('\'').join('\\\''), logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain));
3201
+ } else {
3202
+ render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(data).split('\'').join('\\\''), logoutControls: encodeURIComponent('{}') }, req, domain));
3203
+ }
3204
+ });
3205
+ } else {
3206
+ // Send the default terms
3207
+ parent.debug('web', 'handleTermsRequest: sending default terms');
3208
+ res.set({ 'Cache-Control': 'no-store' });
3209
+ if (req.session && req.session.userid) {
3210
+ if (req.session.userid.split('/')[1] != domain.id) { req.session = null; res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain
3211
+ var user = obj.users[req.session.userid];
3212
+ var logoutcontrols = { name: user.name };
3213
+ var extras = (req.query.key != null) ? ('&key=' + req.query.key) : '';
3214
+ 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
3215
+ render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain));
3216
+ } else {
3217
+ render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ logoutControls: encodeURIComponent('{}') }, req, domain));
3218
+ }
3219
+ }
3220
+ }
3221
+ }
3222
+
3223
+ // Render the messenger application.
3224
+ function handleMessengerRequest(req, res) {
3225
+ const domain = getDomain(req);
3226
+ if (domain == null) { parent.debug('web', 'handleMessengerRequest: no domain'); res.sendStatus(404); return; }
3227
+ parent.debug('web', 'handleMessengerRequest()');
3228
+
3229
+ // Check if we are in maintenance mode
3230
+ if (parent.config.settings.maintenancemode != null) {
3231
+ 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));
3232
+ return;
3233
+ }
3234
+
3235
+ // Check if this session is for a user
3236
+ if (req.query.id == null) { res.sendStatus(404); return; }
3237
+ var idSplit = decodeURIComponent(req.query.id).split('/');
3238
+ if ((idSplit.length != 7) || (idSplit[0] != 'meshmessenger')) { res.sendStatus(404); return; }
3239
+ if ((idSplit[1] == 'user') && (idSplit[4] == 'user')) {
3240
+ // This is a user to user conversation, both users must be logged in.
3241
+ var user1 = idSplit[1] + '/' + idSplit[2] + '/' + idSplit[3]
3242
+ var user2 = idSplit[4] + '/' + idSplit[5] + '/' + idSplit[6]
3243
+ if (!req.session || !req.session.userid) {
3244
+ // Redirect to login page
3245
+ if (req.query.key != null) { res.redirect(domain.url + '?key=' + req.query.key + '&meshmessengerid=' + req.query.id); } else { res.redirect(domain.url + '?meshmessengerid=' + req.query.id); }
3246
+ return;
3247
+ }
3248
+ if ((req.session.userid != user1) && (req.session.userid != user2)) { res.sendStatus(404); return; }
3249
+ }
3250
+
3251
+ // Get WebRTC configuration
3252
+ var webRtcConfig = null;
3253
+ 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'); }
3254
+ else if (args.webrtconfig && (typeof args.webrtconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(args.webrtconfig)).replace(/'/g, '%27'); }
3255
+
3256
+ // Setup other options
3257
+ var options = { webrtconfig: webRtcConfig };
3258
+ if (typeof domain.meshmessengertitle == 'string') { options.meshMessengerTitle = domain.meshmessengertitle; } else { options.meshMessengerTitle = '!'; }
3259
+
3260
+ // Get the userid and name
3261
+ if ((domain.meshmessengertitle != null) && (req.query.id != null) && (req.query.id.startsWith('meshmessenger/node'))) {
3262
+ if (idSplit.length == 7) {
3263
+ const user = obj.users[idSplit[4] + '/' + idSplit[5] + '/' + idSplit[6]];
3264
+ if (user != null) {
3265
+ if (domain.meshmessengertitle.indexOf('{0}') >= 0) { options.username = encodeURIComponent(user.realname ? user.realname : user.name).replace(/'/g, '%27'); }
3266
+ if (domain.meshmessengertitle.indexOf('{1}') >= 0) { options.userid = encodeURIComponent(user.name).replace(/'/g, '%27'); }
3267
+ }
3268
+ }
3269
+ }
3270
+
3271
+ // Render the page
3272
+ res.set({ 'Cache-Control': 'no-store' });
3273
+ render(req, res, getRenderPage('messenger', req, domain), getRenderArgs(options, req, domain));
3274
+ }
3275
+
3276
+ // Handle messenger image request
3277
+ function handleMessengerImageRequest(req, res) {
3278
+ const domain = getDomain(req);
3279
+ if (domain == null) { parent.debug('web', 'handleMessengerImageRequest: no domain'); res.sendStatus(404); return; }
3280
+ parent.debug('web', 'handleMessengerImageRequest()');
3281
+
3282
+ // Check if we are in maintenance mode
3283
+ if (parent.config.settings.maintenancemode != null) { res.sendStatus(404); return; }
3284
+
3285
+ //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
3286
+ if (domain.meshmessengerpicture) {
3287
+ // Use the configured messenger logo picture
3288
+ try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.meshmessengerpicture)); return; } catch (ex) { }
3289
+ }
3290
+
3291
+ var imagefile = 'images/messenger.png';
3292
+ if (domain.webpublicpath != null) {
3293
+ obj.fs.exists(obj.path.join(domain.webpublicpath, imagefile), function (exists) {
3294
+ if (exists) {
3295
+ // Use the domain logo picture
3296
+ try { res.sendFile(obj.path.join(domain.webpublicpath, imagefile)); } catch (ex) { res.sendStatus(404); }
3297
+ } else {
3298
+ // Use the default logo picture
3299
+ try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
3300
+ }
3301
+ });
3302
+ } else if (parent.webPublicOverridePath) {
3303
+ obj.fs.exists(obj.path.join(obj.parent.webPublicOverridePath, imagefile), function (exists) {
3304
+ if (exists) {
3305
+ // Use the override logo picture
3306
+ try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, imagefile)); } catch (ex) { res.sendStatus(404); }
3307
+ } else {
3308
+ // Use the default logo picture
3309
+ try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
3310
+ }
3311
+ });
3312
+ } else {
3313
+ // Use the default logo picture
3314
+ try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
3315
+ }
3316
+ }
3317
+
3318
+ // Returns the server root certificate encoded in base64
3319
+ function getRootCertBase64() {
3320
+ var rootcert = obj.certificates.root.cert;
3321
+ var i = rootcert.indexOf('-----BEGIN CERTIFICATE-----\r\n');
3322
+ if (i >= 0) { rootcert = rootcert.substring(i + 29); }
3323
+ i = rootcert.indexOf('-----END CERTIFICATE-----');
3324
+ if (i >= 0) { rootcert = rootcert.substring(i, 0); }
3325
+ return Buffer.from(rootcert, 'base64').toString('base64');
3326
+ }
3327
+
3328
+ // Returns the mesh server root certificate
3329
+ function handleRootCertRequest(req, res) {
3330
+ const domain = getDomain(req);
3331
+ if (domain == null) { parent.debug('web', 'handleRootCertRequest: no domain'); res.sendStatus(404); return; }
3332
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
3333
+ if ((obj.userAllowedIp != null) && (checkIpAddressEx(req, res, obj.userAllowedIp, false) === false)) { parent.debug('web', 'handleRootCertRequest: invalid ip'); return; } // Check server-wide IP filter only.
3334
+ parent.debug('web', 'handleRootCertRequest()');
3335
+ setContentDispositionHeader(res, 'application/octet-stream', certificates.RootName + '.cer', null, 'rootcert.cer');
3336
+ res.send(Buffer.from(getRootCertBase64(), 'base64'));
3337
+ }
3338
+
3339
+ // Handle user public file downloads
3340
+ function handleDownloadUserFiles(req, res) {
3341
+ const domain = checkUserIpAddress(req, res);
3342
+ if (domain == null) { return; }
3343
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
3344
+
3345
+ if (obj.common.validateString(req.path, 1, 4096) == false) { res.sendStatus(404); return; }
3346
+ var domainname = 'domain', spliturl = decodeURIComponent(req.path).split('/'), filename = '';
3347
+ if ((spliturl.length < 3) || (obj.common.IsFilenameValid(spliturl[2]) == false) || (domain.userQuota == -1)) { res.sendStatus(404); return; }
3348
+ if (domain.id != '') { domainname = 'domain-' + domain.id; }
3349
+ var path = obj.path.join(obj.filespath, domainname + '/user-' + spliturl[2] + '/Public');
3350
+ 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; } }
3351
+
3352
+ var stat = null;
3353
+ try { stat = obj.fs.statSync(path); } catch (e) { }
3354
+ if ((stat != null) && ((stat.mode & 0x004000) == 0)) {
3355
+ if (req.query.download == 1) {
3356
+ setContentDispositionHeader(res, 'application/octet-stream', filename, null, 'file.bin');
3357
+ try { res.sendFile(obj.path.resolve(__dirname, path)); } catch (e) { res.sendStatus(404); }
3358
+ } else {
3359
+ render(req, res, getRenderPage((domain.sitestyle == 2) ? 'download2' : 'download', req, domain), getRenderArgs({ rootCertLink: getRootCertLink(domain), messageid: 1, fileurl: req.path + '?download=1', filename: filename, filesize: stat.size }, req, domain));
3360
+ }
3361
+ } else {
3362
+ render(req, res, getRenderPage((domain.sitestyle == 2) ? 'download2' : 'download', req, domain), getRenderArgs({ rootCertLink: getRootCertLink(domain), messageid: 2 }, req, domain));
3363
+ }
3364
+ }
3365
+
3366
+ // Handle device file request
3367
+ function handleDeviceFile(req, res) {
3368
+ const domain = checkUserIpAddress(req, res);
3369
+ if (domain == null) { return; }
3370
+ if ((req.query.c == null) || (req.query.f == null)) { res.sendStatus(404); return; }
3371
+
3372
+ // Check the inbound desktop sharing cookie
3373
+ var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
3374
+ if ((c == null) || (c.domainid !== domain.id)) { res.sendStatus(404); return; }
3375
+
3376
+ // Check userid
3377
+ const user = obj.users[c.userid];
3378
+ if ((c == user)) { res.sendStatus(404); return; }
3379
+
3380
+ // If this cookie has restricted usages, check that it's allowed to perform downloads
3381
+ if (Array.isArray(c.usages) && (c.usages.indexOf(10) < 0)) { res.sendStatus(404); return; } // Check protocol #10
3382
+
3383
+ if (c.nid != null) { req.query.n = c.nid.split('/')[2]; } // This cookie is restricted to a specific nodeid.
3384
+ if (req.query.n == null) { res.sendStatus(404); return; }
3385
+
3386
+ // Check if this user has permission to manage this computer
3387
+ obj.GetNodeWithRights(domain, user, 'node/' + domain.id + '/' + req.query.n, function (node, rights, visible) {
3388
+ if ((node == null) || ((rights & MESHRIGHT_REMOTECONTROL) == 0) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device
3389
+
3390
+ // All good, start the file transfer
3391
+ req.query.id = getRandomLowerCase(12);
3392
+ obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, null, res, req, domain, user, node.meshid, node._id);
3393
+ });
3394
+ }
3395
+
3396
+ // Handle download of a server file by an agent
3397
+ function handleAgentDownloadFile(req, res) {
3398
+ const domain = checkUserIpAddress(req, res);
3399
+ if (domain == null) { return; }
3400
+ if (req.query.c == null) { res.sendStatus(404); return; }
3401
+
3402
+ // Check the inbound desktop sharing cookie
3403
+ var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 5); // 5 minute timeout
3404
+ 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; }
3405
+
3406
+ // Send the file back
3407
+ try { res.sendFile(obj.path.join(obj.filespath, 'tmp', c.f)); return; } catch (ex) { res.sendStatus(404); }
3408
+ }
3409
+
3410
+ // Handle logo request
3411
+ function handleLogoRequest(req, res) {
3412
+ const domain = checkUserIpAddress(req, res);
3413
+ if (domain == null) { return; }
3414
+
3415
+ //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
3416
+ if (domain.titlepicture) {
3417
+ if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.titlepicture] != null)) {
3418
+ // Use the logo in the database
3419
+ res.set({ 'Content-Type': domain.titlepicture.toLowerCase().endsWith('.png')?'image/png':'image/jpeg' });
3420
+ res.send(parent.configurationFiles[domain.titlepicture]);
3421
+ return;
3422
+ } else {
3423
+ // Use the logo on file
3424
+ try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.titlepicture)); return; } catch (ex) { }
3425
+ }
3426
+ }
3427
+
3428
+ if ((domain.webpublicpath != null) && (obj.fs.existsSync(obj.path.join(domain.webpublicpath, 'images/logoback.png')))) {
3429
+ // Use the domain logo picture
3430
+ try { res.sendFile(obj.path.join(domain.webpublicpath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
3431
+ } else if (parent.webPublicOverridePath && obj.fs.existsSync(obj.path.join(obj.parent.webPublicOverridePath, 'images/logoback.png'))) {
3432
+ // Use the override logo picture
3433
+ try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
3434
+ } else {
3435
+ // Use the default logo picture
3436
+ try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
3437
+ }
3438
+ }
3439
+
3440
+ // Handle login logo request
3441
+ function handleLoginLogoRequest(req, res) {
3442
+ const domain = checkUserIpAddress(req, res);
3443
+ if (domain == null) { return; }
3444
+
3445
+ //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
3446
+ if (domain.loginpicture) {
3447
+ if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.loginpicture] != null)) {
3448
+ // Use the logo in the database
3449
+ res.set({ 'Content-Type': domain.loginpicture.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
3450
+ res.send(parent.configurationFiles[domain.loginpicture]);
3451
+ return;
3452
+ } else {
3453
+ // Use the logo on file
3454
+ try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.loginpicture)); return; } catch (ex) { res.sendStatus(404); }
3455
+ }
3456
+ } else {
3457
+ res.sendStatus(404);
3458
+ }
3459
+ }
3460
+
3461
+ // Handle translation request
3462
+ function handleTranslationsRequest(req, res) {
3463
+ const domain = checkUserIpAddress(req, res);
3464
+ if (domain == null) { return; }
3465
+ //if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
3466
+ if ((obj.userAllowedIp != null) && (checkIpAddressEx(req, res, obj.userAllowedIp, false) === false)) { return; } // Check server-wide IP filter only.
3467
+
3468
+ var user = null;
3469
+ if (obj.args.user != null) {
3470
+ // A default user is active
3471
+ user = obj.users['user/' + domain.id + '/' + obj.args.user];
3472
+ if (!user) { parent.debug('web', 'handleTranslationsRequest: user not found.'); res.sendStatus(401); return; }
3473
+ } else {
3474
+ // Check if the user is logged and we have all required parameters
3475
+ if (!req.session || !req.session.userid) { parent.debug('web', 'handleTranslationsRequest: failed checks (2).'); res.sendStatus(401); return; }
3476
+
3477
+ // Get the current user
3478
+ user = obj.users[req.session.userid];
3479
+ if (!user) { parent.debug('web', 'handleTranslationsRequest: user not found.'); res.sendStatus(401); return; }
3480
+ if (user.siteadmin != 0xFFFFFFFF) { parent.debug('web', 'handleTranslationsRequest: user not site administrator.'); res.sendStatus(401); return; }
3481
+ }
3482
+
3483
+ var data = '';
3484
+ req.setEncoding('utf8');
3485
+ req.on('data', function (chunk) { data += chunk; });
3486
+ req.on('end', function () {
3487
+ try { data = JSON.parse(data); } catch (ex) { data = null; }
3488
+ if (data == null) { res.sendStatus(404); return; }
3489
+ if (data.action == 'getTranslations') {
3490
+ if (obj.fs.existsSync(obj.path.join(obj.parent.datapath, 'translate.json'))) {
3491
+ // Return the translation file (JSON)
3492
+ try { res.sendFile(obj.path.join(obj.parent.datapath, 'translate.json')); } catch (ex) { res.sendStatus(404); }
3493
+ } else if (obj.fs.existsSync(obj.path.join(__dirname, 'translate', 'translate.json'))) {
3494
+ // Return the default translation file (JSON)
3495
+ try { res.sendFile(obj.path.join(__dirname, 'translate', 'translate.json')); } catch (ex) { res.sendStatus(404); }
3496
+ } else { res.sendStatus(404); }
3497
+ } else if (data.action == 'setTranslations') {
3498
+ 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 })); } });
3499
+ } else if (data.action == 'translateServer') {
3500
+ if (obj.pendingTranslation === true) { res.send(JSON.stringify({ response: 'Server is already performing a translation.' })); return; }
3501
+ const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
3502
+ if (nodeVersion < 8) { res.send(JSON.stringify({ response: 'Server requires NodeJS 8.x or better.' })); return; }
3503
+ var translateFile = obj.path.join(obj.parent.datapath, 'translate.json');
3504
+ if (obj.fs.existsSync(translateFile) == false) { translateFile = obj.path.join(__dirname, 'translate', 'translate.json'); }
3505
+ if (obj.fs.existsSync(translateFile) == false) { res.send(JSON.stringify({ response: 'Unable to find translate.js file on the server.' })); return; }
3506
+ res.send(JSON.stringify({ response: 'ok' }));
3507
+ console.log('Started server translation...');
3508
+ obj.pendingTranslation = true;
3509
+ require('child_process').exec('node translate.js translateall \"' + translateFile + '\"', { maxBuffer: 512000, timeout: 120000, cwd: obj.path.join(__dirname, 'translate') }, function (error, stdout, stderr) {
3510
+ delete obj.pendingTranslation;
3511
+ //console.log('error', error);
3512
+ //console.log('stdout', stdout);
3513
+ //console.log('stderr', stderr);
3514
+ //console.log('Server restart...'); // Perform a server restart
3515
+ //process.exit(0);
3516
+ console.log('Server translation completed.');
3517
+ });
3518
+ } else {
3519
+ // Unknown request
3520
+ res.sendStatus(404);
3521
+ }
3522
+ });
3523
+ }
3524
+
3525
+ // Handle welcome image request
3526
+ function handleWelcomeImageRequest(req, res) {
3527
+ const domain = checkUserIpAddress(req, res);
3528
+ if (domain == null) { return; }
3529
+
3530
+ //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
3531
+ if (domain.welcomepicture) {
3532
+ if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.welcomepicture] != null)) {
3533
+ // Use the welcome image in the database
3534
+ res.set({ 'Content-Type': domain.welcomepicture.toLowerCase().endsWith('.png')?'image/png':'image/jpeg' });
3535
+ res.send(parent.configurationFiles[domain.welcomepicture]);
3536
+ return;
3537
+ }
3538
+
3539
+ // Use the configured logo picture
3540
+ try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.welcomepicture)); return; } catch (ex) { }
3541
+ }
3542
+
3543
+ var imagefile = 'images/mainwelcome.jpg';
3544
+ if (domain.sitestyle == 2) { imagefile = 'images/login/back.png'; }
3545
+ if (domain.webpublicpath != null) {
3546
+ obj.fs.exists(obj.path.join(domain.webpublicpath, imagefile), function (exists) {
3547
+ if (exists) {
3548
+ // Use the domain logo picture
3549
+ try { res.sendFile(obj.path.join(domain.webpublicpath, imagefile)); } catch (ex) { res.sendStatus(404); }
3550
+ } else {
3551
+ // Use the default logo picture
3552
+ try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
3553
+ }
3554
+ });
3555
+ } else if (parent.webPublicOverridePath) {
3556
+ obj.fs.exists(obj.path.join(obj.parent.webPublicOverridePath, imagefile), function (exists) {
3557
+ if (exists) {
3558
+ // Use the override logo picture
3559
+ try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, imagefile)); } catch (ex) { res.sendStatus(404); }
3560
+ } else {
3561
+ // Use the default logo picture
3562
+ try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
3563
+ }
3564
+ });
3565
+ } else {
3566
+ // Use the default logo picture
3567
+ try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
3568
+ }
3569
+ }
3570
+
3571
+ // Download a session recording
3572
+ function handleGetRecordings(req, res) {
3573
+ const domain = checkUserIpAddress(req, res);
3574
+ if (domain == null) return;
3575
+
3576
+ // Check the query
3577
+ if ((domain.sessionrecording == null) || (req.query.file == null) || (obj.common.IsFilenameValid(req.query.file) !== true) || (req.query.file.endsWith('.mcrec') == false)) { res.sendStatus(401); return; }
3578
+
3579
+ // Get the recording path
3580
+ var recordingsPath = null;
3581
+ if (domain.sessionrecording.filepath) { recordingsPath = domain.sessionrecording.filepath; } else { recordingsPath = parent.recordpath; }
3582
+ if (recordingsPath == null) { res.sendStatus(401); return; }
3583
+
3584
+ // Get the user and check user rights
3585
+ var authUserid = null;
3586
+ if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
3587
+ if (authUserid == null) { res.sendStatus(401); return; }
3588
+ const user = obj.users[authUserid];
3589
+ if (user == null) { res.sendStatus(401); return; }
3590
+ if ((user.siteadmin & 512) == 0) { res.sendStatus(401); return; } // Check if we have right to get recordings
3591
+
3592
+ // Send the recorded file
3593
+ setContentDispositionHeader(res, 'application/octet-stream', req.query.file, null, 'recording.mcrec');
3594
+ try { res.sendFile(obj.path.join(recordingsPath, req.query.file)); } catch (ex) { res.sendStatus(404); }
3595
+ }
3596
+
3597
+ // Stream a session recording
3598
+ function handleGetRecordingsWebSocket(ws, req) {
3599
+ var domain = checkAgentIpAddress(ws, req);
3600
+ if (domain == null) { parent.debug('web', 'Got recordings file transfer connection with bad domain or blocked IP address ' + req.clientIp + ', dropping.'); try { ws.close(); } catch (ex) { } return; }
3601
+
3602
+ // Check the query
3603
+ if ((domain.sessionrecording == null) || (req.query.file == null) || (obj.common.IsFilenameValid(req.query.file) !== true) || (req.query.file.endsWith('.mcrec') == false)) { try { ws.close(); } catch (ex) { } return; }
3604
+
3605
+ // Get the recording path
3606
+ var recordingsPath = null;
3607
+ if (domain.sessionrecording.filepath) { recordingsPath = domain.sessionrecording.filepath; } else { recordingsPath = parent.recordpath; }
3608
+ if (recordingsPath == null) { try { ws.close(); } catch (ex) { } return; }
3609
+
3610
+ // Get the user and check user rights
3611
+ var authUserid = null;
3612
+ if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
3613
+ if (authUserid == null) { try { ws.close(); } catch (ex) { } return; }
3614
+ const user = obj.users[authUserid];
3615
+ if (user == null) { try { ws.close(); } catch (ex) { } return; }
3616
+ if ((user.siteadmin & 512) == 0) { try { ws.close(); } catch (ex) { } return; } // Check if we have right to get recordings
3617
+ const filefullpath = obj.path.join(recordingsPath, req.query.file);
3618
+
3619
+ obj.fs.stat(filefullpath, function(err, stats) {
3620
+ if (err) {
3621
+ try { ws.close(); } catch (ex) { } // File does not exist
3622
+ } else {
3623
+ obj.fs.open(filefullpath, 'r', function (err, fd) {
3624
+ if (err == null) {
3625
+ // When data is received from the web socket
3626
+ ws.on('message', function (msg) {
3627
+ if (typeof msg != 'string') return;
3628
+ var command;
3629
+ try { command = JSON.parse(msg); } catch (e) { return; }
3630
+ if ((command == null) || (typeof command.action != 'string')) return;
3631
+ switch (command.action) {
3632
+ case 'get': {
3633
+ const buffer = Buffer.alloc(8 + command.size);
3634
+ //buffer.writeUInt32BE((command.ptr >> 32), 0);
3635
+ buffer.writeUInt32BE((command.ptr & 0xFFFFFFFF), 4);
3636
+ obj.fs.read(fd, buffer, 8, command.size, command.ptr, function (err, bytesRead, buffer) { if (bytesRead > (buffer.length - 8)) { buffer = buffer.slice(0, bytesRead + 8); } ws.send(buffer); });
3637
+ break;
3638
+ }
3639
+ }
3640
+ });
3641
+
3642
+ // If error, do nothing
3643
+ ws.on('error', function (err) { try { ws.close(); } catch (ex) { } obj.fs.close(fd, function (err) { }); });
3644
+
3645
+ // If the web socket is closed
3646
+ ws.on('close', function (req) { try { ws.close(); } catch (ex) { } obj.fs.close(fd, function (err) { }); });
3647
+
3648
+ ws.send(JSON.stringify({ "action": "info", "name": req.query.file, "size": stats.size }));
3649
+ } else {
3650
+ try { ws.close(); } catch (ex) { }
3651
+ }
3652
+ });
3653
+ }
3654
+ });
3655
+ }
3656
+
3657
+ // Serve the player page
3658
+ function handlePlayerRequest(req, res) {
3659
+ const domain = checkUserIpAddress(req, res);
3660
+ if (domain == null) { return; }
3661
+
3662
+ parent.debug('web', 'handlePlayerRequest: sending player');
3663
+ res.set({ 'Cache-Control': 'no-store' });
3664
+ render(req, res, getRenderPage('player', req, domain), getRenderArgs({}, req, domain));
3665
+ }
3666
+
3667
+ // Serve the guest sharing page
3668
+ function handleSharingRequest(req, res) {
3669
+ const domain = getDomain(req, res);
3670
+ if (domain == null) { return; }
3671
+ if (req.query.c == null) { res.sendStatus(404); return; }
3672
+ if (domain.guestdevicesharing === false) { res.sendStatus(404); return; } // This feature is not allowed.
3673
+
3674
+ // Check the inbound guest sharing cookie
3675
+ var c = obj.parent.decodeCookie(req.query.c, obj.parent.invitationLinkEncryptionKey, 9999999999); // Decode cookies with unlimited time.
3676
+ if (c == null) { res.sendStatus(404); return; }
3677
+
3678
+ if (c.a === 5) {
3679
+ // This is the older style sharing cookie with everything encoded within it.
3680
+ // This cookie style gives a very large URL, so it's not used anymore.
3681
+ if ((typeof c.p !== 'number') || (c.p < 1) || (c.p > 7) || (typeof c.uid != 'string') || (typeof c.nid != 'string') || (typeof c.gn != 'string') || (typeof c.cf != 'number') || (typeof c.pid != 'string')) { res.sendStatus(404); return; }
3682
+ handleSharingRequestEx(req, res, domain, c);
3683
+ return;
3684
+ }
3685
+ if (c.a === 6) {
3686
+ // This is the new style sharing cookie, just encodes the pointer to the sharing information in the database.
3687
+ // Gives a much more compact URL.
3688
+ if (typeof c.pid != 'string') { res.sendStatus(404); return; }
3689
+
3690
+ // Check the expired time, expire message.
3691
+ if ((c.e != null) && (c.e <= 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; }
3692
+
3693
+ obj.db.Get('deviceshare-' + c.pid, function (err, docs) {
3694
+ if ((err != null) || (docs == null) || (docs.length != 1)) { res.sendStatus(404); return; }
3695
+ const doc = docs[0];
3696
+
3697
+ // If this is a recurrent share, check if we are at the currect time to make use of it
3698
+ if (typeof doc.recurring == 'number') {
3699
+ const now = Date.now();
3700
+ if (now >= doc.startTime) { // We don't want to move the validity window before the start time
3701
+ const deltaTime = (now - doc.startTime);
3702
+ if (doc.recurring === 1) {
3703
+ // This moves the start time to the next valid daily window
3704
+ const oneDay = (24 * 60 * 60 * 1000);
3705
+ var addition = Math.floor(deltaTime / oneDay);
3706
+ if ((deltaTime - (addition * oneDay)) > (doc.duration * 60000)) { addition++; } // If we are passed the current windows, move to the next one. This will show link as not being valid yet.
3707
+ doc.startTime += (addition * oneDay);
3708
+ } else if (doc.recurring === 2) {
3709
+ // This moves the start time to the next valid weekly window
3710
+ const oneWeek = (7 * 24 * 60 * 60 * 1000);
3711
+ var addition = Math.floor(deltaTime / oneWeek);
3712
+ if ((deltaTime - (addition * oneWeek)) > (doc.duration * 60000)) { addition++; } // If we are passed the current windows, move to the next one. This will show link as not being valid yet.
3713
+ doc.startTime += (addition * oneWeek);
3714
+ }
3715
+ }
3716
+ }
3717
+
3718
+ // Generate an old style cookie from the information in the database
3719
+ var cookie = { a: 5, p: doc.p, gn: doc.guestName, nid: doc.nodeid, cf: doc.consent, pid: doc.publicid, k: doc.extrakey };
3720
+ if (doc.userid) { cookie.uid = doc.userid; }
3721
+ if ((cookie.userid == null) && (cookie.pid.startsWith('AS:node/'))) { cookie.nouser = 1; }
3722
+ if (doc.startTime != null) {
3723
+ if (doc.expireTime != null) { cookie.start = doc.startTime; cookie.expire = doc.expireTime; }
3724
+ else if (doc.duration != null) { cookie.start = doc.startTime; cookie.expire = doc.startTime + (doc.duration * 60000); }
3725
+ }
3726
+ if (doc.viewOnly === true) { cookie.vo = 1; }
3727
+ handleSharingRequestEx(req, res, domain, cookie);
3728
+ });
3729
+ return;
3730
+ }
3731
+ res.sendStatus(404); return;
3732
+ }
3733
+
3734
+ // Serve the guest sharing page
3735
+ function handleSharingRequestEx(req, res, domain, c) {
3736
+ // Check the expired time, expire message.
3737
+ if ((c.expire != null) && (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; }
3738
+
3739
+ // Check the public id
3740
+ obj.db.GetAllTypeNodeFiltered([c.nid], domain.id, 'deviceshare', null, function (err, docs) {
3741
+ // Check if any desktop sharing links are present, expire message.
3742
+ 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; }
3743
+
3744
+ // Search for the device share public identifier, expire message.
3745
+ var found = false;
3746
+ for (var i = 0; i < docs.length; i++) { if ((docs[i].publicid == c.pid) && ((docs[i].extrakey == null) || (docs[i].extrakey === c.k))) { found = true; } }
3747
+ 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; }
3748
+
3749
+ // Get information about this node
3750
+ obj.db.Get(c.nid, function (err, nodes) {
3751
+ if ((err != null) || (nodes == null) || (nodes.length != 1)) { res.sendStatus(404); return; }
3752
+ var node = nodes[0];
3753
+
3754
+ // Check the start time, not yet valid message.
3755
+ if ((c.start != null) && (c.expire != null) && ((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; }
3756
+
3757
+ // Looks good, let's create the outbound session cookies.
3758
+ // Consent flags are 1 = Notify, 8 = Prompt, 64 = Privacy Bar.
3759
+ const authCookieData = { userid: c.uid, domainid: domain.id, nid: c.nid, ip: req.clientIp, p: c.p, gn: c.gn, cf: c.cf, r: 8, expire: c.expire, pid: c.pid, vo: c.vo };
3760
+ if ((authCookieData.userid == null) && (authCookieData.pid.startsWith('AS:node/'))) { authCookieData.nouser = 1; }
3761
+ if (c.k != null) { authCookieData.k = c.k; }
3762
+ const authCookie = obj.parent.encodeCookie(authCookieData, obj.parent.loginCookieEncryptionKey);
3763
+
3764
+ // Server features
3765
+ var features2 = 0;
3766
+ if (obj.args.allowhighqualitydesktop !== false) { features2 += 1; } // Enable AllowHighQualityDesktop (Default true)
3767
+
3768
+ // Lets respond by sending out the desktop viewer.
3769
+ var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
3770
+ parent.debug('web', 'handleSharingRequest: Sending guest sharing page for \"' + c.uid + '\", guest \"' + c.gn + '\".');
3771
+ res.set({ 'Cache-Control': 'no-store' });
3772
+ render(req, res, getRenderPage('sharing', 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, nodeName: encodeURIComponent(node.name).replace(/'/g, '%27'), features: c.p, features2: features2 }, req, domain));
3773
+ });
3774
+ });
3775
+ }
3776
+
3777
+ // Handle domain redirection
3778
+ obj.handleDomainRedirect = function (req, res) {
3779
+ const domain = checkUserIpAddress(req, res);
3780
+ if (domain == null) { return; }
3781
+ if (domain.redirects == null) { res.sendStatus(404); return; }
3782
+ var urlArgs = '', urlName = null, splitUrl = req.originalUrl.split('?');
3783
+ if (splitUrl.length > 1) { urlArgs = '?' + splitUrl[1]; }
3784
+ if ((splitUrl.length > 0) && (splitUrl[0].length > 1)) { urlName = splitUrl[0].substring(1).toLowerCase(); }
3785
+ if ((urlName == null) || (domain.redirects[urlName] == null) || (urlName[0] == '_')) { res.sendStatus(404); return; }
3786
+ if (domain.redirects[urlName] == '~showversion') {
3787
+ // Show the current version
3788
+ res.end('MeshCentral v' + obj.parent.currentVer);
3789
+ } else {
3790
+ // Perform redirection
3791
+ res.redirect(domain.redirects[urlName] + urlArgs + getQueryPortion(req));
3792
+ }
3793
+ }
3794
+
3795
+ // Take a "user/domain/userid/path/file" format and return the actual server disk file path if access is allowed
3796
+ obj.getServerFilePath = function (user, domain, path) {
3797
+ var splitpath = path.split('/'), serverpath = obj.path.join(obj.filespath, 'domain'), filename = '';
3798
+ if ((splitpath.length < 3) || (splitpath[0] != 'user' && splitpath[0] != 'mesh') || (splitpath[1] != domain.id)) return null; // Basic validation
3799
+ var objid = splitpath[0] + '/' + splitpath[1] + '/' + splitpath[2];
3800
+ if (splitpath[0] == 'user' && (objid != user._id)) return null; // User validation, only self allowed
3801
+ if (splitpath[0] == 'mesh') { if ((obj.GetMeshRights(user, objid) & 32) == 0) { return null; } } // Check mesh server file rights
3802
+ if (splitpath[1] != '') { serverpath += '-' + splitpath[1]; } // Add the domain if needed
3803
+ serverpath += ('/' + splitpath[0] + '-' + splitpath[2]);
3804
+ 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
3805
+ return { fullpath: obj.path.resolve(obj.filespath, serverpath), path: serverpath, name: filename, quota: obj.getQuota(objid, domain) };
3806
+ };
3807
+
3808
+ // Return the maximum number of bytes allowed in the user account "My Files".
3809
+ obj.getQuota = function (objid, domain) {
3810
+ if (objid == null) return 0;
3811
+ if (objid.startsWith('user/')) {
3812
+ var user = obj.users[objid];
3813
+ if (user == null) return 0;
3814
+ if (user.siteadmin == 0xFFFFFFFF) return null; // Administrators have no user limit
3815
+ if ((user.quota != null) && (typeof user.quota == 'number')) { return user.quota; }
3816
+ if ((domain != null) && (domain.userquota != null) && (typeof domain.userquota == 'number')) { return domain.userquota; }
3817
+ return null; // By default, the user will have no limit
3818
+ } else if (objid.startsWith('mesh/')) {
3819
+ var mesh = obj.meshes[objid];
3820
+ if (mesh == null) return 0;
3821
+ if ((mesh.quota != null) && (typeof mesh.quota == 'number')) { return mesh.quota; }
3822
+ if ((domain != null) && (domain.meshquota != null) && (typeof domain.meshquota == 'number')) { return domain.meshquota; }
3823
+ return null; // By default, the mesh will have no limit
3824
+ }
3825
+ return 0;
3826
+ };
3827
+
3828
+ // Download a file from the server
3829
+ function handleDownloadFile(req, res) {
3830
+ const domain = checkUserIpAddress(req, res);
3831
+ if (domain == null) { return; }
3832
+ if ((req.query.link == null) || (req.session == null) || (req.session.userid == null) || (domain == null) || (domain.userQuota == -1)) { res.sendStatus(404); return; }
3833
+ const user = obj.users[req.session.userid];
3834
+ if (user == null) { res.sendStatus(404); return; }
3835
+ const file = obj.getServerFilePath(user, domain, req.query.link);
3836
+ if (file == null) { res.sendStatus(404); return; }
3837
+ setContentDispositionHeader(res, 'application/octet-stream', file.name, null, 'file.bin');
3838
+ obj.fs.exists(file.fullpath, function (exists) { if (exists == true) { res.sendFile(file.fullpath); } else { res.sendStatus(404); } });
3839
+ }
3840
+
3841
+ // Upload a MeshCore.js file to the server
3842
+ function handleUploadMeshCoreFile(req, res) {
3843
+ const domain = checkUserIpAddress(req, res);
3844
+ if (domain == null) { return; }
3845
+ if (domain.id !== '') { res.sendStatus(401); return; }
3846
+
3847
+ var authUserid = null;
3848
+ if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
3849
+
3850
+ const multiparty = require('multiparty');
3851
+ const form = new multiparty.Form();
3852
+ form.parse(req, function (err, fields, files) {
3853
+ // If an authentication cookie is embedded in the form, use that.
3854
+ if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
3855
+ var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
3856
+ if ((loginCookie != null) && (obj.args.cookieipcheck !== false) && (loginCookie.ip != null) && (loginCookie.ip != req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
3857
+ if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
3858
+ }
3859
+ if (authUserid == null) { res.sendStatus(401); return; }
3860
+ if ((fields == null) || (fields.attrib == null) || (fields.attrib.length != 1)) { res.sendStatus(404); return; }
3861
+
3862
+ // Get the user
3863
+ const user = obj.users[authUserid];
3864
+ if (user == null) { res.sendStatus(401); return; } // Check this user exists
3865
+
3866
+ // Get the node and check node rights
3867
+ const nodeid = fields.attrib[0];
3868
+ obj.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
3869
+ if ((node == null) || (rights != 0xFFFFFFFF) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device
3870
+ for (var i in files.files) {
3871
+ var file = files.files[i];
3872
+ obj.fs.readFile(file.path, 'utf8', function (err, data) {
3873
+ if (err != null) return;
3874
+ data = obj.common.IntToStr(0) + data; // Add the 4 bytes encoding type & flags (Set to 0 for raw)
3875
+ obj.sendMeshAgentCore(user, domain, fields.attrib[0], 'custom', data); // Upload the core
3876
+ try { obj.fs.unlinkSync(file.path); } catch (e) { }
3877
+ });
3878
+ }
3879
+ res.send('');
3880
+ });
3881
+ });
3882
+ }
3883
+
3884
+ // Upload a MeshCore.js file to the server
3885
+ function handleOneClickRecoveryFile(req, res) {
3886
+ const domain = checkUserIpAddress(req, res);
3887
+ if (domain == null) { return; }
3888
+ if (domain.id !== '') { res.sendStatus(401); return; }
3889
+
3890
+ var authUserid = null;
3891
+ if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
3892
+
3893
+ const multiparty = require('multiparty');
3894
+ const form = new multiparty.Form();
3895
+ form.parse(req, function (err, fields, files) {
3896
+ // If an authentication cookie is embedded in the form, use that.
3897
+ if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
3898
+ var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
3899
+ if ((loginCookie != null) && (obj.args.cookieipcheck !== false) && (loginCookie.ip != null) && (loginCookie.ip != req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
3900
+ if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
3901
+ }
3902
+ if (authUserid == null) { res.sendStatus(401); return; }
3903
+ if ((fields == null) || (fields.attrib == null) || (fields.attrib.length != 1)) { res.sendStatus(404); return; }
3904
+
3905
+ // Get the user
3906
+ const user = obj.users[authUserid];
3907
+ if (user == null) { res.sendStatus(401); return; } // Check this user exists
3908
+
3909
+ // Get the node and check node rights
3910
+ const nodeid = fields.attrib[0];
3911
+ obj.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
3912
+ if ((node == null) || (rights != 0xFFFFFFFF) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device
3913
+ for (var i in files.files) {
3914
+ var file = files.files[i];
3915
+
3916
+ // Event Intel AMT One Click Recovery, this will cause Intel AMT wake operations on this and other servers.
3917
+ parent.DispatchEvent('*', obj, { action: 'oneclickrecovery', userid: user._id, username: user.name, nodeids: [node._id], domain: domain.id, nolog: 1, file: file.path });
3918
+
3919
+ //try { obj.fs.unlinkSync(file.path); } catch (e) { } // TODO: Remove this file after 30 minutes.
3920
+ }
3921
+ res.send('');
3922
+ });
3923
+ });
3924
+ }
3925
+
3926
+ // Upload a file to the server
3927
+ function handleUploadFile(req, res) {
3928
+ const domain = checkUserIpAddress(req, res);
3929
+ if (domain == null) { return; }
3930
+ if (domain.userQuota == -1) { res.sendStatus(401); return; }
3931
+ var authUserid = null;
3932
+ if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
3933
+ const multiparty = require('multiparty');
3934
+ const form = new multiparty.Form();
3935
+ form.parse(req, function (err, fields, files) {
3936
+ // If an authentication cookie is embedded in the form, use that.
3937
+ if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
3938
+ var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
3939
+ if ((loginCookie != null) && (obj.args.cookieipcheck !== false) && (loginCookie.ip != null) && (loginCookie.ip != req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
3940
+ if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
3941
+ }
3942
+ if (authUserid == null) { res.sendStatus(401); return; }
3943
+
3944
+ // Get the user
3945
+ const user = obj.users[authUserid];
3946
+ if ((user == null) || (user.siteadmin & 8) == 0) { res.sendStatus(401); return; } // Check if we have file rights
3947
+
3948
+ if ((fields == null) || (fields.link == null) || (fields.link.length != 1)) { /*console.log('UploadFile, Invalid Fields:', fields, files);*/ console.log('err4'); res.sendStatus(404); return; }
3949
+ var xfile = null;
3950
+ try { xfile = obj.getServerFilePath(user, domain, decodeURIComponent(fields.link[0])); } catch (ex) { }
3951
+ if (xfile == null) { res.sendStatus(404); return; }
3952
+ // Get total bytes in the path
3953
+ var totalsize = readTotalFileSize(xfile.fullpath);
3954
+ if ((xfile.quota == null) || (totalsize < xfile.quota)) { // Check if the quota is not already broken
3955
+ if (fields.name != null) {
3956
+
3957
+ // See if we need to create the folder
3958
+ var domainx = 'domain';
3959
+ if (domain.id.length > 0) { domainx = 'domain-' + usersplit[1]; }
3960
+ try { obj.fs.mkdirSync(obj.parent.filespath); } catch (ex) { }
3961
+ try { obj.fs.mkdirSync(obj.parent.path.join(obj.parent.filespath, domainx)); } catch (ex) { }
3962
+ try { obj.fs.mkdirSync(xfile.fullpath); } catch (ex) { }
3963
+
3964
+ // Upload method where all the file data is within the fields.
3965
+ var names = fields.name[0].split('*'), sizes = fields.size[0].split('*'), types = fields.type[0].split('*'), datas = fields.data[0].split('*');
3966
+ if ((names.length == sizes.length) && (types.length == datas.length) && (names.length == types.length)) {
3967
+ for (var i = 0; i < names.length; i++) {
3968
+ if (obj.common.IsFilenameValid(names[i]) == false) { res.sendStatus(404); return; }
3969
+ var filedata = Buffer.from(datas[i].split(',')[1], 'base64');
3970
+ if ((xfile.quota == null) || ((totalsize + filedata.length) < xfile.quota)) { // Check if quota would not be broken if we add this file
3971
+ // Create the user folder if needed
3972
+ (function (fullpath, filename, filedata) {
3973
+ obj.fs.mkdir(xfile.fullpath, function () {
3974
+ // Write the file
3975
+ obj.fs.writeFile(obj.path.join(xfile.fullpath, filename), filedata, function () {
3976
+ obj.parent.DispatchEvent([user._id], obj, 'updatefiles'); // Fire an event causing this user to update this files
3977
+ });
3978
+ });
3979
+ })(xfile.fullpath, names[i], filedata);
3980
+ } else {
3981
+ // Send a notification
3982
+ obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: "Disk quota exceed", value: names[i], nolog: 1, id: Math.random() });
3983
+ }
3984
+ }
3985
+ }
3986
+ } else {
3987
+ // More typical upload method, the file data is in a multipart mime post.
3988
+ for (var i in files.files) {
3989
+ var file = files.files[i], fpath = obj.path.join(xfile.fullpath, file.originalFilename);
3990
+ 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
3991
+
3992
+ // See if we need to create the folder
3993
+ var domainx = 'domain';
3994
+ if (domain.id.length > 0) { domainx = 'domain-' + domain.id; }
3995
+ try { obj.fs.mkdirSync(obj.parent.filespath); } catch (e) { }
3996
+ try { obj.fs.mkdirSync(obj.parent.path.join(obj.parent.filespath, domainx)); } catch (e) { }
3997
+ try { obj.fs.mkdirSync(xfile.fullpath); } catch (e) { }
3998
+
3999
+ // Rename the file
4000
+ obj.fs.rename(file.path, fpath, function (err) {
4001
+ if (err && (err.code === 'EXDEV')) {
4002
+ // On some Linux, the rename will fail with a "EXDEV" error, do a copy+unlink instead.
4003
+ obj.common.copyFile(file.path, fpath, function (err) {
4004
+ obj.fs.unlink(file.path, function (err) {
4005
+ obj.parent.DispatchEvent([user._id], obj, 'updatefiles'); // Fire an event causing this user to update this files
4006
+ });
4007
+ });
4008
+ } else {
4009
+ obj.parent.DispatchEvent([user._id], obj, 'updatefiles'); // Fire an event causing this user to update this files
4010
+ }
4011
+ });
4012
+ } else {
4013
+ // Send a notification
4014
+ obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: "Disk quota exceed", value: file.originalFilename, nolog: 1, id: Math.random() });
4015
+ try { obj.fs.unlink(file.path, function (err) { }); } catch (e) { }
4016
+ }
4017
+ }
4018
+ }
4019
+ } else {
4020
+ // Send a notification
4021
+ obj.parent.DispatchEvent([user._id], obj, { action: 'notify', value: "Disk quota exceed", nolog: 1, id: Math.random() });
4022
+ }
4023
+ res.send('');
4024
+ });
4025
+ }
4026
+
4027
+ // Upload a file to the server and then batch upload to many agents
4028
+ function handleUploadFileBatch(req, res) {
4029
+ const domain = checkUserIpAddress(req, res);
4030
+ if (domain == null) { return; }
4031
+ var authUserid = null;
4032
+ if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
4033
+ const multiparty = require('multiparty');
4034
+ const form = new multiparty.Form();
4035
+ form.parse(req, function (err, fields, files) {
4036
+ // If an authentication cookie is embedded in the form, use that.
4037
+ if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
4038
+ var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
4039
+ if ((loginCookie != null) && (obj.args.cookieipcheck !== false) && (loginCookie.ip != null) && (loginCookie.ip != req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
4040
+ if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
4041
+ }
4042
+ if (authUserid == null) { res.sendStatus(401); return; }
4043
+
4044
+ // Get the user
4045
+ const user = obj.users[authUserid];
4046
+ if (user == null) { parent.debug('web', 'Batch upload error, invalid user.'); res.sendStatus(401); return; } // Check if user exists
4047
+
4048
+ // Get fields
4049
+ if ((fields == null) || (fields.nodeIds == null) || (fields.nodeIds.length != 1)) { res.sendStatus(404); return; }
4050
+ var cmd = { nodeids: fields.nodeIds[0].split(','), files: [], user: user, domain: domain, overwrite: false, createFolder: false };
4051
+ if ((fields.winpath != null) && (fields.winpath.length == 1)) { cmd.windowsPath = fields.winpath[0]; }
4052
+ if ((fields.linuxpath != null) && (fields.linuxpath.length == 1)) { cmd.linuxPath = fields.linuxpath[0]; }
4053
+ if ((fields.overwriteFiles != null) && (fields.overwriteFiles.length == 1) && (fields.overwriteFiles[0] == 'on')) { cmd.overwrite = true; }
4054
+ if ((fields.createFolder != null) && (fields.createFolder.length == 1) && (fields.createFolder[0] == 'on')) { cmd.createFolder = true; }
4055
+
4056
+ // Check if we have at least one target path
4057
+ if ((cmd.windowsPath == null) && (cmd.linuxPath == null)) {
4058
+ parent.debug('web', 'Batch upload error, invalid fields: ' + JSON.stringify(fields));
4059
+ res.send('');
4060
+ return;
4061
+ }
4062
+
4063
+ // Get server temporary path
4064
+ var serverpath = obj.path.join(obj.filespath, 'tmp')
4065
+ try { obj.fs.mkdirSync(obj.parent.filespath); } catch (ex) { }
4066
+ try { obj.fs.mkdirSync(serverpath); } catch (ex) { }
4067
+
4068
+ // More typical upload method, the file data is in a multipart mime post.
4069
+ for (var i in files.files) {
4070
+ var file = files.files[i], ftarget = getRandomPassword() + '-' + file.originalFilename, fpath = obj.path.join(serverpath, ftarget);
4071
+ cmd.files.push({ name: file.originalFilename, target: ftarget });
4072
+ // Rename the file
4073
+ obj.fs.rename(file.path, fpath, function (err) {
4074
+ if (err && (err.code === 'EXDEV')) {
4075
+ // On some Linux, the rename will fail with a "EXDEV" error, do a copy+unlink instead.
4076
+ obj.common.copyFile(file.path, fpath, function (err) { obj.fs.unlink(file.path, function (err) { }); });
4077
+ }
4078
+ });
4079
+ }
4080
+
4081
+ // Instruct one of more agents to download a URL to a given local drive location.
4082
+ var tlsCertHash = null;
4083
+ if ((parent.args.ignoreagenthashcheck == null) || (parent.args.ignoreagenthashcheck === false)) { // TODO: If ignoreagenthashcheck is an array of IP addresses, not sure how to handle this.
4084
+ tlsCertHash = obj.webCertificateFullHashs[cmd.domain.id];
4085
+ if (tlsCertHash != null) { tlsCertHash = Buffer.from(tlsCertHash, 'binary').toString('hex'); }
4086
+ }
4087
+ for (var i in cmd.nodeids) {
4088
+ obj.GetNodeWithRights(cmd.domain, cmd.user, cmd.nodeids[i], function (node, rights, visible) {
4089
+ if ((node == null) || ((rights & 8) == 0) || (visible == false)) return; // We don't have remote control rights to this device
4090
+ var agentPath = (((node.agent.id > 0) && (node.agent.id < 5)) || (node.agent.id == 34)) ? cmd.windowsPath : cmd.linuxPath;
4091
+ if (agentPath == null) return;
4092
+
4093
+ // Compute user consent
4094
+ var consent = 0;
4095
+ var mesh = obj.meshes[node.meshid];
4096
+ if (typeof domain.userconsentflags == 'number') { consent |= domain.userconsentflags; } // Add server required consent flags
4097
+ if ((mesh != null) && (typeof mesh.consent == 'number')) { consent |= mesh.consent; } // Add device group user consent
4098
+ if (typeof node.consent == 'number') { consent |= node.consent; } // Add node user consent
4099
+ if (typeof user.consent == 'number') { consent |= user.consent; } // Add user consent
4100
+
4101
+ // Check if we need to add consent flags because of a user group link
4102
+ if ((mesh != null) && (user.links != null) && (user.links[mesh._id] == null) && (user.links[node._id] == null)) {
4103
+ // This user does not have a direct link to the device group or device. Find all user groups the would cause the link.
4104
+ for (var i in user.links) {
4105
+ var ugrp = obj.userGroups[i];
4106
+ if ((ugrp != null) && (ugrp.consent != null) && (ugrp.links != null) && ((ugrp.links[mesh._id] != null) || (ugrp.links[node._id] != null))) {
4107
+ consent |= ugrp.consent; // Add user group consent flags
4108
+ }
4109
+ }
4110
+ }
4111
+
4112
+ // Event that this operation is being performed.
4113
+ var targets = obj.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', cmd.user._id]);
4114
+ var msgid = 103; // "Batch upload of {0} file(s) to folder {1}"
4115
+ 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 };
4116
+ parent.DispatchEvent(targets, obj, event);
4117
+
4118
+ // Send the agent commands to perform the batch upload operation
4119
+ for (var f in cmd.files) {
4120
+ if (cmd.files[f].name != null) {
4121
+ const acmd = { action: 'wget', userid: user._id, username: user.name, realname: user.realname, remoteaddr: req.clientIp, consent: consent, rights: rights, 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 };
4122
+ var agent = obj.wsagents[node._id];
4123
+ if (agent != null) { try { agent.send(JSON.stringify(acmd)); } catch (ex) { } }
4124
+ // TODO: Add support for peer servers.
4125
+ }
4126
+ }
4127
+ });
4128
+ }
4129
+
4130
+ res.send('');
4131
+ });
4132
+ }
4133
+
4134
+ // Subscribe to all events we are allowed to receive
4135
+ obj.subscribe = function (userid, target) {
4136
+ const user = obj.users[userid];
4137
+ const subscriptions = [userid, 'server-allusers'];
4138
+ if (user.siteadmin != null) {
4139
+ // Allow full site administrators of users with all events rights to see all events.
4140
+ if ((user.siteadmin == 0xFFFFFFFF) || ((user.siteadmin & 2048) != 0)) { subscriptions.push('*'); }
4141
+ else if ((user.siteadmin & 2) != 0) {
4142
+ if ((user.groups == null) || (user.groups.length == 0)) {
4143
+ // Subscribe to all user changes
4144
+ subscriptions.push('server-users');
4145
+ } else {
4146
+ // Subscribe to user changes for some groups
4147
+ for (var i in user.groups) { subscriptions.push('server-users:' + i); }
4148
+ }
4149
+ }
4150
+ }
4151
+ if (user.links != null) { for (var i in user.links) { subscriptions.push(i); } }
4152
+ obj.parent.RemoveAllEventDispatch(target);
4153
+ obj.parent.AddEventDispatch(subscriptions, target);
4154
+ return subscriptions;
4155
+ };
4156
+
4157
+ // Handle a web socket relay request
4158
+ function handleRelayWebSocket(ws, req, domain, user, cookie) {
4159
+ if (!(req.query.host)) { console.log('ERR: No host target specified'); try { ws.close(); } catch (e) { } return; } // Disconnect websocket
4160
+ parent.debug('web', 'Websocket relay connected from ' + user.name + ' for ' + req.query.host + '.');
4161
+
4162
+ try { ws._socket.setKeepAlive(true, 240000); } catch (ex) { } // Set TCP keep alive
4163
+
4164
+ // Fetch information about the target
4165
+ obj.db.Get(req.query.host, function (err, docs) {
4166
+ if (docs.length == 0) { console.log('ERR: Node not found'); try { ws.close(); } catch (e) { } return; } // Disconnect websocket
4167
+ var node = docs[0];
4168
+ if (!node.intelamt) { console.log('ERR: Not AMT node'); try { ws.close(); } catch (e) { } return; } // Disconnect websocket
4169
+
4170
+ // Check if this user has permission to manage this computer
4171
+ if ((obj.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0) { console.log('ERR: Access denied (3)'); try { ws.close(); } catch (e) { } return; }
4172
+
4173
+ // Check what connectivity is available for this node
4174
+ var state = parent.GetConnectivityState(req.query.host);
4175
+ var conn = 0;
4176
+ if (!state || state.connectivity == 0) { parent.debug('web', 'ERR: No routing possible (1)'); try { ws.close(); } catch (e) { } return; } else { conn = state.connectivity; }
4177
+
4178
+ // Check what server needs to handle this connection
4179
+ 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
4180
+ var server = obj.parent.GetRoutingServerId(req.query.host, 2); // Check for Intel CIRA connection
4181
+ if (server != null) {
4182
+ if (server.serverid != obj.parent.serverId) {
4183
+ // Do local Intel CIRA routing using a different server
4184
+ parent.debug('web', 'Route Intel AMT CIRA connection to peer server: ' + server.serverid);
4185
+ obj.parent.multiServer.createPeerRelay(ws, req, server.serverid, user);
4186
+ return;
4187
+ }
4188
+ } else {
4189
+ server = obj.parent.GetRoutingServerId(req.query.host, 4); // Check for local Intel AMT connection
4190
+ if ((server != null) && (server.serverid != obj.parent.serverId)) {
4191
+ // Do local Intel AMT routing using a different server
4192
+ parent.debug('web', 'Route Intel AMT direct connection to peer server: ' + server.serverid);
4193
+ obj.parent.multiServer.createPeerRelay(ws, req, server.serverid, user);
4194
+ return;
4195
+ }
4196
+ }
4197
+ }
4198
+
4199
+ // Setup session recording if needed
4200
+ 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
4201
+ // Check again if we need to do recording
4202
+ var record = true;
4203
+
4204
+ // Check user or device group recording
4205
+ if ((typeof domain.sessionrecording == 'object') && ((domain.sessionrecording.onlyselectedusers === true) || (domain.sessionrecording.onlyselecteddevicegroups === true))) {
4206
+ record = false;
4207
+
4208
+ // Check device group recording
4209
+ if (domain.sessionrecording.onlyselecteddevicegroups === true) {
4210
+ var mesh = obj.meshes[node.meshid];
4211
+ if ((mesh.flags != null) && ((mesh.flags & 4) != 0)) { record = true; } // Record the session
4212
+ }
4213
+
4214
+ // Check user recording
4215
+ if (domain.sessionrecording.onlyselectedusers === true) {
4216
+ if ((user.flags != null) && ((user.flags & 2) != 0)) { record = true; } // Record the session
4217
+ }
4218
+ }
4219
+
4220
+ if (record == true) {
4221
+ var now = new Date(Date.now());
4222
+ var recFilename = 'relaysession' + ((domain.id == '') ? '' : '-') + domain.id + '-' + now.getUTCFullYear() + '-' + obj.common.zeroPad(now.getUTCMonth() + 1, 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'
4223
+ var recFullFilename = null;
4224
+ if (domain.sessionrecording.filepath) {
4225
+ try { obj.fs.mkdirSync(domain.sessionrecording.filepath); } catch (e) { }
4226
+ recFullFilename = obj.path.join(domain.sessionrecording.filepath, recFilename);
4227
+ } else {
4228
+ try { obj.fs.mkdirSync(parent.recordpath); } catch (e) { }
4229
+ recFullFilename = obj.path.join(parent.recordpath, recFilename);
4230
+ }
4231
+ var fd = obj.fs.openSync(recFullFilename, 'w');
4232
+ if (fd != null) {
4233
+ // Write the recording file header
4234
+ 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() })
4235
+ recordingEntry(fd, 1, 0, firstBlock, function () { });
4236
+ ws.logfile = { fd: fd, lock: false };
4237
+ if (req.query.p == 2) { ws.send(Buffer.from(String.fromCharCode(0xF0), 'binary')); } // Intel AMT Redirection: Indicate the session is being recorded
4238
+ }
4239
+ }
4240
+ }
4241
+
4242
+ // If Intel AMT CIRA connection is available, use it
4243
+ var ciraconn = parent.mpsserver.GetConnectionToNode(req.query.host, null, false);
4244
+ if (ciraconn != null) {
4245
+ parent.debug('web', 'Opening relay CIRA channel connection to ' + req.query.host + '.');
4246
+
4247
+ // TODO: If the CIRA connection is a relay or LMS connection, we can't detect the TLS state like this.
4248
+ // Compute target port, look at the CIRA port mappings, if non-TLS is allowed, use that, if not use TLS
4249
+ var port = 16993;
4250
+ //if (node.intelamt.tls == 0) port = 16992; // DEBUG: Allow TLS flag to set TLS mode within CIRA
4251
+ if (ciraconn.tag.boundPorts.indexOf(16992) >= 0) port = 16992; // RELEASE: Always use non-TLS mode if available within CIRA
4252
+ if (req.query.p == 2) port += 2;
4253
+
4254
+ // Setup a new CIRA channel
4255
+ if ((port == 16993) || (port == 16995)) {
4256
+ // Perform TLS
4257
+ var ser = new SerialTunnel();
4258
+ var chnl = parent.mpsserver.SetupChannel(ciraconn, port);
4259
+
4260
+ // Let's chain up the TLSSocket <-> SerialTunnel <-> CIRA APF (chnl)
4261
+ // Anything that needs to be forwarded by SerialTunnel will be encapsulated by chnl write
4262
+ ser.forwardwrite = function (data) { if (data.length > 0) { chnl.write(data); } }; // TLS ---> CIRA
4263
+
4264
+ // When APF tunnel return something, update SerialTunnel buffer
4265
+ chnl.onData = function (ciraconn, data) { if (data.length > 0) { try { ser.updateBuffer(data); } catch (ex) { console.log(ex); } } }; // CIRA ---> TLS
4266
+
4267
+ // Handle CIRA tunnel state change
4268
+ chnl.onStateChange = function (ciraconn, state) {
4269
+ parent.debug('webrelay', 'Relay TLS CIRA state change', state);
4270
+ if (state == 0) { try { ws.close(); } catch (e) { } }
4271
+ if (state == 2) {
4272
+ // TLSSocket to encapsulate TLS communication, which then tunneled via SerialTunnel an then wrapped through CIRA APF
4273
+ 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 };
4274
+ if (req.query.tls1only == 1) { tlsoptions.secureProtocol = 'TLSv1_method'; }
4275
+ var tlsock = obj.tls.connect(tlsoptions, function () { parent.debug('webrelay', "CIRA Secure TLS Connection"); ws._socket.resume(); });
4276
+ tlsock.chnl = chnl;
4277
+ tlsock.setEncoding('binary');
4278
+ tlsock.on('error', function (err) { parent.debug('webrelay', "CIRA TLS Connection Error", err); });
4279
+
4280
+ // Decrypted tunnel from TLS communcation to be forwarded to websocket
4281
+ tlsock.on('data', function (data) {
4282
+ // AMT/TLS ---> WS
4283
+ if (ws.interceptor) { data = ws.interceptor.processAmtData(data); } // Run data thru interceptor
4284
+ try { ws.send(data); } catch (ex) { }
4285
+ });
4286
+
4287
+ // If TLS is on, forward it through TLSSocket
4288
+ ws.forwardclient = tlsock;
4289
+ ws.forwardclient.xtls = 1;
4290
+
4291
+ ws.forwardclient.onStateChange = function (ciraconn, state) {
4292
+ parent.debug('webrelay', 'Relay CIRA state change', state);
4293
+ if (state == 0) { try { ws.close(); } catch (e) { } }
4294
+ };
4295
+
4296
+ ws.forwardclient.onData = function (ciraconn, data) {
4297
+ // Run data thru interceptor
4298
+ if (ws.interceptor) { data = ws.interceptor.processAmtData(data); }
4299
+
4300
+ if (data.length > 0) {
4301
+ if (ws.logfile == null) {
4302
+ try { ws.send(data); } catch (e) { }
4303
+ } else {
4304
+ // Log to recording file
4305
+ recordingEntry(ws.logfile.fd, 2, 0, data, function () { try { ws.send(data); } catch (ex) { console.log(ex); } }); // TODO: Add TLS support
4306
+ }
4307
+ }
4308
+ };
4309
+
4310
+ // TODO: Flow control? (Dont' really need it with AMT, but would be nice)
4311
+ ws.forwardclient.onSendOk = function (ciraconn) { };
4312
+ }
4313
+ };
4314
+ } else {
4315
+ // Without TLS
4316
+ ws.forwardclient = parent.mpsserver.SetupChannel(ciraconn, port);
4317
+ ws.forwardclient.xtls = 0;
4318
+ ws._socket.resume();
4319
+
4320
+ ws.forwardclient.onStateChange = function (ciraconn, state) {
4321
+ parent.debug('webrelay', 'Relay CIRA state change', state);
4322
+ if (state == 0) { try { ws.close(); } catch (e) { } }
4323
+ };
4324
+
4325
+ ws.forwardclient.onData = function (ciraconn, data) {
4326
+ //parent.debug('webrelaydata', 'Relay CIRA data to WS', data.length);
4327
+
4328
+ // Run data thru interceptorp
4329
+ if (ws.interceptor) { data = ws.interceptor.processAmtData(data); }
4330
+
4331
+ //console.log('AMT --> WS', Buffer.from(data, 'binary').toString('hex'));
4332
+ if (data.length > 0) {
4333
+ if (ws.logfile == null) {
4334
+ try { ws.send(data); } catch (e) { }
4335
+ } else {
4336
+ // Log to recording file
4337
+ recordingEntry(ws.logfile.fd, 2, 0, data, function () { try { ws.send(data); } catch (ex) { console.log(ex); } });
4338
+ }
4339
+ }
4340
+ };
4341
+
4342
+ // TODO: Flow control? (Dont' really need it with AMT, but would be nice)
4343
+ ws.forwardclient.onSendOk = function (ciraconn) { };
4344
+ }
4345
+
4346
+ // When data is received from the web socket, forward the data into the associated CIRA cahnnel.
4347
+ // If the CIRA connection is pending, the CIRA channel has built-in buffering, so we are ok sending anyway.
4348
+ ws.on('message', function (data) {
4349
+ //parent.debug('webrelaydata', 'Relay WS data to CIRA', data.length);
4350
+ if (typeof data == 'string') { data = Buffer.from(data, 'binary'); }
4351
+
4352
+ // WS ---> AMT/TLS
4353
+ if (ws.interceptor) { data = ws.interceptor.processBrowserData(data); } // Run data thru interceptor
4354
+
4355
+ // Log to recording file
4356
+ if (ws.logfile == null) {
4357
+ // Forward data to the associated TCP connection.
4358
+ try { ws.forwardclient.write(data); } catch (ex) { }
4359
+ } else {
4360
+ // Log to recording file
4361
+ recordingEntry(ws.logfile.fd, 2, 2, data, function () { try { ws.forwardclient.write(data); } catch (ex) { } });
4362
+ }
4363
+ });
4364
+
4365
+ // If error, close the associated TCP connection.
4366
+ ws.on('error', function (err) {
4367
+ console.log('CIRA server websocket error from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.');
4368
+ parent.debug('webrelay', 'Websocket relay closed on error.');
4369
+
4370
+ // Websocket closed, close the CIRA channel and TLS session.
4371
+ if (ws.forwardclient) {
4372
+ if (ws.forwardclient.close) { ws.forwardclient.close(); } // NonTLS, close the CIRA channel
4373
+ if (ws.forwardclient.end) { ws.forwardclient.end(); } // TLS, close the TLS session
4374
+ if (ws.forwardclient.chnl) { ws.forwardclient.chnl.close(); } // TLS, close the CIRA channel
4375
+ delete ws.forwardclient;
4376
+ }
4377
+
4378
+ // Close the recording file
4379
+ if (ws.logfile != null) { recordingEntry(ws.logfile.fd, 3, 0, 'MeshCentralMCREC', function (fd, ws) { obj.fs.close(fd); delete ws.logfile; }, ws); }
4380
+ });
4381
+
4382
+ // If the web socket is closed, close the associated TCP connection.
4383
+ ws.on('close', function (req) {
4384
+ parent.debug('webrelay', 'Websocket relay closed.');
4385
+
4386
+ // Websocket closed, close the CIRA channel and TLS session.
4387
+ if (ws.forwardclient) {
4388
+ if (ws.forwardclient.close) { ws.forwardclient.close(); } // NonTLS, close the CIRA channel
4389
+ if (ws.forwardclient.end) { ws.forwardclient.end(); } // TLS, close the TLS session
4390
+ if (ws.forwardclient.chnl) { ws.forwardclient.chnl.close(); } // TLS, close the CIRA channel
4391
+ delete ws.forwardclient;
4392
+ }
4393
+
4394
+ // Close the recording file
4395
+ if (ws.logfile != null) { recordingEntry(ws.logfile.fd, 3, 0, 'MeshCentralMCREC', function (fd, ws) { obj.fs.close(fd); delete ws.logfile; }, ws); }
4396
+ });
4397
+
4398
+ // 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
4399
+
4400
+ // Fetch Intel AMT credentials & Setup interceptor
4401
+ if (req.query.p == 1) {
4402
+ parent.debug('webrelaydata', 'INTERCEPTOR1', { host: node.host, port: port, user: node.intelamt.user, pass: node.intelamt.pass });
4403
+ ws.interceptor = obj.interceptor.CreateHttpInterceptor({ host: node.host, port: port, user: node.intelamt.user, pass: node.intelamt.pass });
4404
+ ws.interceptor.blockAmtStorage = true;
4405
+ } else if (req.query.p == 2) {
4406
+ parent.debug('webrelaydata', 'INTERCEPTOR2', { user: node.intelamt.user, pass: node.intelamt.pass });
4407
+ ws.interceptor = obj.interceptor.CreateRedirInterceptor({ user: node.intelamt.user, pass: node.intelamt.pass });
4408
+ ws.interceptor.blockAmtStorage = true;
4409
+ }
4410
+
4411
+ return;
4412
+ }
4413
+
4414
+ // If Intel AMT direct connection is possible, option a direct socket
4415
+ if ((conn & 4) != 0) { // We got a new web socket connection, initiate a TCP connection to the target Intel AMT host/port.
4416
+ parent.debug('webrelay', 'Opening relay TCP socket connection to ' + req.query.host + '.');
4417
+
4418
+ // When data is received from the web socket, forward the data into the associated TCP connection.
4419
+ ws.on('message', function (msg) {
4420
+ //parent.debug('webrelaydata', 'TCP relay data to ' + node.host + ', ' + msg.length + ' bytes');
4421
+
4422
+ if (typeof msg == 'string') { msg = Buffer.from(msg, 'binary'); }
4423
+ if (ws.interceptor) { msg = ws.interceptor.processBrowserData(msg); } // Run data thru interceptor
4424
+
4425
+ // Log to recording file
4426
+ if (ws.logfile == null) {
4427
+ // Forward data to the associated TCP connection.
4428
+ try { ws.forwardclient.write(msg); } catch (ex) { }
4429
+ } else {
4430
+ // Log to recording file
4431
+ recordingEntry(ws.logfile.fd, 2, 2, msg, function () { try { ws.forwardclient.write(msg); } catch (ex) { } });
4432
+ }
4433
+ });
4434
+
4435
+ // If error, close the associated TCP connection.
4436
+ ws.on('error', function (err) {
4437
+ console.log('Error with relay web socket connection from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.');
4438
+ parent.debug('webrelay', 'Error with relay web socket connection from ' + req.clientIp + '.');
4439
+ if (ws.forwardclient) { try { ws.forwardclient.destroy(); } catch (e) { } }
4440
+
4441
+ // Close the recording file
4442
+ if (ws.logfile != null) {
4443
+ recordingEntry(ws.logfile.fd, 3, 0, 'MeshCentralMCREC', function (fd) {
4444
+ obj.fs.close(fd);
4445
+ ws.logfile = null;
4446
+ });
4447
+ }
4448
+ });
4449
+
4450
+ // If the web socket is closed, close the associated TCP connection.
4451
+ ws.on('close', function () {
4452
+ parent.debug('webrelay', 'Closing relay web socket connection to ' + req.query.host + '.');
4453
+ if (ws.forwardclient) { try { ws.forwardclient.destroy(); } catch (e) { } }
4454
+
4455
+ // Close the recording file
4456
+ if (ws.logfile != null) {
4457
+ recordingEntry(ws.logfile.fd, 3, 0, 'MeshCentralMCREC', function (fd) {
4458
+ obj.fs.close(fd);
4459
+ ws.logfile = null;
4460
+ });
4461
+ }
4462
+ });
4463
+
4464
+ // Compute target port
4465
+ var port = 16992;
4466
+ if (node.intelamt.tls > 0) port = 16993; // This is a direct connection, use TLS when possible
4467
+ if ((req.query.p == 2) || (req.query.p == 4)) port += 2;
4468
+
4469
+ if (node.intelamt.tls == 0) {
4470
+ // If this is TCP (without TLS) set a normal TCP socket
4471
+ ws.forwardclient = new obj.net.Socket();
4472
+ ws.forwardclient.setEncoding('binary');
4473
+ ws.forwardclient.xstate = 0;
4474
+ ws.forwardclient.forwardwsocket = ws;
4475
+ ws._socket.resume();
4476
+ } else {
4477
+ // If TLS is going to be used, setup a TLS socket
4478
+ 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 };
4479
+ if (req.query.tls1only == 1) { tlsoptions.secureProtocol = 'TLSv1_method'; }
4480
+ ws.forwardclient = obj.tls.connect(port, node.host, tlsoptions, function () {
4481
+ // The TLS connection method is the same as TCP, but located a bit differently.
4482
+ parent.debug('webrelay', 'TLS connected to ' + node.host + ':' + port + '.');
4483
+ ws.forwardclient.xstate = 1;
4484
+ ws._socket.resume();
4485
+ });
4486
+ ws.forwardclient.setEncoding('binary');
4487
+ ws.forwardclient.xstate = 0;
4488
+ ws.forwardclient.forwardwsocket = ws;
4489
+ }
4490
+
4491
+ // When we receive data on the TCP connection, forward it back into the web socket connection.
4492
+ ws.forwardclient.on('data', function (data) {
4493
+ if (typeof data == 'string') { data = Buffer.from(data, 'binary'); }
4494
+ if (obj.parent.debugLevel >= 1) { // DEBUG
4495
+ parent.debug('webrelaydata', 'TCP relay data from ' + node.host + ', ' + data.length + ' bytes.');
4496
+ //if (obj.parent.debugLevel >= 4) { Debug(4, ' ' + Buffer.from(data, 'binary').toString('hex')); }
4497
+ }
4498
+ if (ws.interceptor) { data = ws.interceptor.processAmtData(data); } // Run data thru interceptor
4499
+ if (ws.logfile == null) {
4500
+ // No logging
4501
+ try { ws.send(data); } catch (e) { }
4502
+ } else {
4503
+ // Log to recording file
4504
+ recordingEntry(ws.logfile.fd, 2, 0, data, function () { try { ws.send(data); } catch (e) { } });
4505
+ }
4506
+ });
4507
+
4508
+ // If the TCP connection closes, disconnect the associated web socket.
4509
+ ws.forwardclient.on('close', function () {
4510
+ parent.debug('webrelay', 'TCP relay disconnected from ' + node.host + ':' + port + '.');
4511
+ try { ws.close(); } catch (e) { }
4512
+ });
4513
+
4514
+ // If the TCP connection causes an error, disconnect the associated web socket.
4515
+ ws.forwardclient.on('error', function (err) {
4516
+ parent.debug('webrelay', 'TCP relay error from ' + node.host + ':' + port + ': ' + err);
4517
+ try { ws.close(); } catch (e) { }
4518
+ });
4519
+
4520
+ // Fetch Intel AMT credentials & Setup interceptor
4521
+ if (req.query.p == 1) { ws.interceptor = obj.interceptor.CreateHttpInterceptor({ host: node.host, port: port, user: node.intelamt.user, pass: node.intelamt.pass }); }
4522
+ else if (req.query.p == 2) { ws.interceptor = obj.interceptor.CreateRedirInterceptor({ user: node.intelamt.user, pass: node.intelamt.pass }); }
4523
+
4524
+ if (node.intelamt.tls == 0) {
4525
+ // A TCP connection to Intel AMT just connected, start forwarding.
4526
+ ws.forwardclient.connect(port, node.host, function () {
4527
+ parent.debug('webrelay', 'TCP relay connected to ' + node.host + ':' + port + '.');
4528
+ ws.forwardclient.xstate = 1;
4529
+ ws._socket.resume();
4530
+ });
4531
+ }
4532
+ return;
4533
+ }
4534
+
4535
+ });
4536
+ }
4537
+
4538
+ // Setup agent to/from server file transfer handler
4539
+ function handleAgentFileTransfer(ws, req) {
4540
+ var domain = checkAgentIpAddress(ws, req);
4541
+ if (domain == null) { parent.debug('web', 'Got agent file transfer connection with bad domain or blocked IP address ' + req.clientIp + ', dropping.'); ws.close(); return; }
4542
+ if (req.query.c == null) { parent.debug('web', 'Got agent file transfer connection without a cookie from ' + req.clientIp + ', dropping.'); ws.close(); return; }
4543
+ var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 10); // 10 minute timeout
4544
+ if ((c == null) || (c.a != 'aft')) { parent.debug('web', 'Got agent file transfer connection with invalid cookie from ' + req.clientIp + ', dropping.'); ws.close(); return; }
4545
+ ws.xcmd = c.b; ws.xarg = c.c, ws.xfilelen = 0;
4546
+ ws.send('c'); // Indicate connection of the tunnel. In this case, we are the termination point.
4547
+ ws.send('5'); // Indicate we want to perform file transfers (5 = Files).
4548
+ if (ws.xcmd == 'coredump') {
4549
+ // Check the agent core dump folder if not already present.
4550
+ var coreDumpPath = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps');
4551
+ if (obj.fs.existsSync(coreDumpPath) == false) { try { obj.fs.mkdirSync(coreDumpPath); } catch (ex) { } }
4552
+ ws.xfilepath = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps', ws.xarg);
4553
+ ws.xid = 'coredump';
4554
+ ws.send(JSON.stringify({ action: 'download', sub: 'start', ask: 'coredump', id: 'coredump' })); // Ask for a core dump file
4555
+ }
4556
+
4557
+ // When data is received from the web socket, echo it back
4558
+ ws.on('message', function (data) {
4559
+ if (typeof data == 'string') {
4560
+ // Control message
4561
+ var cmd = null;
4562
+ try { cmd = JSON.parse(data); } catch (ex) { }
4563
+ if ((cmd == null) || (cmd.action != 'download') || (cmd.sub == null)) return;
4564
+ switch (cmd.sub) {
4565
+ case 'start': {
4566
+ // Perform an async file open
4567
+ var callback = function onFileOpen(err, fd) {
4568
+ onFileOpen.xws.xfile = fd;
4569
+ try { onFileOpen.xws.send(JSON.stringify({ action: 'download', sub: 'startack', id: onFileOpen.xws.xid, ack: 1 })); } catch (ex) { } // Ask for a directory (test)
4570
+ };
4571
+ callback.xws = this;
4572
+ obj.fs.open(this.xfilepath + '.part', 'w', callback);
4573
+ break;
4574
+ }
4575
+ }
4576
+ } else {
4577
+ // Binary message
4578
+ if (data.length < 4) return;
4579
+ var flags = data.readInt32BE(0);
4580
+ if ((data.length > 4)) {
4581
+ // Write the file
4582
+ this.xfilelen += (data.length - 4);
4583
+ try {
4584
+ var callback = function onFileDataWritten(err, bytesWritten, buffer) {
4585
+ if (onFileDataWritten.xflags & 1) {
4586
+ // End of file
4587
+ parent.debug('web', "Completed downloads of agent dumpfile, " + onFileDataWritten.xws.xfilelen + " bytes.");
4588
+ if (onFileDataWritten.xws.xfile) {
4589
+ obj.fs.close(onFileDataWritten.xws.xfile, function (err) { });
4590
+ obj.fs.rename(onFileDataWritten.xws.xfilepath + '.part', onFileDataWritten.xws.xfilepath, function (err) { });
4591
+ onFileDataWritten.xws.xfile = null;
4592
+ }
4593
+ try { onFileDataWritten.xws.send(JSON.stringify({ action: 'markcoredump' })); } catch (ex) { } // Ask to delete the core dump file
4594
+ try { onFileDataWritten.xws.close(); } catch (ex) { }
4595
+ } else {
4596
+ // Send ack
4597
+ try { onFileDataWritten.xws.send(JSON.stringify({ action: 'download', sub: 'ack', id: onFileDataWritten.xws.xid })); } catch (ex) { } // Ask for a directory (test)
4598
+ }
4599
+ };
4600
+ callback.xws = this;
4601
+ callback.xflags = flags;
4602
+ obj.fs.write(this.xfile, data, 4, data.length - 4, callback);
4603
+ } catch (ex) { }
4604
+ } else {
4605
+ if (flags & 1) {
4606
+ // End of file
4607
+ parent.debug('web', "Completed downloads of agent dumpfile, " + this.xfilelen + " bytes.");
4608
+ if (this.xfile) {
4609
+ obj.fs.close(this.xfile, function (err) { });
4610
+ obj.fs.rename(this.xfilepath + '.part', this.xfilepath, function (err) { });
4611
+ this.xfile = null;
4612
+ }
4613
+ this.send(JSON.stringify({ action: 'markcoredump' })); // Ask to delete the core dump file
4614
+ try { this.close(); } catch (ex) { }
4615
+ } else {
4616
+ // Send ack
4617
+ this.send(JSON.stringify({ action: 'download', sub: 'ack', id: this.xid })); // Ask for a directory (test)
4618
+ }
4619
+ }
4620
+ }
4621
+ });
4622
+
4623
+ // If error, do nothing.
4624
+ ws.on('error', function (err) { console.log('Agent file transfer server error from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.'); });
4625
+
4626
+ // If closed, do nothing
4627
+ ws.on('close', function (req) {
4628
+ if (this.xfile) {
4629
+ obj.fs.close(this.xfile, function (err) { });
4630
+ obj.fs.unlink(this.xfilepath + '.part', function (err) { }); // Remove a partial file
4631
+ }
4632
+ });
4633
+ }
4634
+
4635
+ // Handle the web socket echo request, just echo back the data sent
4636
+ function handleEchoWebSocket(ws, req) {
4637
+ const domain = checkUserIpAddress(ws, req);
4638
+ if (domain == null) { return; }
4639
+ ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive
4640
+
4641
+ // When data is received from the web socket, echo it back
4642
+ ws.on('message', function (data) {
4643
+ if (data.toString('utf8') == 'close') {
4644
+ try { ws.close(); } catch (e) { console.log(e); }
4645
+ } else {
4646
+ try { ws.send(data); } catch (e) { console.log(e); }
4647
+ }
4648
+ });
4649
+
4650
+ // If error, do nothing.
4651
+ ws.on('error', function (err) { console.log('Echo server error from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.'); });
4652
+
4653
+ // If closed, do nothing
4654
+ ws.on('close', function (req) { });
4655
+ }
4656
+
4657
+ // Handle the 2FA hold web socket
4658
+ // Accept an hold a web socket connection until the 2FA response is received.
4659
+ function handle2faHoldWebSocket(ws, req) {
4660
+ const domain = checkUserIpAddress(ws, req);
4661
+ if (domain == null) { return; }
4662
+ if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.push2factor == false)) { ws.close(); return; } // Push 2FA is disabled
4663
+ if (typeof req.query.c !== 'string') { ws.close(); return; }
4664
+ const cookie = parent.decodeCookie(req.query.c, null, 1);
4665
+ if ((cookie == null) || (cookie.d != domain.id)) { ws.close(); return; }
4666
+ var user = obj.users[cookie.u];
4667
+ if ((user == null) || (typeof user.otpdev != 'string')) { ws.close(); return; }
4668
+ ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive
4669
+
4670
+ // 2FA event subscription
4671
+ obj.parent.AddEventDispatch(['2fadev-' + cookie.s], ws);
4672
+ ws.cookie = cookie;
4673
+ ws.HandleEvent = function (source, event, ids, id) {
4674
+ obj.parent.RemoveAllEventDispatch(this);
4675
+ if ((event.approved === true) && (event.userid == this.cookie.u)) {
4676
+ // Create a login cookie
4677
+ const loginCookie = obj.parent.encodeCookie({ a: 'pushAuth', u: event.userid, d: event.domain }, obj.parent.loginCookieEncryptionKey);
4678
+ try { ws.send(JSON.stringify({ approved: true, token: loginCookie })); } catch (ex) { }
4679
+ } else {
4680
+ // Reject the login
4681
+ try { ws.send(JSON.stringify({ approved: false })); } catch (ex) { }
4682
+ }
4683
+ }
4684
+
4685
+ // We do not accept any data on this connection.
4686
+ ws.on('message', function (data) { this.close(); });
4687
+
4688
+ // If error, do nothing.
4689
+ ws.on('error', function (err) { });
4690
+
4691
+ // If closed, unsubscribe
4692
+ ws.on('close', function (req) { obj.parent.RemoveAllEventDispatch(this); });
4693
+
4694
+ // Perform push notification to device
4695
+ try {
4696
+ const deviceCookie = parent.encodeCookie({ a: 'checkAuth', c: cookie.c, u: cookie.u, n: cookie.n, s: cookie.s });
4697
+ var code = Buffer.from(cookie.c, 'base64').toString();
4698
+ var payload = { notification: { title: (domain.title ? domain.title : 'MeshCentral'), body: "Authentication - " + code }, data: { url: '2fa://auth?code=' + cookie.c + '&c=' + deviceCookie } };
4699
+ var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute
4700
+ parent.firebase.sendToDevice(user.otpdev, payload, options, function (id, err, errdesc) {
4701
+ if (err == null) {
4702
+ try { ws.send(JSON.stringify({ sent: true, code: code })); } catch (ex) { }
4703
+ } else {
4704
+ try { ws.send(JSON.stringify({ sent: false })); } catch (ex) { }
4705
+ }
4706
+ });
4707
+ } catch (ex) { console.log(ex); }
4708
+ }
4709
+
4710
+ // Get the total size of all files in a folder and all sub-folders. (TODO: try to make all async version)
4711
+ function readTotalFileSize(path) {
4712
+ var r = 0, dir;
4713
+ try { dir = obj.fs.readdirSync(path); } catch (e) { return 0; }
4714
+ for (var i in dir) {
4715
+ var stat = obj.fs.statSync(path + '/' + dir[i]);
4716
+ if ((stat.mode & 0x004000) == 0) { r += stat.size; } else { r += readTotalFileSize(path + '/' + dir[i]); }
4717
+ }
4718
+ return r;
4719
+ }
4720
+
4721
+ // Delete a folder and all sub items. (TODO: try to make all async version)
4722
+ function deleteFolderRec(path) {
4723
+ if (obj.fs.existsSync(path) == false) return;
4724
+ try {
4725
+ obj.fs.readdirSync(path).forEach(function (file, index) {
4726
+ var pathx = path + '/' + file;
4727
+ if (obj.fs.lstatSync(pathx).isDirectory()) { deleteFolderRec(pathx); } else { obj.fs.unlinkSync(pathx); }
4728
+ });
4729
+ obj.fs.rmdirSync(path);
4730
+ } catch (ex) { }
4731
+ }
4732
+
4733
+ // Handle Intel AMT events
4734
+ // To subscribe, add "http://server:port/amtevents.ashx" to Intel AMT subscriptions.
4735
+ obj.handleAmtEventRequest = function (req, res) {
4736
+ const domain = getDomain(req);
4737
+ try {
4738
+ if (req.headers.authorization) {
4739
+ var authstr = req.headers.authorization;
4740
+ if (authstr.substring(0, 7) == 'Digest ') {
4741
+ var auth = obj.common.parseNameValueList(obj.common.quoteSplit(authstr.substring(7)));
4742
+ if ((req.url === auth.uri) && (obj.httpAuthRealm === auth.realm) && (auth.opaque === obj.crypto.createHmac('SHA384', obj.httpAuthRandom).update(auth.nonce).digest('hex'))) {
4743
+
4744
+ // Read the data, we need to get the arg field
4745
+ var eventData = '';
4746
+ req.on('data', function (chunk) { eventData += chunk; });
4747
+ req.on('end', function () {
4748
+
4749
+ // Completed event read, let get the argument that must contain the nodeid
4750
+ var i = eventData.indexOf('<m:arg xmlns:m="http://x.com">');
4751
+ if (i > 0) {
4752
+ var nodeid = eventData.substring(i + 30, i + 30 + 64);
4753
+ if (nodeid.length == 64) {
4754
+ var nodekey = 'node/' + domain.id + '/' + nodeid;
4755
+
4756
+ // See if this node exists in the database
4757
+ obj.db.Get(nodekey, function (err, nodes) {
4758
+ if (nodes.length == 1) {
4759
+ // Yes, the node exists, compute Intel AMT digest password
4760
+ var node = nodes[0];
4761
+ 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');
4762
+
4763
+ // Check the MD5 hash
4764
+ if (auth.response === obj.common.ComputeDigesthash(auth.username, amtpass, auth.realm, 'POST', auth.uri, auth.qop, auth.nonce, auth.nc, auth.cnonce)) {
4765
+
4766
+ // This is an authenticated Intel AMT event, update the host address
4767
+ var amthost = req.clientIp;
4768
+ if (amthost.substring(0, 7) === '::ffff:') { amthost = amthost.substring(7); }
4769
+ if (node.host != amthost) {
4770
+ // Get the mesh for this device
4771
+ var mesh = obj.meshes[node.meshid];
4772
+ if (mesh) {
4773
+ // Update the database
4774
+ var oldname = node.host;
4775
+ node.host = amthost;
4776
+ obj.db.Set(obj.cleanDevice(node));
4777
+
4778
+ // Event the node change
4779
+ 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 };
4780
+
4781
+ // Remove the Intel AMT password before eventing this.
4782
+ event.node = node;
4783
+ if (event.node.intelamt && event.node.intelamt.pass) {
4784
+ event.node = Object.assign({}, event.node); // Shallow clone
4785
+ event.node.intelamt = Object.assign({}, event.node.intelamt); // Shallow clone
4786
+ delete event.node.intelamt.pass;
4787
+ }
4788
+
4789
+ 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.
4790
+ obj.parent.DispatchEvent(['*', node.meshid], obj, event);
4791
+ }
4792
+ }
4793
+
4794
+ if (parent.amtEventHandler) { parent.amtEventHandler.handleAmtEvent(eventData, nodeid, amthost); }
4795
+ //res.send('OK');
4796
+
4797
+ return;
4798
+ }
4799
+ }
4800
+ });
4801
+ }
4802
+ }
4803
+ });
4804
+ }
4805
+ }
4806
+ }
4807
+ } catch (e) { console.log(e); }
4808
+
4809
+ // Send authentication response
4810
+ obj.crypto.randomBytes(48, function (err, buf) {
4811
+ var nonce = buf.toString('hex'), opaque = obj.crypto.createHmac('SHA384', obj.httpAuthRandom).update(nonce).digest('hex');
4812
+ res.set({ 'WWW-Authenticate': 'Digest realm="' + obj.httpAuthRealm + '", qop="auth,auth-int", nonce="' + nonce + '", opaque="' + opaque + '"' });
4813
+ res.sendStatus(401);
4814
+ });
4815
+ };
4816
+
4817
+ // Handle a server backup request
4818
+ function handleBackupRequest(req, res) {
4819
+ const domain = checkUserIpAddress(req, res);
4820
+ if (domain == null) { return; }
4821
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
4822
+ if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
4823
+ if ((domain.myserver === false) || ((domain.myserver != null) && (domain.myserver.backup !== true))) { res.sendStatus(401); return; }
4824
+
4825
+ var user = obj.users[req.session.userid];
4826
+ if ((user == null) || ((user.siteadmin & 1) == 0)) { res.sendStatus(401); return; } // Check if we have server backup rights
4827
+
4828
+ // Require modules
4829
+ const archive = require('archiver')('zip', { level: 9 }); // Sets the compression method to maximum.
4830
+
4831
+ // Good practice to catch this error explicitly
4832
+ archive.on('error', function (err) { throw err; });
4833
+
4834
+ // Set the archive name
4835
+ res.attachment((domain.title ? domain.title : 'MeshCentral') + '-Backup-' + new Date().toLocaleDateString().replace('/', '-').replace('/', '-') + '.zip');
4836
+
4837
+ // Pipe archive data to the file
4838
+ archive.pipe(res);
4839
+
4840
+ // Append files from a glob pattern
4841
+ archive.directory(obj.parent.datapath, false);
4842
+
4843
+ // Finalize the archive (ie we are done appending files but streams have to finish yet)
4844
+ archive.finalize();
4845
+ }
4846
+
4847
+ // Handle a server restore request
4848
+ function handleRestoreRequest(req, res) {
4849
+ const domain = checkUserIpAddress(req, res);
4850
+ if (domain == null) { return; }
4851
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
4852
+ if ((domain.myserver === false) || ((domain.myserver != null) && (domain.myserver.restore !== true))) { res.sendStatus(401); return; }
4853
+
4854
+ var authUserid = null;
4855
+ if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
4856
+ const multiparty = require('multiparty');
4857
+ const form = new multiparty.Form();
4858
+ form.parse(req, function (err, fields, files) {
4859
+ // If an authentication cookie is embedded in the form, use that.
4860
+ if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
4861
+ var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
4862
+ if ((loginCookie != null) && (obj.args.cookieipcheck !== false) && (loginCookie.ip != null) && (loginCookie.ip != req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
4863
+ if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
4864
+ }
4865
+ if (authUserid == null) { res.sendStatus(401); return; }
4866
+
4867
+ // Get the user
4868
+ const user = obj.users[req.session.userid];
4869
+ if ((user == null) || ((user.siteadmin & 4) == 0)) { res.sendStatus(401); return; } // Check if we have server restore rights
4870
+
4871
+ res.set('Content-Type', 'text/html');
4872
+ res.end('<html><body>Server must be restarted, <a href="' + domain.url + '">click here to login</a>.</body></html>');
4873
+ parent.Stop(files.datafile[0].path);
4874
+ });
4875
+ }
4876
+
4877
+ // Handle a request to download a mesh agent
4878
+ obj.handleMeshAgentRequest = function (req, res) {
4879
+ var domain = getDomain(req, res);
4880
+ if (domain == null) { parent.debug('web', 'handleRootRequest: invalid domain.'); try { res.sendStatus(404); } catch (ex) { } return; }
4881
+
4882
+ // If required, check if this user has rights to do this
4883
+ if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true)) && (req.session.userid == null)) { res.sendStatus(401); return; }
4884
+
4885
+ if ((req.query.meshinstall != null) && (req.query.id != null)) {
4886
+ if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { try { res.sendStatus(404); } catch (ex) { } return; } // Check 3FA URL key
4887
+
4888
+ // Send meshagent with included self installer for a specific platform back
4889
+ // Start by getting the .msh for this request
4890
+ var meshsettings = getMshFromRequest(req, res, domain);
4891
+ if (meshsettings == null) { try { res.sendStatus(401); } catch (ex) { } return; }
4892
+
4893
+ // Get the interactive install script, this only works for non-Windows agents
4894
+ var agentid = parseInt(req.query.meshinstall);
4895
+ var argentInfo = obj.parent.meshAgentBinaries[agentid];
4896
+ if (domain.meshAgentBinaries && domain.meshAgentBinaries[agentid]) { argentInfo = domain.meshAgentBinaries[agentid]; }
4897
+ var scriptInfo = obj.parent.meshAgentInstallScripts[6];
4898
+ if ((argentInfo == null) || (scriptInfo == null) || (argentInfo.platform == 'win32')) { try { res.sendStatus(404); } catch (ex) { } return; }
4899
+
4900
+ // Change the .msh file into JSON format and merge it into the install script
4901
+ var tokens, msh = {}, meshsettingslines = meshsettings.split('\r').join('').split('\n');
4902
+ for (var i in meshsettingslines) { tokens = meshsettingslines[i].split('='); if (tokens.length == 2) { msh[tokens[0]] = tokens[1]; } }
4903
+ var js = scriptInfo.data.replace('var msh = {};', 'var msh = ' + JSON.stringify(msh) + ';');
4904
+
4905
+ // Get the agent filename
4906
+ var meshagentFilename = 'meshagent';
4907
+ if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) { meshagentFilename = domain.agentcustomization.filename; }
4908
+
4909
+ setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename, null, 'meshagent');
4910
+ if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
4911
+ res.statusCode = 200;
4912
+ obj.parent.exeHandler.streamExeWithJavaScript({ platform: argentInfo.platform, sourceFileName: argentInfo.path, destinationStream: res, js: Buffer.from(js, 'utf8'), peinfo: argentInfo.pe });
4913
+ } else if (req.query.id != null) {
4914
+ // Send a specific mesh agent back
4915
+ var argentInfo = obj.parent.meshAgentBinaries[req.query.id];
4916
+ if (domain.meshAgentBinaries && domain.meshAgentBinaries[req.query.id]) { argentInfo = domain.meshAgentBinaries[req.query.id]; }
4917
+ if (argentInfo == null) { try { res.sendStatus(404); } catch (ex) { } return; }
4918
+
4919
+ // Download PDB debug files, only allowed for administrator or accounts with agent dump access
4920
+ if (req.query.pdb == 1) {
4921
+ if ((req.session == null) || (req.session.userid == null)) { try { res.sendStatus(404); } catch (ex) { } return; }
4922
+ var user = obj.users[req.session.userid];
4923
+ if (user == null) { try { res.sendStatus(404); } catch (ex) { } return; }
4924
+ if ((user != null) && ((user.siteadmin == 0xFFFFFFFF) || ((Array.isArray(obj.parent.config.settings.agentcoredumpusers)) && (obj.parent.config.settings.agentcoredumpusers.indexOf(user._id) >= 0)))) {
4925
+ if (argentInfo.id == 3) {
4926
+ setContentDispositionHeader(res, 'application/octet-stream', 'MeshService.pdb', null, 'MeshService.pdb');
4927
+ if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
4928
+ try { res.sendFile(argentInfo.path.split('MeshService-signed.exe').join('MeshService.pdb')); } catch (ex) { }
4929
+ return;
4930
+ }
4931
+ if (argentInfo.id == 4) {
4932
+ setContentDispositionHeader(res, 'application/octet-stream', 'MeshService64.pdb', null, 'MeshService64.pdb');
4933
+ if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
4934
+ try { res.sendFile(argentInfo.path.split('MeshService64-signed.exe').join('MeshService64.pdb')); } catch (ex) { }
4935
+ return;
4936
+ }
4937
+ }
4938
+ try { res.sendStatus(404); } catch (ex) { }
4939
+ return;
4940
+ }
4941
+
4942
+ if ((req.query.meshid == null) || (argentInfo.platform != 'win32')) {
4943
+ // Get the agent filename
4944
+ var meshagentFilename = argentInfo.rname;
4945
+ if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) { meshagentFilename = domain.agentcustomization.filename; }
4946
+ if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
4947
+ if (req.query.zip == 1) { if (argentInfo.zdata != null) { setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename + '.zip', null, 'meshagent.zip'); res.send(argentInfo.zdata); } else { try { res.sendStatus(404); } catch (ex) { } } return; } // Send compressed agent
4948
+ setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename, null, 'meshagent');
4949
+ if (argentInfo.data == null) { res.sendFile(argentInfo.path); } else { res.send(argentInfo.data); }
4950
+ return;
4951
+ } else {
4952
+ // Check if the meshid is a time limited, encrypted cookie
4953
+ var meshcookie = obj.parent.decodeCookie(req.query.meshid, obj.parent.invitationLinkEncryptionKey);
4954
+ if ((meshcookie != null) && (meshcookie.m != null)) { req.query.meshid = meshcookie.m; }
4955
+
4956
+ // We are going to embed the .msh file into the Windows executable (signed or not).
4957
+ // First, fetch the mesh object to build the .msh file
4958
+ var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.meshid];
4959
+ if (mesh == null) { try { res.sendStatus(401); } catch (ex) { } return; }
4960
+
4961
+ // If required, check if this user has rights to do this
4962
+ if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true))) {
4963
+ if ((domain.id != mesh.domain) || ((obj.GetMeshRights(req.session.userid, mesh) & 1) == 0)) { try { res.sendStatus(401); } catch (ex) { } return; }
4964
+ }
4965
+
4966
+ var meshidhex = Buffer.from(req.query.meshid.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
4967
+ var serveridhex = Buffer.from(obj.agentCertificateHashBase64.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
4968
+ var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port if specified
4969
+ if (obj.args.agentport != null) { httpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
4970
+ if (obj.args.agentaliasport != null) { httpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
4971
+
4972
+ // Prepare a mesh agent file name using the device group name.
4973
+ var meshfilename = mesh.name
4974
+ meshfilename = meshfilename.split('\\').join('').split('/').join('').split(':').join('').split('*').join('').split('?').join('').split('"').join('').split('<').join('').split('>').join('').split('|').join('').split(' ').join('').split('\'').join('');
4975
+ if (argentInfo.rname.endsWith('.exe')) { meshfilename = argentInfo.rname.substring(0, argentInfo.rname.length - 4) + '-' + meshfilename + '.exe'; } else { meshfilename = argentInfo.rname + '-' + meshfilename; }
4976
+
4977
+ // Customize the mesh agent file name
4978
+ if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) {
4979
+ meshfilename = meshfilename.split('meshagent').join(domain.agentcustomization.filename).split('MeshAgent').join(domain.agentcustomization.filename);
4980
+ }
4981
+
4982
+ // Get the agent connection server name
4983
+ var serverName = obj.getWebServerName(domain);
4984
+ if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
4985
+
4986
+ // Build the agent connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
4987
+ var xdomain = (domain.dns == null) ? domain.id : '';
4988
+ if (xdomain != '') xdomain += '/';
4989
+ var meshsettings = '\r\nMeshName=' + mesh.name + '\r\nMeshType=' + mesh.mtype + '\r\nMeshID=0x' + meshidhex + '\r\nServerID=' + serveridhex + '\r\n';
4990
+ if (obj.args.lanonly != true) { meshsettings += 'MeshServer=wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'agent.ashx\r\n'; } else {
4991
+ meshsettings += 'MeshServer=local\r\n';
4992
+ 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'; }
4993
+ }
4994
+ if ((req.query.tag != null) && (typeof req.query.tag == 'string') && (obj.common.isAlphaNumeric(req.query.tag) == true)) { meshsettings += 'Tag=' + req.query.tag + '\r\n'; }
4995
+ if ((req.query.installflags != null) && (req.query.installflags != 0) && (parseInt(req.query.installflags) == req.query.installflags)) { meshsettings += 'InstallFlags=' + parseInt(req.query.installflags) + '\r\n'; }
4996
+ if (req.query.id == '10006') { // Assistant settings and customizations
4997
+ if ((req.query.ac != null)) { meshsettings += 'AutoConnect=' + req.query.ac + '\r\n'; } // Set MeshCentral Assistant flags if needed. 0x01 = Always Connected, 0x02 = Not System Tray
4998
+ if (obj.args.assistantconfig) { for (var i in obj.args.assistantconfig) { meshsettings += obj.args.assistantconfig[i] + '\r\n'; } }
4999
+ if (domain.assistantconfig) { for (var i in domain.assistantconfig) { meshsettings += domain.assistantconfig[i] + '\r\n'; } }
This file is too large to show in full.
webserver.js
+12
-11
@@ -1168,7 +1168,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
1168
var maxCookieAge = domain.twofactorcookiedurationdays;
1169
if (typeof maxCookieAge != 'number') { maxCookieAge = 30; }
1170
const twoFactorCookie = obj.parent.encodeCookie({ userid: user._id, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey);
1171
- res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: parent.config.settings.cookiesamesite, secure: true });
1171
+ res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: parent.config.settings.sessionsamesite, secure: true });
1172
}
1173
1174
// Check if email address needs to be confirmed
@@ -2625,7 +2625,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2625
setSessionRandom(req);
2626
} else if (req.query.login && (obj.parent.loginCookieEncryptionKey != null)) {
2627
var loginCookie = obj.parent.decodeCookie(req.query.login, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
2628
- //if ((loginCookie != null) && (loginCookie.ip != null) && checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // If the cookie if binded to an IP address, check here.
2628
+ //if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // If the cookie if binded to an IP address, check here.
2629
if ((loginCookie != null) && (loginCookie.a == 3) && (loginCookie.u != null) && (loginCookie.u.split('/')[1] == domain.id)) {
2630
// If a login cookie was provided, setup the session here.
2631
parent.debug('web', 'handleRootRequestEx: cookie auth ok.');
@@ -3087,7 +3087,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
3087
var maxCookieAge = domain.twofactorcookiedurationdays;
3088
if (typeof maxCookieAge != 'number') { maxCookieAge = 30; }
3089
const twoFactorCookie = obj.parent.encodeCookie({ userid: cookie.u, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey);
3090
- res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: parent.config.settings.cookiesamesite, secure: true });
3090
+ res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: parent.config.settings.sessionsamesite, secure: true });
3091
}
3092
3093
handleRootRequestEx(req, res, domain);
@@ -3853,7 +3853,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
3853
// If an authentication cookie is embedded in the form, use that.
3854
if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
3855
var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
3856
- if ((loginCookie != null) && (loginCookie.ip != null) && checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
3856
+ if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
3857
if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
3858
}
3859
if (authUserid == null) { res.sendStatus(401); return; }
@@ -3896,7 +3896,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
3896
// If an authentication cookie is embedded in the form, use that.
3897
if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
3898
var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
3899
- if ((loginCookie != null) && (loginCookie.ip != null) && checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
3899
+ if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
3900
if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
3901
}
3902
if (authUserid == null) { res.sendStatus(401); return; }
@@ -3936,7 +3936,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
3936
// If an authentication cookie is embedded in the form, use that.
3937
if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
3938
var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
3939
- if ((loginCookie != null) && (loginCookie.ip != null) && checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
3939
+ if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
3940
if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
3941
}
3942
if (authUserid == null) { res.sendStatus(401); return; }
@@ -4036,7 +4036,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
4036
// If an authentication cookie is embedded in the form, use that.
4037
if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
4038
var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
4039
- if ((loginCookie != null) && (loginCookie.ip != null) && checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
4039
+ if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
4040
if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
4041
}
4042
if (authUserid == null) { res.sendStatus(401); return; }
@@ -4859,7 +4859,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
4859
// If an authentication cookie is embedded in the form, use that.
4860
if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
4861
var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
4862
- if ((loginCookie != null) && (loginCookie.ip != null) && checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
4862
+ if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
4863
if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
4864
}
4865
if (authUserid == null) { res.sendStatus(401); return; }
@@ -5701,7 +5701,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
5701
httpOnly: true,
5702
keys: [obj.args.sessionkey], // If multiple instances of this server are behind a load-balancer, this secret must be the same for all instances
5703
secure: (obj.args.tlsoffload == null), // Use this cookie only over TLS (Check this: https://expressjs.com/en/guide/behind-proxies.html)
5704
- sameSite: obj.args.cookiesamesite
5704
+ sameSite: obj.args.sessionsamesite
5705
}
5706
if (obj.args.sessiontime != null) { sessionOptions.maxAge = (obj.args.sessiontime * 60 * 1000); }
5707
obj.app.use(obj.session(sessionOptions));
@@ -5834,7 +5834,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
5834
res.set(headers);
5835
5836
// Check the session if bound to the external IP address
5837
- if ((req.session.ip != null) && (req.clientIp != null) && checkCookieIp(req.session.ip, req.clientIp)) { req.session = {}; }
5837
+ if ((req.session.ip != null) && (req.clientIp != null) && !checkCookieIp(req.session.ip, req.clientIp)) { req.session = {}; }
5838
5839
// Extend the session time by forcing a change to the session every minute.
5840
if (req.session.userid != null) { req.session.t = Math.floor(Date.now() / 60e3); } else { delete req.session.t; }
@@ -6817,7 +6817,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6817
// This is a encrypted cookie authentication
6818
var cookie = obj.parent.decodeCookie(req.query.auth, obj.parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
6819
if ((cookie == null) && (obj.parent.multiServer != null)) { cookie = obj.parent.decodeCookie(req.query.auth, obj.parent.serverKey, 60); } // Try the server key
6820
- if ((cookie != null) && (cookie.ip != null) && checkCookieIp(cookie.ip, req.clientIp)) { // If the cookie if binded to an IP address, check here.
6820
+ if ((cookie != null) && (cookie.ip != null) && !checkCookieIp(cookie.ip, req.clientIp)) { // If the cookie if binded to an IP address, check here.
6821
parent.debug('web', 'ERR: Invalid cookie IP address, got \"' + cookie.ip + '\", expected \"' + cleanRemoteAddr(req.clientIp) + '\".');
6822
cookie = null;
6823
}
@@ -8201,6 +8201,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
8201
return header + value + '\r\n';
8202
}
8203
8204
+
8205
// Check that a cookie IP is within the correct range depending on the active policy
8206
function checkCookieIp(cookieip, ip) {
8207
if (obj.args.cookieipcheck == 'none') return true; // 'none' - No IP address checking