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, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;'); if (typeof x == 'boolean') return x; if (typeof x == 'number') return x; }
229 + //function EscapeHtmlBreaks(x) { if (typeof x == "string") return x.replace(/&/g, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;').replace(/\r/g, '<br />').replace(/\n/g, '').replace(/\t/g, '&nbsp;&nbsp;'); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
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, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;'); if (typeof x == 'boolean') return x; if (typeof x == 'number') return x; }
229 + //function EscapeHtmlBreaks(x) { if (typeof x == "string") return x.replace(/&/g, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;').replace(/\r/g, '<br />').replace(/\n/g, '').replace(/\t/g, '&nbsp;&nbsp;'); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
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