master
js 10,886 lines 736 KB
Raw
Large file — syntax highlighting disabled.
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.os = require('os');
43 obj.bodyParser = require('body-parser');
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-js');
56 obj.uaclienthints = require('ua-client-hints-js');
57 const constants = (obj.crypto.constants ? obj.crypto.constants : require('constants')); // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead.
58
59 // Setup WebAuthn / FIDO2
60 obj.webauthn = require('./webauthn.js').CreateWebAuthnModule();
61
62 if (process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']) {
63 obj.httpsProxyAgent = new (require('https-proxy-agent').HttpsProxyAgent)(process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']);
64 }
65
66 // Variables
67 obj.args = args;
68 obj.parent = parent;
69 obj.filespath = parent.filespath;
70 obj.db = db;
71 obj.app = obj.express();
72 if (obj.args.agentport) { obj.agentapp = obj.express(); }
73 if (args.compression === true) {
74 obj.app.use(require('compression')({ filter: function (req, res) {
75 if (req.path == '/devicefile.ashx') return false; // Don't compress device file transfers to show file sizes
76 if ((args.relaydns != null) && (obj.args.relaydns.indexOf(req.hostname) >= 0)) return false; // Don't compress DNS relay requests
77 return require('compression').filter(req, res);
78 }}));
79 }
80 obj.app.disable('x-powered-by');
81 obj.tlsServer = null;
82 obj.tcpServer = null;
83 obj.certificates = certificates;
84 obj.users = {}; // UserID --> User
85 obj.meshes = {}; // MeshID --> Mesh (also called device group)
86 obj.userGroups = {}; // UGrpID --> User Group
87 obj.useNodeDefaultTLSCiphers = args.usenodedefaulttlsciphers; // Use TLS ciphers provided by node
88 obj.tlsCiphers = args.tlsciphers; // List of TLS ciphers to use
89 obj.userAllowedIp = args.userallowedip; // List of allowed IP addresses for users
90 obj.agentAllowedIp = args.agentallowedip; // List of allowed IP addresses for agents
91 obj.agentBlockedIp = args.agentblockedip; // List of blocked IP addresses for agents
92 obj.tlsSniCredentials = null;
93 obj.dnsDomains = {};
94 obj.relaySessionCount = 0;
95 obj.relaySessionErrorCount = 0;
96 obj.blockedUsers = 0;
97 obj.blockedAgents = 0;
98 obj.renderPages = null;
99 obj.renderLanguages = [];
100 obj.destroyedSessions = {}; // userid/req.session.x --> destroyed session time
101
102 const isWindowsPlatform = (obj.os.platform() === 'win32');
103 const safeUploadTempRoots = (function () {
104 const roots = [];
105 const addRoot = function (p) {
106 if (typeof p !== 'string') { return; }
107 var resolved;
108 try { resolved = obj.path.normalize(obj.path.resolve(p)); } catch (ex) { return; }
109 if (resolved.length === 0) { return; }
110 if ((resolved.length > 1) && resolved.endsWith(obj.path.sep)) { resolved = resolved.slice(0, -1); }
111 const comparison = isWindowsPlatform ? resolved.toLowerCase() : resolved;
112 const comparisonWithSep = comparison + obj.path.sep;
113 roots.push({ comparison: comparison, comparisonWithSep: comparisonWithSep });
114 };
115 addRoot(obj.os.tmpdir());
116 if (typeof obj.parent.filespath === 'string') { addRoot(obj.path.join(obj.parent.filespath, 'tmp')); }
117 return roots;
118 })();
119 function resolveSafeUploadTempPath(tempPath) {
120 if (typeof tempPath !== 'string') { return null; }
121 var resolvedPath;
122 try { resolvedPath = obj.path.normalize(obj.path.resolve(tempPath)); } catch (ex) { return null; }
123 var comparisonPath = isWindowsPlatform ? resolvedPath.toLowerCase() : resolvedPath;
124 var comparisonPathNoTrailing = comparisonPath;
125 if ((comparisonPathNoTrailing.length > 1) && comparisonPathNoTrailing.endsWith(obj.path.sep)) { comparisonPathNoTrailing = comparisonPathNoTrailing.slice(0, -1); }
126 for (var i = 0; i < safeUploadTempRoots.length; i++) {
127 var root = safeUploadTempRoots[i];
128 if ((comparisonPathNoTrailing === root.comparison) || comparisonPath.startsWith(root.comparisonWithSep)) { return resolvedPath; }
129 }
130 return null;
131 }
132
133 // Web relay sessions
134 var webRelayNextSessionId = 1;
135 var webRelaySessions = {} // UserId/SessionId/Host --> Web Relay Session
136 var webRelayCleanupTimer = null;
137
138 // Monitor web relay session removals
139 parent.AddEventDispatch(['server-shareremove'], obj);
140 obj.HandleEvent = function (source, event, ids, id) {
141 if (event.action == 'removedDeviceShare') {
142 for (var relaySessionId in webRelaySessions) {
143 // A share was removed that matches an active session, close the web relay session.
144 if (webRelaySessions[relaySessionId].xpublicid === event.publicid) { webRelaySessions[relaySessionId].close(); }
145 }
146 }
147 }
148
149 // Mesh Rights
150 const MESHRIGHT_EDITMESH = 0x00000001;
151 const MESHRIGHT_MANAGEUSERS = 0x00000002;
152 const MESHRIGHT_MANAGECOMPUTERS = 0x00000004;
153 const MESHRIGHT_REMOTECONTROL = 0x00000008;
154 const MESHRIGHT_AGENTCONSOLE = 0x00000010;
155 const MESHRIGHT_SERVERFILES = 0x00000020;
156 const MESHRIGHT_WAKEDEVICE = 0x00000040;
157 const MESHRIGHT_SETNOTES = 0x00000080;
158 const MESHRIGHT_REMOTEVIEWONLY = 0x00000100;
159 const MESHRIGHT_NOTERMINAL = 0x00000200;
160 const MESHRIGHT_NOFILES = 0x00000400;
161 const MESHRIGHT_NOAMT = 0x00000800;
162 const MESHRIGHT_DESKLIMITEDINPUT = 0x00001000;
163 const MESHRIGHT_LIMITEVENTS = 0x00002000;
164 const MESHRIGHT_CHATNOTIFY = 0x00004000;
165 const MESHRIGHT_UNINSTALL = 0x00008000;
166 const MESHRIGHT_NODESKTOP = 0x00010000;
167 const MESHRIGHT_REMOTECOMMAND = 0x00020000;
168 const MESHRIGHT_RESETOFF = 0x00040000;
169 const MESHRIGHT_GUESTSHARING = 0x00080000;
170 const MESHRIGHT_ADMIN = 0xFFFFFFFF;
171
172 // Site rights
173 const SITERIGHT_SERVERBACKUP = 0x00000001;
174 const SITERIGHT_MANAGEUSERS = 0x00000002;
175 const SITERIGHT_SERVERRESTORE = 0x00000004;
176 const SITERIGHT_FILEACCESS = 0x00000008;
177 const SITERIGHT_SERVERUPDATE = 0x00000010;
178 const SITERIGHT_LOCKED = 0x00000020;
179 const SITERIGHT_NONEWGROUPS = 0x00000040;
180 const SITERIGHT_NOMESHCMD = 0x00000080;
181 const SITERIGHT_USERGROUPS = 0x00000100;
182 const SITERIGHT_RECORDINGS = 0x00000200;
183 const SITERIGHT_LOCKSETTINGS = 0x00000400;
184 const SITERIGHT_ALLEVENTS = 0x00000800;
185 const SITERIGHT_NONEWDEVICES = 0x00001000;
186 const SITERIGHT_ADMIN = 0xFFFFFFFF;
187
188 // Setup SSPI authentication if needed
189 if ((obj.parent.platform == 'win32') && (obj.args.nousers != true) && (obj.parent.config != null) && (obj.parent.config.domains != null)) {
190 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 }); } }
191 }
192
193 // Perform hash on web certificate and agent certificate
194 obj.webCertificateHash = parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.web.cert);
195 obj.webCertificateHashs = { '': obj.webCertificateHash };
196 obj.webCertificateHashBase64 = Buffer.from(obj.webCertificateHash, 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
197 obj.webCertificateFullHash = parent.certificateOperations.getCertHashBinary(obj.certificates.web.cert);
198 obj.webCertificateFullHashs = { '': obj.webCertificateFullHash };
199 obj.webCertificateExpire = { '': parent.certificateOperations.getCertificateExpire(parent.certificates.web.cert) };
200 obj.agentCertificateHashHex = parent.certificateOperations.getPublicKeyHash(obj.certificates.agent.cert);
201 obj.agentCertificateHashBase64 = Buffer.from(obj.agentCertificateHashHex, 'hex').toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
202 obj.agentCertificateAsn1 = parent.certificateOperations.forge.asn1.toDer(parent.certificateOperations.forge.pki.certificateToAsn1(parent.certificateOperations.forge.pki.certificateFromPem(parent.certificates.agent.cert))).getBytes();
203 obj.defaultWebCertificateHash = obj.certificates.webdefault ? parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.webdefault.cert) : null;
204 obj.defaultWebCertificateFullHash = obj.certificates.webdefault ? parent.certificateOperations.getCertHashBinary(obj.certificates.webdefault.cert) : null;
205
206 // Compute the hash of all of the web certificates for each domain
207 for (var i in obj.parent.config.domains) {
208 if (obj.parent.config.domains[i].certhash != null) {
209 // If the web certificate hash is provided, use it.
210 obj.webCertificateHashs[i] = obj.webCertificateFullHashs[i] = Buffer.from(obj.parent.config.domains[i].certhash, 'hex').toString('binary');
211 if (obj.parent.config.domains[i].certkeyhash != null) { obj.webCertificateHashs[i] = Buffer.from(obj.parent.config.domains[i].certkeyhash, 'hex').toString('binary'); }
212 delete obj.webCertificateExpire[i]; // Expire time is not provided
213 } else if ((obj.parent.config.domains[i].dns != null) && (obj.parent.config.domains[i].certs != null)) {
214 // If the domain has a different DNS name, use a different certificate hash.
215 // Hash the full certificate
216 obj.webCertificateFullHashs[i] = parent.certificateOperations.getCertHashBinary(obj.parent.config.domains[i].certs.cert);
217 obj.webCertificateExpire[i] = Date.parse(parent.certificateOperations.forge.pki.certificateFromPem(obj.parent.config.domains[i].certs.cert).validity.notAfter);
218 try {
219 // Decode a RSA certificate and hash the public key.
220 obj.webCertificateHashs[i] = parent.certificateOperations.getPublicKeyHashBinary(obj.parent.config.domains[i].certs.cert);
221 } catch (ex) {
222 // This may be a ECDSA certificate, hash the entire cert.
223 obj.webCertificateHashs[i] = obj.webCertificateFullHashs[i];
224 }
225 } else if ((obj.parent.config.domains[i].dns != null) && (obj.certificates.dns[i] != null)) {
226 // If this domain has a DNS and a matching DNS cert, use it. This case works for wildcard certs.
227 obj.webCertificateFullHashs[i] = parent.certificateOperations.getCertHashBinary(obj.certificates.dns[i].cert);
228 obj.webCertificateHashs[i] = parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.dns[i].cert);
229 obj.webCertificateExpire[i] = Date.parse(parent.certificateOperations.forge.pki.certificateFromPem(obj.certificates.dns[i].cert).validity.notAfter);
230 } else if (i != '') {
231 // For any other domain, use the default cert.
232 obj.webCertificateFullHashs[i] = obj.webCertificateFullHashs[''];
233 obj.webCertificateHashs[i] = obj.webCertificateHashs[''];
234 obj.webCertificateExpire[i] = obj.webCertificateExpire[''];
235 }
236 }
237
238 // If we are running the legacy swarm server, compute the hash for that certificate
239 if (parent.certificates.swarmserver != null) {
240 obj.swarmCertificateAsn1 = parent.certificateOperations.forge.asn1.toDer(parent.certificateOperations.forge.pki.certificateToAsn1(parent.certificateOperations.forge.pki.certificateFromPem(parent.certificates.swarmserver.cert))).getBytes();
241 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' });
242 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' });
243 }
244
245 // Main lists
246 obj.wsagents = {}; // NodeId --> Agent
247 obj.wsagentsWithBadWebCerts = {}; // NodeId --> Agent
248 obj.wsagentsDisconnections = {};
249 obj.wsagentsDisconnectionsTimer = null;
250 obj.duplicateAgentsLog = {};
251 obj.wssessions = {}; // UserId --> Array Of Sessions
252 obj.wssessions2 = {}; // "UserId + SessionRnd" --> Session (Note that the SessionId is the UserId + / + SessionRnd)
253 obj.wsPeerSessions = {}; // ServerId --> Array Of "UserId + SessionRnd"
254 obj.wsPeerSessions2 = {}; // "UserId + SessionRnd" --> ServerId
255 obj.wsPeerSessions3 = {}; // ServerId --> UserId --> [ SessionId ]
256 obj.sessionsCount = {}; // Merged session counters, used when doing server peering. UserId --> SessionCount
257 obj.wsrelays = {}; // Id -> Relay
258 obj.desktoprelays = {}; // Id -> Desktop Multiplexer Relay
259 obj.wsPeerRelays = {}; // Id -> { ServerId, Time }
260 var tlsSessionStore = {}; // Store TLS session information for quick resume.
261 var tlsSessionStoreCount = 0; // Number of cached TLS session information in store.
262
263 // Setup randoms
264 obj.crypto.randomBytes(48, function (err, buf) { obj.httpAuthRandom = buf; });
265 obj.crypto.randomBytes(16, function (err, buf) { obj.httpAuthRealm = buf.toString('hex'); });
266 obj.crypto.randomBytes(48, function (err, buf) { obj.relayRandom = buf; });
267
268 // Get non-english web pages and emails
269 getRenderList();
270 getEmailLanguageList();
271
272 // Setup DNS domain TLS SNI credentials
273 {
274 var dnscount = 0;
275 obj.tlsSniCredentials = {};
276 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++; } }
277 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; }
278 }
279 function TlsSniCallback(name, cb) {
280 var c = obj.tlsSniCredentials[name];
281 if (c != null) {
282 cb(null, c);
283 } else {
284 cb(null, obj.tlsSniCredentials['']);
285 }
286 }
287
288 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; }
289 //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; }
290 // Fetch all users from the database, keep this in memory
291 obj.db.GetAllType('user', function (err, docs) {
292 obj.common.unEscapeAllLinksFieldName(docs);
293 var domainUserCount = {}, i = 0;
294 for (i in parent.config.domains) { domainUserCount[i] = 0; }
295 for (i in docs) { var u = obj.users[docs[i]._id] = docs[i]; domainUserCount[u.domain]++; }
296 for (i in parent.config.domains) {
297 if ((parent.config.domains[i].share == null) && (domainUserCount[i] == 0)) {
298 // If newaccounts is set to no new accounts, but no accounts exists, temporarily allow account creation.
299 //if ((parent.config.domains[i].newaccounts === 0) || (parent.config.domains[i].newaccounts === false)) { parent.config.domains[i].newaccounts = 2; }
300 console.log('Server ' + ((i == '') ? '' : (i + ' ')) + 'has no users, next new account will be site administrator.');
301 }
302 }
303
304 // Fetch all device groups (meshes) from the database, keep this in memory
305 // As we load things in memory, we will also be doing some cleaning up.
306 // We will not save any clean up in the database right now, instead it will be saved next time there is a change.
307 obj.db.GetAllType('mesh', function (err, docs) {
308 obj.common.unEscapeAllLinksFieldName(docs);
309 for (var i in docs) { obj.meshes[docs[i]._id] = docs[i]; } // Get all meshes, including deleted ones.
310
311 // Fetch all user groups from the database, keep this in memory
312 obj.db.GetAllType('ugrp', function (err, docs) {
313 obj.common.unEscapeAllLinksFieldName(docs);
314
315 // Perform user group link cleanup
316 for (var i in docs) {
317 const ugrp = docs[i];
318 if (ugrp.links != null) {
319 for (var j in ugrp.links) {
320 if (j.startsWith('user/') && (obj.users[j] == null)) { delete ugrp.links[j]; } // User group has a link to a user that does not exist
321 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
322 }
323 }
324 obj.userGroups[docs[i]._id] = docs[i]; // Get all user groups
325 }
326
327 // Mapping between users and groups
328 for (var ugrpId in obj.userGroups) {
329 const ugrp = obj.userGroups[ugrpId];
330 if (ugrp.links != null) {
331 for (var userId in ugrp.links) {
332 if (userId.startsWith('user/') && (obj.users[userId] != null)) {
333 const user = obj.users[userId];
334 if (user.links == null) { user.links = {}; }
335 if (user.links[ugrpId] == null) {
336 // Adding group link to user
337 user.links[ugrpId] = { rights: ugrp.links[userId].rights || 1 };
338 }
339 }
340 }
341 }
342 }
343
344 // Perform device group link cleanup
345 for (var i in obj.meshes) {
346 const mesh = obj.meshes[i];
347 if (mesh.links != null) {
348 for (var j in mesh.links) {
349 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
350 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
351 }
352 }
353 }
354
355 // Perform user link cleanup
356 for (var i in obj.users) {
357 const user = obj.users[i];
358 if (user.links != null) {
359 for (var j in user.links) {
360 if (j.startsWith('ugrp/') && (obj.userGroups[j] == null)) { delete user.links[j]; } // User has a link to a user group that does not exist
361 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
362 //else if (j.startsWith('node/') && (obj.nodes[j] == null)) { delete user.links[j]; } // TODO
363 }
364 //if (Object.keys(user.links).length == 0) { delete user.links; }
365 }
366 }
367
368 // We loaded the users, device groups and user group state, start the server
369 serverStart();
370 });
371 });
372 });
373
374 // Clean up a device, used before saving it in the database
375 obj.cleanDevice = function (device) {
376 // Check device links, if a link points to an unknown user, remove it.
377 if (device.links != null) {
378 for (var j in device.links) {
379 if ((obj.users[j] == null) && (obj.userGroups[j] == null)) {
380 delete device.links[j];
381 if (Object.keys(device.links).length == 0) { delete device.links; }
382 }
383 }
384 }
385 return device;
386 }
387
388 // Return statistics about this web server
389 obj.getStats = function () {
390 return {
391 users: Object.keys(obj.users).length,
392 meshes: Object.keys(obj.meshes).length,
393 dnsDomains: Object.keys(obj.dnsDomains).length,
394 relaySessionCount: obj.relaySessionCount,
395 relaySessionErrorCount: obj.relaySessionErrorCount,
396 wsagents: Object.keys(obj.wsagents).length,
397 wsagentsDisconnections: Object.keys(obj.wsagentsDisconnections).length,
398 wsagentsDisconnectionsTimer: Object.keys(obj.wsagentsDisconnectionsTimer).length,
399 wssessions: Object.keys(obj.wssessions).length,
400 wssessions2: Object.keys(obj.wssessions2).length,
401 wsPeerSessions: Object.keys(obj.wsPeerSessions).length,
402 wsPeerSessions2: Object.keys(obj.wsPeerSessions2).length,
403 wsPeerSessions3: Object.keys(obj.wsPeerSessions3).length,
404 sessionsCount: Object.keys(obj.sessionsCount).length,
405 wsrelays: Object.keys(obj.wsrelays).length,
406 wsPeerRelays: Object.keys(obj.wsPeerRelays).length,
407 tlsSessionStore: Object.keys(tlsSessionStore).length,
408 blockedUsers: obj.blockedUsers,
409 blockedAgents: obj.blockedAgents
410 };
411 }
412
413 // Agent counters
414 obj.agentStats = {
415 createMeshAgentCount: 0,
416 agentClose: 0,
417 agentBinaryUpdate: 0,
418 agentMeshCoreBinaryUpdate: 0,
419 coreIsStableCount: 0,
420 verifiedAgentConnectionCount: 0,
421 clearingCoreCount: 0,
422 updatingCoreCount: 0,
423 recoveryCoreIsStableCount: 0,
424 meshDoesNotExistCount: 0,
425 invalidPkcsSignatureCount: 0,
426 invalidRsaSignatureCount: 0,
427 invalidJsonCount: 0,
428 unknownAgentActionCount: 0,
429 agentBadWebCertHashCount: 0,
430 agentBadSignature1Count: 0,
431 agentBadSignature2Count: 0,
432 agentMaxSessionHoldCount: 0,
433 invalidDomainMeshCount: 0,
434 invalidMeshTypeCount: 0,
435 invalidDomainMesh2Count: 0,
436 invalidMeshType2Count: 0,
437 duplicateAgentCount: 0,
438 maxDomainDevicesReached: 0,
439 agentInTrouble: 0,
440 agentInBigTrouble: 0
441 }
442 obj.getAgentStats = function () { return obj.agentStats; }
443
444 // Traffic counters
445 obj.trafficStats = {
446 httpRequestCount: 0,
447 httpWebSocketCount: 0,
448 httpIn: 0,
449 httpOut: 0,
450 relayCount: {},
451 relayIn: {},
452 relayOut: {},
453 localRelayCount: {},
454 localRelayIn: {},
455 localRelayOut: {},
456 AgentCtrlIn: 0,
457 AgentCtrlOut: 0,
458 LMSIn: 0,
459 LMSOut: 0,
460 CIRAIn: 0,
461 CIRAOut: 0
462 }
463 obj.trafficStats.time = Date.now();
464 obj.getTrafficStats = function () { return obj.trafficStats; }
465 obj.getTrafficDelta = function (oldTraffic) { // Return the difference between the old and new data along with the delta time.
466 const data = obj.common.Clone(obj.trafficStats);
467 data.time = Date.now();
468 const delta = calcDelta(oldTraffic ? oldTraffic : {}, data);
469 if (oldTraffic && oldTraffic.time) { delta.delta = (data.time - oldTraffic.time); }
470 delta.time = data.time;
471 return { current: data, delta: delta }
472 }
473 function calcDelta(oldData, newData) { // Recursive function that computes the difference of all numbers
474 const r = {};
475 for (var i in newData) {
476 if (typeof newData[i] == 'object') { r[i] = calcDelta(oldData[i] ? oldData[i] : {}, newData[i]); }
477 if (typeof newData[i] == 'number') { if (typeof oldData[i] == 'number') { r[i] = (newData[i] - oldData[i]); } else { r[i] = newData[i]; } }
478 }
479 return r;
480 }
481
482 // Keep a record of the last agent issues.
483 obj.getAgentIssues = function () { return obj.agentIssues; }
484 obj.setAgentIssue = function (agent, issue) { obj.agentIssues.push([new Date().toLocaleString(), agent.remoteaddrport, issue]); while (obj.setAgentIssue.length > 50) { obj.agentIssues.shift(); } }
485 obj.agentIssues = [];
486
487 // Authenticate the user
488 obj.authenticate = function (name, pass, domain, fn) {
489 if ((typeof (name) != 'string') || (typeof (pass) != 'string') || (typeof (domain) != 'object')) { fn(new Error('invalid fields')); return; }
490 if (name.startsWith('~t:')) {
491 // Login token, try to fetch the token from the database
492 obj.db.Get('logintoken-' + name, function (err, docs) {
493 if (err != null) { fn(err); return; }
494 if ((docs == null) || (docs.length != 1)) { fn(new Error('login token not found')); return; }
495 const loginToken = docs[0];
496 if ((loginToken.expire != 0) && (loginToken.expire < Date.now())) { fn(new Error('login token expired')); return; }
497
498 // Default strong password hashing (pbkdf2 SHA384)
499 require('./pass').hash(pass, loginToken.salt, function (err, hash, tag) {
500 if (err) return fn(err);
501 if (hash == loginToken.hash) {
502 // Login username and password are valid.
503 var user = obj.users[loginToken.userid];
504 if (!user) { fn(new Error('cannot find user')); return; }
505 if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
506
507 // Successful login token authentication
508 var loginOptions = { tokenName: loginToken.name, tokenUser: loginToken.tokenUser };
509 if (loginToken.expire != 0) { loginOptions.expire = loginToken.expire; }
510 return fn(null, user._id, null, loginOptions);
511 }
512 fn(new Error('invalid password'));
513 }, 0);
514 });
515 } else if (domain.auth == 'ldap') {
516 // This method will handle LDAP login
517 const ldapHandler = function ldapHandlerFunc(err, xxuser) {
518 if (err) { parent.debug('ldap', 'LDAP Error: ' + err); if (ldapHandlerFunc.ldapobj) { try { ldapHandlerFunc.ldapobj.close(); } catch (ex) { console.log(ex); } } fn(new Error('invalid password')); return; }
519
520 // Save this LDAP user to file if needed
521 if (typeof domain.ldapsaveusertofile == 'string') {
522 obj.fs.appendFile(domain.ldapsaveusertofile, JSON.stringify(xxuser) + '\r\n\r\n', function (err) { });
523 }
524
525 // Work on getting the userid for this LDAP user
526 var shortname = null;
527 var username = xxuser['displayName'];
528 if (typeof domain.ldapusername == 'string') {
529 if (domain.ldapusername.indexOf('{{{') >= 0) { username = assembleStringFromObject(domain.ldapusername, xxuser); } else { username = xxuser[domain.ldapusername]; }
530 } else { username = xxuser['displayName'] ? xxuser['displayName'] : xxuser['name']; }
531 if (domain.ldapuserbinarykey) {
532 // Use a binary key as the userid
533 if (xxuser[domain.ldapuserbinarykey]) { shortname = Buffer.from(xxuser[domain.ldapuserbinarykey], 'binary').toString('hex').toLowerCase(); }
534 } else if (domain.ldapuserkey) {
535 // Use a string key as the userid
536 if (xxuser[domain.ldapuserkey]) { shortname = xxuser[domain.ldapuserkey]; }
537 } else {
538 // Use the default key as the userid
539 if (xxuser['objectSid']) { shortname = Buffer.from(xxuser['objectSid'], 'binary').toString('hex').toLowerCase(); }
540 else if (xxuser['objectGUID']) { shortname = Buffer.from(xxuser['objectGUID'], 'binary').toString('hex').toLowerCase(); }
541 else if (xxuser['name']) { shortname = xxuser['name']; }
542 else if (xxuser['cn']) { shortname = xxuser['cn']; }
543 }
544 if (shortname == null) { fn(new Error('no user identifier')); if (ldapHandlerFunc.ldapobj) { try { ldapHandlerFunc.ldapobj.close(); } catch (ex) { console.log(ex); } } return; }
545 if (username == null) { username = shortname; }
546 var userid = 'user/' + domain.id + '/' + shortname;
547
548 // Get the list of groups this user is a member of.
549 var userMemberships = xxuser[(typeof domain.ldapusergroups == 'string') ? domain.ldapusergroups : 'memberOf'];
550 if (typeof userMemberships == 'string') { userMemberships = [userMemberships]; }
551 if (Array.isArray(userMemberships) == false) { userMemberships = []; }
552
553 // See if the user is required to be part of an LDAP user group in order to log into this server.
554 if (typeof domain.ldapuserrequiredgroupmembership == 'string') { domain.ldapuserrequiredgroupmembership = [domain.ldapuserrequiredgroupmembership]; }
555 if (Array.isArray(domain.ldapuserrequiredgroupmembership)) {
556 // Look for a matching LDAP user group
557 var userMembershipMatch = false;
558 for (var i in domain.ldapuserrequiredgroupmembership) { if (userMemberships.indexOf(domain.ldapuserrequiredgroupmembership[i]) >= 0) { userMembershipMatch = true; } }
559 if (userMembershipMatch === false) { parent.authLog('ldapHandler', 'LDAP denying login to a user that is not a member of a LDAP required group.'); fn('denied'); return; } // If there is no match, deny the login
560 }
561
562 // Check if user is in an site administrator group
563 var siteAdminGroup = null;
564 if (typeof domain.ldapsiteadmingroups == 'string') { domain.ldapsiteadmingroups = [domain.ldapsiteadmingroups]; }
565 if (Array.isArray(domain.ldapsiteadmingroups)) {
566 siteAdminGroup = false;
567 for (var i in domain.ldapsiteadmingroups) {
568 if (userMemberships.indexOf(domain.ldapsiteadmingroups[i]) >= 0) { siteAdminGroup = domain.ldapsiteadmingroups[i]; }
569 }
570 }
571
572 // See if we need to sync LDAP user memberships with user groups
573 if (domain.ldapsyncwithusergroups === true) { domain.ldapsyncwithusergroups = {}; }
574 if (typeof domain.ldapsyncwithusergroups == 'object') {
575 // LDAP user memberships sync is enabled, see if there are any filters to apply
576 if (typeof domain.ldapsyncwithusergroups.filter == 'string') { domain.ldapsyncwithusergroups.filter = [domain.ldapsyncwithusergroups.filter]; }
577 if (Array.isArray(domain.ldapsyncwithusergroups.filter)) {
578 const g = [];
579 for (var i in userMemberships) {
580 var match = false;
581 for (var j in domain.ldapsyncwithusergroups.filter) {
582 if (userMemberships[i].indexOf(domain.ldapsyncwithusergroups.filter[j]) >= 0) { match = true; }
583 }
584 if (match) { g.push(userMemberships[i]); }
585 }
586 userMemberships = g;
587 }
588 } else {
589 // LDAP user memberships sync is disabled, sync the user with empty membership
590 userMemberships = [];
591 }
592
593 // Get the email address for this LDAP user
594 var email = null;
595 if (domain.ldapuseremail) { email = xxuser[domain.ldapuseremail]; } else if (xxuser['mail']) { email = xxuser['mail']; } // Use given field name or default
596 if (Array.isArray(email)) { email = email[0]; } // Mail may be multivalued in LDAP in which case, answer is an array. Use the 1st value.
597 if (email) { email = email.toLowerCase(); } // it seems some code elsewhere also lowercase the emailaddress, so let's be consistent.
598
599 // Get the real name for this LDAP user
600 var realname = null;
601 if (typeof domain.ldapuserrealname == 'string') {
602 if (domain.ldapuserrealname.indexOf('{{{') >= 0) { realname = assembleStringFromObject(domain.ldapuserrealname, xxuser); } else { realname = xxuser[domain.ldapuserrealname]; }
603 }
604 else { if (typeof xxuser['name'] == 'string') { realname = xxuser['name']; } }
605
606 // Get the phone number for this LDAP user
607 var phonenumber = null;
608 if (domain.ldapuserphonenumber) { phonenumber = xxuser[domain.ldapuserphonenumber]; }
609 else { if (typeof xxuser['telephoneNumber'] == 'string') { phonenumber = xxuser['telephoneNumber']; } }
610
611 // Work on getting the image of this LDAP user
612 var userimage = null, userImageBuffer = null;
613 if (xxuser._raw) { // Using _raw allows us to get data directly as buffer.
614 if (domain.ldapuserimage && xxuser[domain.ldapuserimage]) { userImageBuffer = xxuser._raw[domain.ldapuserimage]; }
615 else if (xxuser['thumbnailPhoto']) { userImageBuffer = xxuser._raw['thumbnailPhoto']; }
616 else if (xxuser['jpegPhoto']) { userImageBuffer = xxuser._raw['jpegPhoto']; }
617 if (userImageBuffer != null) {
618 if ((userImageBuffer[0] == 0xFF) && (userImageBuffer[1] == 0xD8) && (userImageBuffer[2] == 0xFF) && (userImageBuffer[3] == 0xE0)) { userimage = 'data:image/jpeg;base64,' + userImageBuffer.toString('base64'); }
619 if ((userImageBuffer[0] == 0x89) && (userImageBuffer[1] == 0x50) && (userImageBuffer[2] == 0x4E) && (userImageBuffer[3] == 0x47)) { userimage = 'data:image/png;base64,' + userImageBuffer.toString('base64'); }
620 }
621 }
622
623 // Display user information extracted from LDAP data
624 parent.authLog('ldapHandler', 'LDAP user login, id: ' + shortname + ', username: ' + username + ', email: ' + email + ', realname: ' + realname + ', phone: ' + phonenumber + ', image: ' + (userimage != null));
625
626 // If there is a testing userid, use that
627 if (ldapHandlerFunc.ldapShortName) {
628 shortname = ldapHandlerFunc.ldapShortName;
629 userid = 'user/' + domain.id + '/' + shortname;
630 }
631
632 // Save the user image
633 if (userimage != null) { parent.db.Set({ _id: 'im' + userid, image: userimage }); } else { db.Remove('im' + userid); }
634
635 // Close the LDAP object
636 if (ldapHandlerFunc.ldapobj) { try { ldapHandlerFunc.ldapobj.close(); } catch (ex) { console.log(ex); } }
637
638 // Check if the user already exists
639 var user = obj.users[userid];
640 if (user == null) {
641 // This user does not exist, create a new account.
642 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 };
643 if (email) { user['email'] = email; user['emailVerified'] = true; }
644 if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; }
645 if (obj.common.validateStrArray(domain.newaccountrealms)) { user.groups = domain.newaccountrealms; }
646 var usercount = 0;
647 for (var i in obj.users) { if (obj.users[i].domain == domain.id) { usercount++; } }
648 if (usercount == 0) { user.siteadmin = 4294967295; /*if (domain.newaccounts === 2) { delete domain.newaccounts; }*/ } // If this is the first user, give the account site admin.
649
650 // Auto-join any user groups
651 if (typeof domain.newaccountsusergroups == 'object') {
652 for (var i in domain.newaccountsusergroups) {
653 var ugrpid = domain.newaccountsusergroups[i];
654 if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
655 var ugroup = obj.userGroups[ugrpid];
656 if (ugroup != null) {
657 // Add group to the user
658 if (user.links == null) { user.links = {}; }
659 user.links[ugroup._id] = { rights: 1 };
660
661 // Add user to the group
662 ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
663 db.Set(ugroup);
664
665 // Notify user group change
666 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 };
667 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.
668 parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
669 }
670 }
671 }
672
673 // Check the user real name
674 if (realname) { user.realname = realname; }
675
676 // Check the user phone number
677 if (phonenumber) { user.phone = phonenumber; }
678
679 // Indicate that this user has a image
680 if (userimage != null) { user.flags = 1; }
681
682 // See if the user is a member of the site admin group.
683 if (typeof siteAdminGroup === 'string') {
684 parent.authLog('ldapHandler', `LDAP: Granting site admin privilages to new user "${user.name}" found in admin group: ${siteAdminGroup}`);
685 user.siteadmin = 0xFFFFFFFF;
686 }
687
688 // Sync the user with LDAP matching user groups
689 if (syncExternalUserGroups(domain, user, userMemberships, 'ldap') == true) { userChanged = true; }
690
691 obj.users[user._id] = user;
692 obj.db.SetUser(user);
693 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 };
694 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.
695 obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
696 return fn(null, user._id);
697 } else {
698 var userChanged = false;
699
700 // This is an existing user
701 // If the display username has changes, update it.
702 if (user.name != username) { user.name = username; userChanged = true; }
703
704 // Check if user email has changed
705 if (user.email && !email) { // email unset in ldap => unset
706 delete user.email;
707 delete user.emailVerified;
708 userChanged = true;
709 } else if (user.email != email) { // update email
710 user['email'] = email;
711 user['emailVerified'] = true;
712 userChanged = true;
713 }
714
715 // Check the user real name
716 if (realname != user.realname) { user.realname = realname; userChanged = true; }
717
718 // Check the user phone number
719 if (phonenumber != user.phone) { user.phone = phonenumber; userChanged = true; }
720
721 // Check the user image flag
722 if ((userimage != null) && ((user.flags == null) || ((user.flags & 1) == 0))) { if (user.flags == null) { user.flags = 1; } else { user.flags += 1; } userChanged = true; }
723 if ((userimage == null) && (user.flags != null) && ((user.flags & 1) != 0)) { if (user.flags == 1) { delete user.flags; } else { user.flags -= 1; } userChanged = true; }
724
725 // See if the user is a member of the site admin group.
726 if ((typeof siteAdminGroup === 'string') && (user.siteadmin !== 0xFFFFFFFF)) {
727 parent.authLog('ldapHandler', `LDAP: Granting site admin privilages to user "${user.name}" found in administrator group: ${siteAdminGroup}`);
728 user.siteadmin = 0xFFFFFFFF;
729 userChanged = true;
730 } else if ((siteAdminGroup === false) && (user.siteadmin === 0xFFFFFFFF)) {
731 parent.authLog('ldapHandler', `LDAP: Revoking site admin privilages from user "${user.name}" since they are not found in any administrator groups.`);
732 delete user.siteadmin;
733 userChanged = true;
734 }
735
736 // Synd the user with LDAP matching user groups
737 if (syncExternalUserGroups(domain, user, userMemberships, 'ldap') == true) { userChanged = true; }
738
739 // If the user changed, save the changes to the database here
740 if (userChanged) {
741 obj.db.SetUser(user);
742 var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msgid: 154, msg: 'Account changed to sync with LDAP data.', domain: domain.id };
743 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.
744 parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
745 }
746
747 // If user is locker out, block here.
748 if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
749 return fn(null, user._id);
750 }
751 }
752
753 if (domain.ldapoptions.url == 'test') {
754 // Test LDAP login
755 var xxuser = domain.ldapoptions[name.toLowerCase()];
756 if (xxuser == null) { fn(new Error('invalid password')); return; } else {
757 ldapHandler.ldapShortName = name.toLowerCase();
758 if (typeof xxuser == 'string') {
759 // The test LDAP user points to a JSON file where the user information is, load it.
760 ldapHandler(null, require(xxuser));
761 } else {
762 // The test user information is in the config.json, use it.
763 ldapHandler(null, xxuser);
764 }
765 }
766 } else {
767 // LDAP login
768 var LdapAuth = require('ldapauth-fork');
769 if (domain.ldapoptions == null) { domain.ldapoptions = {}; }
770 domain.ldapoptions.includeRaw = true; // This allows us to get data as buffers which is useful for images.
771 var ldap = new LdapAuth(domain.ldapoptions);
772 ldapHandler.ldapobj = ldap;
773 ldap.on('error', function (err) { parent.debug('ldap', 'LDAP OnError: ' + err); try { ldap.close(); } catch (ex) { console.log(ex); } }); // Close the LDAP object
774 ldap.authenticate(name, pass, ldapHandler);
775 }
776 } else {
777 // Regular login
778 var user = obj.users['user/' + domain.id + '/' + name.toLowerCase()];
779 // Query the db for the given username
780 if (!user) { fn(new Error('cannot find user')); return; }
781 // Apply the same algorithm to the POSTed password, applying the hash against the pass / salt, if there is a match we found the user
782 if (user.salt == null) {
783 fn(new Error('invalid password'));
784 } else {
785 if (user.passtype != null) {
786 // IIS default clear or weak password hashing (SHA-1)
787 require('./pass').iishash(user.passtype, pass, user.salt, function (err, hash) {
788 if (err) return fn(err);
789 if (hash == user.hash) {
790 // Update the password to the stronger format.
791 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);
792 if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
793 return fn(null, user._id);
794 }
795 fn(new Error('invalid password'), null, user.passhint);
796 });
797 } else {
798 // Default strong password hashing (pbkdf2 SHA384)
799 require('./pass').hash(pass, user.salt, function (err, hash, tag) {
800 if (err) return fn(err);
801 if (hash == user.hash) {
802 if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
803 return fn(null, user._id);
804 }
805 fn(new Error('invalid password'), null, user.passhint);
806 }, 0);
807 }
808 }
809 }
810 };
811
812 /*
813 obj.restrict = function (req, res, next) {
814 console.log('restrict', req.url);
815 var domain = getDomain(req);
816 if (req.session.userid) {
817 next();
818 } else {
819 req.session.messageid = 111; // Access denied.
820 res.redirect(domain.url + 'login');
821 }
822 };
823 */
824
825 // Check if the source IP address is in the IP list, return false if not.
826 function checkIpAddressEx(req, res, ipList, closeIfThis, redirectUrl) {
827 try {
828 if (req.connection) {
829 // HTTP(S) request
830 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; } } }
831 if (closeIfThis === false) { if (typeof redirectUrl == 'string') { res.redirect(redirectUrl); } else { res.sendStatus(401); } }
832 } else {
833 // WebSocket request
834 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; } } }
835 if (closeIfThis === false) { try { req.close(); } catch (e) { } }
836 }
837 } catch (e) { console.log(e); } // Should never happen
838 return false;
839 }
840
841 // Check if the source IP address is allowed, return domain if allowed
842 // If there is a fail and null is returned, the request or connection is closed already.
843 function checkUserIpAddress(req, res) {
844 if ((parent.config.settings.userblockedip != null) && (checkIpAddressEx(req, res, parent.config.settings.userblockedip, true, parent.config.settings.ipblockeduserredirect) == true)) { obj.blockedUsers++; return null; }
845 if ((parent.config.settings.userallowedip != null) && (checkIpAddressEx(req, res, parent.config.settings.userallowedip, false, parent.config.settings.ipblockeduserredirect) == false)) { obj.blockedUsers++; return null; }
846 const domain = (req.url ? getDomain(req) : getDomain(res));
847 if (domain == null) { parent.debug('web', 'handleRootRequest: invalid domain.'); try { res.sendStatus(404); } catch (ex) { } return; }
848 if ((domain.userblockedip != null) && (checkIpAddressEx(req, res, domain.userblockedip, true, domain.ipblockeduserredirect) == true)) { obj.blockedUsers++; return null; }
849 if ((domain.userallowedip != null) && (checkIpAddressEx(req, res, domain.userallowedip, false, domain.ipblockeduserredirect) == false)) { obj.blockedUsers++; return null; }
850 return domain;
851 }
852
853 // Check if the source IP address is allowed, return domain if allowed
854 // If there is a fail and null is returned, the request or connection is closed already.
855 function checkAgentIpAddress(req, res) {
856 if ((parent.config.settings.agentblockedip != null) && (checkIpAddressEx(req, res, parent.config.settings.agentblockedip, null) == true)) { obj.blockedAgents++; return null; }
857 if ((parent.config.settings.agentallowedip != null) && (checkIpAddressEx(req, res, parent.config.settings.agentallowedip, null) == false)) { obj.blockedAgents++; return null; }
858 const domain = (req.url ? getDomain(req) : getDomain(res));
859 if ((domain.agentblockedip != null) && (checkIpAddressEx(req, res, domain.agentblockedip, null) == true)) { obj.blockedAgents++; return null; }
860 if ((domain.agentallowedip != null) && (checkIpAddressEx(req, res, domain.agentallowedip, null) == false)) { obj.blockedAgents++; return null; }
861 return domain;
862 }
863
864 // Return the current domain of the request
865 // Request or connection says open regardless of the response
866 function getDomain(req) {
867 if (req.xdomain != null) { return req.xdomain; } // Domain already set for this request, return it.
868 if ((req.hostname == 'localhost') && (req.query.domainid != null)) { const d = parent.config.domains[req.query.domainid]; if (d != null) return d; } // This is a localhost access with the domainid specified in the URL
869 if (req.hostname != null) { const d = obj.dnsDomains[req.hostname.toLowerCase()]; if (d != null) return d; } // If this is a DNS name domain, return it here.
870 const x = req.url.split('/');
871 if (x.length < 2) return parent.config.domains[''];
872 const y = parent.config.domains[x[1].toLowerCase()];
873 if ((y != null) && (y.dns == null)) { return parent.config.domains[x[1].toLowerCase()]; }
874 return parent.config.domains[''];
875 }
876
877 function parseAllowedFramingOrigins(val) {
878 if (val == null) return [];
879 var arr = [];
880 if (Array.isArray(val)) { arr = val.slice(); } else if (typeof val == 'string') { arr = val.split(',').map(function (s) { return s.trim(); }).filter(function (s) { return s.length > 0; }); } else { return []; }
881 var out = [];
882 for (var i = 0; i < arr.length; i++) {
883 var o = arr[i].trim().replace(/\/+$/, '');
884 if (o.length === 0) continue;
885 if (o.indexOf('https://') === 0 || o.indexOf('http://') === 0) { out.push(o); }
886 }
887 return out;
888 }
889
890 function handleLogoutRequest(req, res) {
891 const domain = checkUserIpAddress(req, res);
892 if (domain == null) { return; }
893 if (domain.auth == 'sspi') { parent.debug('web', 'handleLogoutRequest: failed checks.'); res.sendStatus(404); return; }
894 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
895
896 // If a HTTP header is required, check new UserRequiredHttpHeader
897 if (domain.userrequiredhttpheader && (typeof domain.userrequiredhttpheader == 'object')) { var ok = false; for (var i in req.headers) { if (domain.userrequiredhttpheader[i.toLowerCase()] == req.headers[i]) { ok = true; } } if (ok == false) { res.sendStatus(404); return; } }
898
899 res.set({ 'Cache-Control': 'no-store' });
900 // Destroy the user's session to log them out will be re-created next request
901 var userid = req.session.userid;
902 if (req.session.userid) {
903 var user = obj.users[req.session.userid];
904 if (user != null) {
905 obj.parent.authLog('https', 'User ' + user.name + ' logout from ' + req.clientIp + ' port ' + req.connection.remotePort, { sessionid: req.session.x, useragent: req.headers['user-agent'] });
906 obj.parent.DispatchEvent(['*'], obj, { etype: 'user', userid: user._id, username: user.name, action: 'logout', msgid: 2, msg: 'Account logout', domain: domain.id });
907 }
908 if (req.session.x) { clearDestroyedSessions(); obj.destroyedSessions[req.session.userid + '/' + req.session.x] = Date.now(); } // Destroy this session
909 }
910 req.session = null;
911 parent.debug('web', 'handleLogoutRequest: success.');
912
913 // If this user was logged in using an authentication strategy and there is a logout URL, use it.
914 if ((userid != null) && (domain.authstrategies?.authStrategyFlags != null)) {
915 let logouturl = null;
916 let userStrategy = ((userid.split('/')[2]).split(':')[0]).substring(1);
917 // Setup logout url for oidc
918 if (userStrategy == 'oidc' && domain.authstrategies.oidc != null) {
919 if (typeof domain.authstrategies.oidc.logouturl == 'string') {
920 logouturl = domain.authstrategies.oidc.logouturl;
921 } else if (typeof domain.authstrategies.oidc.issuer.end_session_endpoint == 'string' && typeof domain.authstrategies.oidc.client.post_logout_redirect_uri == 'string') {
922 logouturl = domain.authstrategies.oidc.issuer.end_session_endpoint + (domain.authstrategies.oidc.issuer.end_session_endpoint.indexOf('?') == -1 ? '?' : '&') + 'post_logout_redirect_uri=' + domain.authstrategies.oidc.client.post_logout_redirect_uri;
923 } else if (typeof domain.authstrategies.oidc.issuer.end_session_endpoint == 'string') {
924 logouturl = domain.authstrategies.oidc.issuer.end_session_endpoint;
925 }
926 // Log out all other strategies
927 } else if ((domain.authstrategies[userStrategy] != null) && (typeof domain.authstrategies[userStrategy].logouturl == 'string')) { logouturl = domain.authstrategies[userStrategy].logouturl; }
928 // If custom logout was setup, use it
929 if (logouturl != null) {
930 parent.authLog('handleLogoutRequest', userStrategy.toUpperCase() + ': LOGOUT: ' + logouturl);
931 res.redirect(logouturl);
932 return;
933 }
934 }
935
936 // This is the default logout redirect to the login page
937 if (req.query.key != null) { res.redirect(domain.url + 'login?key=' + encodeURIComponent(req.query.key)); } else { res.redirect(domain.url + 'login'); }
938 }
939
940 // Return an object with 2FA type if 2-step auth can be skipped
941 function checkUserOneTimePasswordSkip(domain, user, req, loginOptions) {
942 if (parent.config.settings.no2factorauth == true) return null;
943
944 // If this login occurred using a login token, no 2FA needed.
945 if ((loginOptions != null) && (typeof loginOptions.tokenName === 'string')) { return { twoFactorType: 'tokenlogin' }; }
946
947 // Check if we can skip 2nd factor auth because of the source IP address
948 if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
949 for (var i in domain.passwordrequirements.skip2factor) { if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) { return { twoFactorType: 'ipaddr' }; } }
950 }
951
952 // Check if a 2nd factor cookie is present
953 if (typeof req.headers.cookie == 'string') {
954 const cookies = req.headers.cookie.split('; ');
955 for (var i in cookies) {
956 if (cookies[i].startsWith('twofactor=')) {
957 var twoFactorCookie = obj.parent.decodeCookie(decodeURIComponent(cookies[i].substring(10)), obj.parent.loginCookieEncryptionKey, (30 * 24 * 60)); // If the cookies does not have an expire field, assume 30 day timeout.
958 if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { return { twoFactorType: 'cookie' }; }
959 }
960 }
961 }
962
963 return null;
964 }
965
966 // Return true if this user has 2-step auth active
967 function checkUserOneTimePasswordRequired(domain, user, req, loginOptions) {
968 // If this login occurred using a login token, no 2FA needed.
969 if ((loginOptions != null) && (typeof loginOptions.tokenName === 'string')) { return false; }
970
971 // Check if we can skip 2nd factor auth because of the source IP address
972 if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
973 for (var i in domain.passwordrequirements.skip2factor) { if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) return false; }
974 }
975
976 // Check if a 2nd factor cookie is present
977 if (typeof req.headers.cookie == 'string') {
978 const cookies = req.headers.cookie.split('; ');
979 for (var i in cookies) {
980 if (cookies[i].startsWith('twofactor=')) {
981 var twoFactorCookie = obj.parent.decodeCookie(decodeURIComponent(cookies[i].substring(10)), obj.parent.loginCookieEncryptionKey, (30 * 24 * 60)); // If the cookies does not have an expire field, assume 30 day timeout.
982 if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { return false; }
983 }
984 }
985 }
986
987 // See if SMS 2FA is available
988 var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
989
990 // See if Messenger 2FA is available
991 var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
992
993 // Check if a 2nd factor is present
994 return ((parent.config.settings.no2factorauth !== true) && (msg2fa || sms2fa || (user.otpsecret != null) || ((user.email != null) && (user.emailVerified == true) && (domain.mailserver != null) && (user.otpekey != null)) || (user.otpduo != null) || ((user.otphkeys != null) && (user.otphkeys.length > 0))));
995 }
996
997 // Check the 2-step auth token
998 function checkUserOneTimePassword(req, domain, user, token, hwtoken, func) {
999 parent.debug('web', 'checkUserOneTimePassword()');
1000 const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.nousers !== true) && (parent.config.settings.no2factorauth !== true));
1001 if (twoStepLoginSupported == false) { parent.debug('web', 'checkUserOneTimePassword: not supported.'); func(true); return; };
1002
1003 // Check if we can use OTP tokens with email
1004 var otpemail = (domain.mailserver != null);
1005 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; }
1006 var otpsms = (parent.smsserver != null);
1007 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; }
1008 var otpmsg = ((parent.msgserver != null) && (parent.msgserver.providers != 0));
1009 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.msg2factor == false)) { otpmsg = false; }
1010
1011 // Check 2FA login cookie
1012 if ((token != null) && (token.startsWith('cookie='))) {
1013 var twoFactorCookie = obj.parent.decodeCookie(decodeURIComponent(token.substring(7)), obj.parent.loginCookieEncryptionKey, (30 * 24 * 60)); // If the cookies does not have an expire field, assume 30 day timeout.
1014 if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { func(true, { twoFactorType: 'cookie' }); return; }
1015 }
1016
1017 // Check email key
1018 if ((otpemail) && (user.otpekey != null) && (user.otpekey.d != null) && (user.otpekey.k === token)) {
1019 var deltaTime = (Date.now() - user.otpekey.d);
1020 if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the email token (10000 * 60 * 5).
1021 user.otpekey = {};
1022 obj.db.SetUser(user);
1023 parent.debug('web', 'checkUserOneTimePassword: success (email).');
1024 func(true, { twoFactorType: 'email' });
1025 return;
1026 }
1027 }
1028
1029 // Check SMS key
1030 if ((otpsms) && (user.phone != null) && (user.otpsms != null) && (user.otpsms.d != null) && (user.otpsms.k === token)) {
1031 var deltaTime = (Date.now() - user.otpsms.d);
1032 if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the SMS token (10000 * 60 * 5).
1033 delete user.otpsms;
1034 obj.db.SetUser(user);
1035 parent.debug('web', 'checkUserOneTimePassword: success (SMS).');
1036 func(true, { twoFactorType: 'sms' });
1037 return;
1038 }
1039 }
1040
1041 // Check messenger key
1042 if ((otpmsg) && (user.msghandle != null) && (user.otpmsg != null) && (user.otpmsg.d != null) && (user.otpmsg.k === token)) {
1043 var deltaTime = (Date.now() - user.otpmsg.d);
1044 if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the Messenger token (10000 * 60 * 5).
1045 delete user.otpmsg;
1046 obj.db.SetUser(user);
1047 parent.debug('web', 'checkUserOneTimePassword: success (Messenger).');
1048 func(true, { twoFactorType: 'messenger' });
1049 return;
1050 }
1051 }
1052
1053 // Check hardware key
1054 if (user.otphkeys && (user.otphkeys.length > 0) && (typeof (hwtoken) == 'string') && (hwtoken.length > 0)) {
1055 var authResponse = null;
1056 try { authResponse = JSON.parse(hwtoken); } catch (ex) { }
1057 if ((authResponse != null) && (authResponse.clientDataJSON)) {
1058 // Get all WebAuthn keys
1059 var webAuthnKeys = [];
1060 for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 3) { webAuthnKeys.push(user.otphkeys[i]); } }
1061 if (webAuthnKeys.length > 0) {
1062 // Decode authentication response
1063 var clientAssertionResponse = { response: {} };
1064 clientAssertionResponse.id = authResponse.id;
1065 clientAssertionResponse.rawId = Buffer.from(authResponse.id, 'base64');
1066 clientAssertionResponse.response.authenticatorData = Buffer.from(authResponse.authenticatorData, 'base64');
1067 clientAssertionResponse.response.clientDataJSON = Buffer.from(authResponse.clientDataJSON, 'base64');
1068 clientAssertionResponse.response.signature = Buffer.from(authResponse.signature, 'base64');
1069 clientAssertionResponse.response.userHandle = Buffer.from(authResponse.userHandle, 'base64');
1070
1071 // Look for the key with clientAssertionResponse.id
1072 var webAuthnKey = null;
1073 for (var i = 0; i < webAuthnKeys.length; i++) { if (webAuthnKeys[i].keyId == clientAssertionResponse.id) { webAuthnKey = webAuthnKeys[i]; } }
1074
1075 // If we found a valid key to use, let's validate the response
1076 if (webAuthnKey != null) {
1077 // Figure out the origin
1078 var httpport = ((args.aliasport != null) ? args.aliasport : args.port);
1079 var origin = 'https://' + (domain.dns ? domain.dns : parent.certificates.CommonName);
1080 if (httpport != 443) { origin += ':' + httpport; }
1081
1082 var u2fchallenge = null;
1083 if ((req.session != null) && (req.session.e != null)) { const sec = parent.decryptSessionData(req.session.e); if (sec != null) { u2fchallenge = sec.u2f; } }
1084 var assertionExpectations = {
1085 challenge: u2fchallenge,
1086 origin: origin,
1087 factor: 'either',
1088 fmt: 'fido-u2f',
1089 publicKey: webAuthnKey.publicKey,
1090 prevCounter: webAuthnKey.counter,
1091 userHandle: Buffer.from(user._id, 'binary').toString('base64')
1092 };
1093
1094 var webauthnResponse = null;
1095 try { webauthnResponse = obj.webauthn.verifyAuthenticatorAssertionResponse(clientAssertionResponse.response, assertionExpectations); } catch (ex) { parent.debug('web', 'checkUserOneTimePassword: exception ' + ex); console.log(ex); }
1096 if ((webauthnResponse != null) && (webauthnResponse.verified === true)) {
1097 // Update the hardware key counter and accept the 2nd factor
1098 webAuthnKey.counter = webauthnResponse.counter;
1099 obj.db.SetUser(user);
1100 parent.debug('web', 'checkUserOneTimePassword: success (hardware).');
1101 func(true, { twoFactorType: 'fido' });
1102 } else {
1103 parent.debug('web', 'checkUserOneTimePassword: fail (hardware).');
1104 func(false);
1105 }
1106 return;
1107 }
1108 }
1109 }
1110 }
1111
1112 // Check Google Authenticator
1113 if (user.otpsecret && (typeof (token) == 'string') && (token.length == 6)){
1114 const otplib = require('otplib');
1115 const verified = otplib.verifySync({
1116 epochTolerance: 60,
1117 token: token,
1118 secret: user.otpsecret,
1119 guardrails: otplib.createGuardrails({
1120 MIN_SECRET_BYTES: 10, // https://github.com/yeojz/otplib/issues/671#issuecomment-4368647105
1121 })
1122 });
1123 if (verified.valid === true) {
1124 parent.debug('web', 'checkUserOneTimePassword: success (authenticator).');
1125 func(true, { twoFactorType: 'otp' });
1126 return;
1127 }
1128 };
1129
1130 // Check written down keys
1131 if ((user.otpkeys != null) && (user.otpkeys.keys != null) && (typeof (token) == 'string') && (token.length == 8)) {
1132 var tokenNumber = parseInt(token);
1133 for (var i = 0; i < user.otpkeys.keys.length; i++) {
1134 if ((tokenNumber === user.otpkeys.keys[i].p) && (user.otpkeys.keys[i].u === true)) {
1135 parent.debug('web', 'checkUserOneTimePassword: success (one-time).');
1136 user.otpkeys.keys[i].u = false; func(true, { twoFactorType: 'backup' }); return;
1137 }
1138 }
1139 }
1140
1141 // Check OTP hardware key (Yubikey OTP)
1142 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)) {
1143 var keyId = token.substring(0, 12);
1144
1145 // Find a matching OTP key
1146 var match = false;
1147 for (var i = 0; i < user.otphkeys.length; i++) { if ((user.otphkeys[i].type === 2) && (user.otphkeys[i].keyid === keyId)) { match = true; } }
1148
1149 // If we have a match, check the OTP
1150 if (match === true) {
1151 var yub = require('yub');
1152 yub.init(domain.yubikey.id, domain.yubikey.secret);
1153 yub.verify(token, function (err, results) {
1154 if ((results != null) && (results.status == 'OK')) {
1155 parent.debug('web', 'checkUserOneTimePassword: success (Yubikey).');
1156 func(true, { twoFactorType: 'hwotp' });
1157 } else {
1158 parent.debug('web', 'checkUserOneTimePassword: fail (Yubikey).');
1159 func(false);
1160 }
1161 });
1162 return;
1163 }
1164 }
1165
1166 parent.debug('web', 'checkUserOneTimePassword: fail (2).');
1167 func(false);
1168 }
1169
1170 // Return a U2F hardware key challenge
1171 function getHardwareKeyChallenge(req, domain, user, func) {
1172 var sec = {};
1173 if (req.session == null) { req.session = {}; } else { try { sec = parent.decryptSessionData(req.session.e); } catch (ex) { } }
1174
1175 if (user.otphkeys && (user.otphkeys.length > 0)) {
1176 // Get all WebAuthn keys
1177 var webAuthnKeys = [];
1178 for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 3) { webAuthnKeys.push(user.otphkeys[i]); } }
1179 if (webAuthnKeys.length > 0) {
1180 // Generate a Webauthn challenge, this is really easy, no need to call any modules to do this.
1181 var authnOptions = { type: 'webAuthn', keyIds: [], timeout: 60000, challenge: obj.crypto.randomBytes(64).toString('base64') };
1182 // userVerification: 'preferred' use security pin if possible (default), 'required' always use security pin, 'discouraged' do not use security pin.
1183 authnOptions.userVerification = (domain.passwordrequirements && domain.passwordrequirements.fidopininput) ? domain.passwordrequirements.fidopininput : 'preferred'; // Use the domain setting if it exists, otherwise use 'preferred'.{
1184 for (var i = 0; i < webAuthnKeys.length; i++) { authnOptions.keyIds.push(webAuthnKeys[i].keyId); }
1185 sec.u2f = authnOptions.challenge;
1186 req.session.e = parent.encryptSessionData(sec);
1187 parent.debug('web', 'getHardwareKeyChallenge: success');
1188 func(JSON.stringify(authnOptions));
1189 return;
1190 }
1191 }
1192
1193 // Remove the challenge if present
1194 if (sec.u2f != null) { delete sec.u2f; req.session.e = parent.encryptSessionData(sec); }
1195
1196 parent.debug('web', 'getHardwareKeyChallenge: fail');
1197 func('');
1198 }
1199
1200 // Redirect a root request to a different page
1201 function handleRootRedirect(req, res, direct) {
1202 const domain = checkUserIpAddress(req, res);
1203 if (domain == null) { return; }
1204 res.redirect(domain.rootredirect + getQueryPortion(req));
1205 }
1206
1207 function handleLoginRequest(req, res, direct) {
1208 const domain = checkUserIpAddress(req, res);
1209 if (domain == null) { return; }
1210 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1211 if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
1212 if (req.session == null) { req.session = {}; }
1213
1214 // Check if this is a banned ip address
1215 if (obj.checkAllowLogin(req) == false) {
1216 // Wait and redirect the user
1217 setTimeout(function () {
1218 req.session.messageid = 114; // IP address blocked, try again later.
1219 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1220 }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
1221 return;
1222 }
1223
1224 // Normally, use the body username/password. If this is a token, use the username/password in the session.
1225 var xusername = req.body.username, xpassword = req.body.password;
1226 if ((xusername == null) && (xpassword == null) && (req.body.token != null)) {
1227 const sec = parent.decryptSessionData(req.session.e);
1228 xusername = sec.tuser; xpassword = sec.tpass;
1229 }
1230
1231 // Authenticate the user
1232 obj.authenticate(xusername, xpassword, domain, function (err, userid, passhint, loginOptions) {
1233 if (userid) {
1234 var user = obj.users[userid];
1235
1236 // Check if we are in maintenance mode
1237 if ((parent.config.settings.maintenancemode != null) && (user.siteadmin != 4294967295)) {
1238 req.session.messageid = 115; // Server under maintenance
1239 req.session.loginmode = 1;
1240 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1241 return;
1242 }
1243
1244 var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.email != null) && (user.emailVerified == true) && (user.otpekey != null));
1245 var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
1246 var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
1247 var push2fa = ((parent.firebase != null) && (user.otpdev != null));
1248 var duo2fa = ((((typeof domain.duo2factor == 'object') && (typeof domain.duo2factor.integrationkey == 'string') && (typeof domain.duo2factor.secretkey == 'string') && (typeof domain.duo2factor.apihostname == 'string')) || ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.duo2factor != false))) && (user.otpduo != null));
1249
1250 // Check if two factor can be skipped
1251 const twoFactorSkip = checkUserOneTimePasswordSkip(domain, user, req, loginOptions);
1252
1253 // Check if this user has 2-step login active
1254 if ((twoFactorSkip == null) && (req.session.loginmode != 6) && checkUserOneTimePasswordRequired(domain, user, req, loginOptions)) {
1255 if ((req.body.hwtoken == '**timeout**')) {
1256 delete req.session; // Clear the session
1257 res.redirect(domain.url + getQueryPortion(req));
1258 return;
1259 }
1260
1261 if ((req.body.hwtoken == '**email**') && email2fa) {
1262 user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
1263 obj.db.SetUser(user);
1264 parent.debug('web', 'Sending 2FA email to: ' + user.email);
1265 domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key);
1266 req.session.messageid = 2; // "Email sent" message
1267 req.session.loginmode = 4;
1268 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1269 return;
1270 }
1271
1272 if ((req.body.hwtoken == '**sms**') && sms2fa) {
1273 // Cause a token to be sent to the user's phone number
1274 user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
1275 obj.db.SetUser(user);
1276 parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
1277 parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
1278 // Ask for a login token & confirm sms was sent
1279 req.session.messageid = 4; // "SMS sent" message
1280 req.session.loginmode = 4;
1281 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1282 return;
1283 }
1284
1285 if ((req.body.hwtoken == '**msg**') && msg2fa) {
1286 // Cause a token to be sent to the user's messenger account
1287 user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
1288 obj.db.SetUser(user);
1289 parent.debug('web', 'Sending 2FA message to: ' + user.msghandle);
1290 parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req));
1291 // Ask for a login token & confirm message was sent
1292 req.session.messageid = 6; // "Message sent" message
1293 req.session.loginmode = 4;
1294 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1295 return;
1296 }
1297
1298 if ((req.body.hwtoken == '**duo**') && duo2fa && (typeof domain.duo2factor == 'object') && (typeof domain.duo2factor.integrationkey == 'string') && (typeof domain.duo2factor.secretkey == 'string') && (typeof domain.duo2factor.apihostname == 'string')) {
1299 // Redirect to duo here
1300 const duo = require('@duosecurity/duo_universal');
1301 const client = new duo.Client({
1302 clientId: domain.duo2factor.integrationkey,
1303 clientSecret: domain.duo2factor.secretkey,
1304 apiHost: domain.duo2factor.apihostname,
1305 redirectUrl: obj.generateBaseURL(domain, req) + 'auth-duo' + (domain.loginkey != null ? ('?key=' + domain.loginkey) : '')
1306 });
1307 // Decrypt any session data
1308 const sec = parent.decryptSessionData(req.session.e);
1309 sec.duostate = client.generateState();
1310 req.session.e = parent.encryptSessionData(sec);
1311 parent.debug('web', 'Redirecting user ' + user._id + ' to Duo');
1312 res.redirect(client.createAuthUrl(user._id.split('/')[2], sec.duostate));
1313 return;
1314 }
1315
1316 // Handle device push notification 2FA request
1317 // We create a browser cookie, send it back and when the browser connects it's web socket, it will trigger the push notification.
1318 if ((req.body.hwtoken == '**push**') && push2fa && ((domain.passwordrequirements == null) || (domain.passwordrequirements.push2factor != false))) {
1319 const logincodeb64 = Buffer.from(obj.common.zeroPad(getRandomSixDigitInteger(), 6)).toString('base64');
1320 const sessioncode = obj.crypto.randomBytes(24).toString('base64');
1321
1322 // Create a browser cookie so the browser can connect using websocket and wait for device accept/reject.
1323 const browserCookie = parent.encodeCookie({ a: 'waitAuth', c: logincodeb64, u: user._id, n: user.otpdev, s: sessioncode, d: domain.id });
1324
1325 // Get the HTTPS port
1326 var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port if specified
1327
1328 // Get the agent connection server name
1329 var serverName = obj.getWebServerName(domain, req);
1330 if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
1331
1332 // Build the connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
1333 var xdomain = (domain.dns == null) ? domain.id : '';
1334 if (xdomain != '') xdomain += '/';
1335 var url = 'wss://' + serverName + ':' + httpsPort + '/' + xdomain + '2fahold.ashx?c=' + browserCookie;
1336
1337 // Request that the login page wait for device auth
1338 req.session.messageid = 5; // "Sending notification..." message
1339 req.session.passhint = url;
1340 req.session.loginmode = 8;
1341 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1342 return;
1343 }
1344
1345 checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result, authData) {
1346 if (result == false) {
1347 var randomWaitTime = 0;
1348
1349 // Check if 2FA is allowed for this IP address
1350 if (obj.checkAllow2Fa(req) == false) {
1351 // Wait and redirect the user
1352 setTimeout(function () {
1353 req.session.messageid = 114; // IP address blocked, try again later.
1354 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1355 }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
1356 return;
1357 }
1358
1359 // 2-step auth is required, but the token is not present or not valid.
1360 if ((req.body.token != null) || (req.body.hwtoken != null)) {
1361 randomWaitTime = 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095); // This is a fail, wait a random time. 2 to 6 seconds.
1362 req.session.messageid = 108; // Invalid token, try again.
1363 obj.parent.authLog('https', 'Failed 2FA for ' + xusername + ' from ' + cleanRemoteAddr(req.clientIp) + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'] });
1364 parent.debug('web', 'handleLoginRequest: invalid 2FA token');
1365 const ua = obj.getUserAgentInfo(req);
1366 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] });
1367 obj.setbad2Fa(req);
1368 } else {
1369 parent.debug('web', 'handleLoginRequest: 2FA token required');
1370 }
1371
1372 // Wait and redirect the user
1373 setTimeout(function () {
1374 req.session.loginmode = 4;
1375 if ((user.email != null) && (user.emailVerified == true) && (domain.mailserver != null) && (user.otpekey != null)) { req.session.temail = 1; }
1376 if ((user.phone != null) && (parent.smsserver != null)) { req.session.tsms = 1; }
1377 if ((user.msghandle != null) && (parent.msgserver != null) && (parent.msgserver.providers != 0)) { req.session.tmsg = 1; }
1378 if ((user.otpdev != null) && (parent.firebase != null)) { req.session.tpush = 1; }
1379 if ((user.otpduo != null)) { req.session.tduo = 1; }
1380 req.session.e = parent.encryptSessionData({ tuserid: userid, tuser: xusername, tpass: xpassword });
1381 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1382 }, randomWaitTime);
1383 } else {
1384 // Check if we need to remember this device
1385 if ((req.body.remembertoken === 'on') && ((domain.twofactorcookiedurationdays == null) || (domain.twofactorcookiedurationdays > 0))) {
1386 var maxCookieAge = domain.twofactorcookiedurationdays;
1387 if (typeof maxCookieAge != 'number') { maxCookieAge = 30; }
1388 const twoFactorCookie = obj.parent.encodeCookie({ userid: user._id, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey);
1389 res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: parent.config.settings.sessionsamesite, secure: true });
1390 }
1391
1392 // Check if email address needs to be confirmed
1393 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'))
1394 if (emailcheck && (user.emailVerified !== true)) {
1395 parent.debug('web', 'Redirecting using ' + user.name + ' to email check login page');
1396 req.session.messageid = 3; // "Email verification required" message
1397 req.session.loginmode = 7;
1398 req.session.passhint = user.email;
1399 req.session.cuserid = userid;
1400 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1401 return;
1402 }
1403
1404 // Login successful
1405 parent.debug('web', 'handleLoginRequest: successful 2FA login');
1406 if (authData != null) { if (loginOptions == null) { loginOptions = {}; } loginOptions.twoFactorType = authData.twoFactorType; }
1407 completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions);
1408 }
1409 });
1410 return;
1411 }
1412
1413 // Check if email address needs to be confirmed
1414 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'))
1415 if (emailcheck && (user.emailVerified !== true)) {
1416 parent.debug('web', 'Redirecting using ' + user.name + ' to email check login page');
1417 req.session.messageid = 3; // "Email verification required" message
1418 req.session.loginmode = 7;
1419 req.session.passhint = user.email;
1420 req.session.cuserid = userid;
1421 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1422 return;
1423 }
1424
1425 // Login successful
1426 parent.debug('web', 'handleLoginRequest: successful login');
1427 if (twoFactorSkip != null) { if (loginOptions == null) { loginOptions = {}; } loginOptions.twoFactorType = twoFactorSkip.twoFactorType; }
1428 completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions);
1429 } else {
1430 // Login failed, log the error
1431 obj.parent.authLog('https', 'Failed password for ' + xusername + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'] });
1432
1433 // Wait a random delay
1434 setTimeout(function () {
1435 // If the account is locked, display that.
1436 if (typeof xusername == 'string') {
1437 var xuserid = 'user/' + domain.id + '/' + xusername.toLowerCase();
1438 if (err == 'locked') {
1439 parent.debug('web', 'handleLoginRequest: login failed, locked account');
1440 req.session.messageid = 110; // Account locked.
1441 const ua = obj.getUserAgentInfo(req);
1442 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] });
1443 obj.setbadLogin(req);
1444 } else if (err == 'denied') {
1445 parent.debug('web', 'handleLoginRequest: login failed, access denied');
1446 req.session.messageid = 111; // Access denied.
1447 const ua = obj.getUserAgentInfo(req);
1448 obj.parent.DispatchEvent(['*', 'server-users', xuserid], obj, { action: 'authfail', userid: xuserid, username: xusername, domain: domain.id, msg: 'Denied user login from ' + req.clientIp, msgid: 155, msgArgs: [req.clientIp, ua.browserStr, ua.osStr] });
1449 obj.setbadLogin(req);
1450 } else {
1451 parent.debug('web', 'handleLoginRequest: login failed, bad username and password');
1452 req.session.messageid = 112; // Login failed, check username and password.
1453 const ua = obj.getUserAgentInfo(req);
1454 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] });
1455 obj.setbadLogin(req);
1456 }
1457 }
1458
1459 // Clean up login mode and display password hint if present.
1460 delete req.session.loginmode;
1461 if ((passhint != null) && (passhint.length > 0)) {
1462 req.session.passhint = passhint;
1463 } else {
1464 delete req.session.passhint;
1465 }
1466
1467 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1468 }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095)); // Wait for 2 to ~6 seconds.
1469 }
1470 });
1471 }
1472
1473 function completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions) {
1474 // Check if we need to change the password
1475 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))))) {
1476 // Request a password change
1477 parent.debug('web', 'handleLoginRequest: login ok, password change requested');
1478 req.session.loginmode = 6;
1479 req.session.messageid = 113; // Password change requested.
1480
1481 // Decrypt any session data
1482 const sec = parent.decryptSessionData(req.session.e);
1483 sec.rtuser = xusername;
1484 sec.rtpass = xpassword;
1485 sec.rtreset = true;
1486 req.session.e = parent.encryptSessionData(sec);
1487
1488 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1489 return;
1490 }
1491
1492 // Save login time
1493 user.pastlogin = user.login;
1494 user.login = user.access = Math.floor(Date.now() / 1000);
1495 obj.db.SetUser(user);
1496
1497 // Notify account login
1498 const targets = ['*', 'server-users', user._id];
1499 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
1500 const ua = obj.getUserAgentInfo(req);
1501 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 from ' + req.clientIp + ', ' + ua.browserStr + ', ' + ua.osStr, domain: domain.id, ip: req.clientIp, userAgent: req.headers['user-agent'], rport: req.connection.remotePort };
1502 if (loginOptions != null) {
1503 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.
1504 if (loginOptions.twoFactorType != null) { loginEvent.twoFactorType = loginOptions.twoFactorType; }
1505 }
1506 obj.parent.DispatchEvent(targets, obj, loginEvent);
1507
1508 // Regenerate session when signing in to prevent fixation
1509 //req.session.regenerate(function () {
1510 // Store the user's primary key in the session store to be retrieved, or in this case the entire user object
1511 delete req.session.e;
1512 delete req.session.u2f;
1513 delete req.session.loginmode;
1514 delete req.session.tuserid;
1515 delete req.session.tuser;
1516 delete req.session.tpass;
1517 delete req.session.temail;
1518 delete req.session.tsms;
1519 delete req.session.tmsg;
1520 delete req.session.tduo;
1521 delete req.session.tpush;
1522 delete req.session.messageid;
1523 delete req.session.passhint;
1524 delete req.session.cuserid;
1525 delete req.session.expire;
1526 delete req.session.currentNode;
1527 req.session.userid = userid;
1528 req.session.ip = req.clientIp;
1529 setSessionRandom(req);
1530 obj.parent.authLog('https', 'Accepted password for ' + (xusername ? xusername : userid) + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'], sessionid: req.session.x });
1531
1532 // If a login token was used, add this information and expire time to the session.
1533 if ((loginOptions != null) && (loginOptions.tokenName != null) && (loginOptions.tokenUser != null)) {
1534 req.session.loginToken = loginOptions.tokenUser;
1535 if (loginOptions.expire != null) { req.session.expire = loginOptions.expire; }
1536 }
1537
1538 if (req.body.viewmode) { req.session.viewmode = req.body.viewmode; }
1539 if (req.body.host) {
1540 // TODO: This is a terrible search!!! FIX THIS.
1541 /*
1542 obj.db.GetAllType('node', function (err, docs) {
1543 for (var i = 0; i < docs.length; i++) {
1544 if (docs[i].name == req.body.host) {
1545 req.session.currentNode = docs[i]._id;
1546 break;
1547 }
1548 }
1549 console.log("CurrentNode: " + req.session.currentNode);
1550 // This redirect happens after finding node is completed
1551 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1552 });
1553 */
1554 parent.debug('web', 'handleLoginRequest: login ok (1)');
1555 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } // Temporary
1556 } else {
1557 parent.debug('web', 'handleLoginRequest: login ok (2)');
1558 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1559 }
1560 //});
1561 }
1562
1563 function handleCreateAccountRequest(req, res, direct) {
1564 const domain = checkUserIpAddress(req, res);
1565 if (domain == null) { return; }
1566 if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handleCreateAccountRequest: failed checks.'); res.sendStatus(404); return; }
1567 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1568 if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
1569 if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
1570
1571 // Check if we are in maintenance mode
1572 if (parent.config.settings.maintenancemode != null) {
1573 req.session.messageid = 115; // Server under maintenance
1574 req.session.loginmode = 1;
1575 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1576 return;
1577 }
1578
1579 // Always lowercase the email address
1580 if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
1581
1582 // If the email is the username, set this here.
1583 if (domain.usernameisemail) { req.body.username = req.body.email; }
1584
1585 // Check if there is domain.newAccountToken, check if supplied token is valid
1586 if ((domain.newaccountspass != null) && (domain.newaccountspass != '') && (req.body.newaccountspass != domain.newaccountspass)) {
1587 parent.debug('web', 'handleCreateAccountRequest: Invalid account creation token');
1588 req.session.loginmode = 2;
1589 req.session.messageid = 103; // Invalid account creation token.
1590 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1591 return;
1592 }
1593
1594 // If needed, check the new account creation CAPTCHA
1595 if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
1596 const c = parent.decodeCookie(req.body.captchaargs, parent.loginCookieEncryptionKey, 10); // 10 minute timeout
1597 if ((c == null) || (c.type != 'newAccount') || (typeof c.captcha != 'string') || (c.captcha.length < 5) || (c.captcha != req.body.anewaccountcaptcha)) {
1598 req.session.loginmode = 2;
1599 req.session.messageid = 117; // Invalid security check
1600 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1601 return;
1602 }
1603 }
1604
1605 // Accounts that start with ~ are not allowed
1606 if ((typeof req.body.username != 'string') || (req.body.username.length < 1) || (req.body.username[0] == '~')) {
1607 parent.debug('web', 'handleCreateAccountRequest: unable to create account (0)');
1608 req.session.loginmode = 2;
1609 req.session.messageid = 100; // Unable to create account.
1610 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1611 return;
1612 }
1613
1614 // Count the number of users in this domain
1615 var domainUserCount = 0;
1616 for (var i in obj.users) { if (obj.users[i].domain == domain.id) { domainUserCount++; } }
1617
1618 // Check if we are allowed to create new users using the login screen
1619 if ((domain.newaccounts !== 1) && (domain.newaccounts !== true) && (domainUserCount > 0)) {
1620 parent.debug('web', 'handleCreateAccountRequest: domainUserCount > 1.');
1621 res.sendStatus(401);
1622 return;
1623 }
1624
1625 // Check if this request is for an allows email domain
1626 if ((domain.newaccountemaildomains != null) && Array.isArray(domain.newaccountemaildomains)) {
1627 var i = -1;
1628 if (typeof req.body.email == 'string') { i = req.body.email.indexOf('@'); }
1629 if (i == -1) {
1630 parent.debug('web', 'handleCreateAccountRequest: unable to create account (1)');
1631 req.session.loginmode = 2;
1632 req.session.messageid = 100; // Unable to create account.
1633 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1634 return;
1635 }
1636 var emailok = false, emaildomain = req.body.email.substring(i + 1).toLowerCase();
1637 for (var i in domain.newaccountemaildomains) { if (emaildomain == domain.newaccountemaildomains[i].toLowerCase()) { emailok = true; } }
1638 if (emailok == false) {
1639 parent.debug('web', 'handleCreateAccountRequest: unable to create account (2)');
1640 req.session.loginmode = 2;
1641 req.session.messageid = 100; // Unable to create account.
1642 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1643 return;
1644 }
1645 }
1646
1647 // Check if we exceed the maximum number of user accounts
1648 obj.db.isMaxType(domain.limits.maxuseraccounts, 'user', domain.id, function (maxExceed) {
1649 if (maxExceed) {
1650 parent.debug('web', 'handleCreateAccountRequest: account limit reached');
1651 req.session.loginmode = 2;
1652 req.session.messageid = 101; // Account limit reached.
1653 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1654 } else {
1655 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)) {
1656 parent.debug('web', 'handleCreateAccountRequest: unable to create account (3)');
1657 req.session.loginmode = 2;
1658 req.session.messageid = 100; // Unable to create account.
1659 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1660 } else {
1661 // Check if this email was already verified
1662 obj.db.GetUserWithVerifiedEmail(domain.id, req.body.email, function (err, docs) {
1663 if ((docs != null) && (docs.length > 0)) {
1664 parent.debug('web', 'handleCreateAccountRequest: Existing account with this email address');
1665 req.session.loginmode = 2;
1666 req.session.messageid = 102; // Existing account with this email address.
1667 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1668 } else {
1669 // Check if user exists
1670 if (obj.users['user/' + domain.id + '/' + req.body.username.toLowerCase()]) {
1671 parent.debug('web', 'handleCreateAccountRequest: Username already exists');
1672 req.session.loginmode = 2;
1673 req.session.messageid = 104; // Username already exists.
1674 } else {
1675 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 };
1676 if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; }
1677 if (obj.common.validateStrArray(domain.newaccountrealms)) { user.groups = domain.newaccountrealms; }
1678 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; }
1679 if (domainUserCount == 0) { user.siteadmin = 4294967295; /*if (domain.newaccounts === 2) { delete domain.newaccounts; }*/ } // If this is the first user, give the account site admin.
1680
1681 // Auto-join any user groups
1682 if (typeof domain.newaccountsusergroups == 'object') {
1683 for (var i in domain.newaccountsusergroups) {
1684 var ugrpid = domain.newaccountsusergroups[i];
1685 if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
1686 var ugroup = obj.userGroups[ugrpid];
1687 if (ugroup != null) {
1688 // Add group to the user
1689 if (user.links == null) { user.links = {}; }
1690 user.links[ugroup._id] = { rights: 1 };
1691
1692 // Add user to the group
1693 ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
1694 db.Set(ugroup);
1695
1696 // Notify user group change
1697 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 };
1698 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.
1699 parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
1700 }
1701 }
1702 }
1703
1704 obj.users[user._id] = user;
1705 req.session.userid = user._id;
1706 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
1707 setSessionRandom(req);
1708 // Create a user, generate a salt and hash the password
1709 require('./pass').hash(req.body.password1, function (err, salt, hash, tag) {
1710 if (err) throw err;
1711 user.salt = salt;
1712 user.hash = hash;
1713 delete user.passtype;
1714 obj.db.SetUser(user);
1715
1716 // Send the verification email
1717 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); }
1718 }, 0);
1719 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 };
1720 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.
1721 obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
1722 }
1723 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1724 }
1725 });
1726 }
1727 }
1728 });
1729 }
1730
1731 // Called to process an account password reset
1732 function handleResetPasswordRequest(req, res, direct) {
1733 const domain = checkUserIpAddress(req, res);
1734 if (domain == null) { return; }
1735 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1736 if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
1737 if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
1738
1739 // Decrypt any session data
1740 const sec = parent.decryptSessionData(req.session.e);
1741
1742 // Check everything is ok
1743 const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false) || (sec.rtreset === true));
1744 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 sec.rtuser != 'string') || (typeof sec.rtpass != 'string')) {
1745 parent.debug('web', 'handleResetPasswordRequest: checks failed');
1746 delete req.session.e;
1747 delete req.session.u2f;
1748 delete req.session.loginmode;
1749 delete req.session.tuserid;
1750 delete req.session.tuser;
1751 delete req.session.tpass;
1752 delete req.session.temail;
1753 delete req.session.tsms;
1754 delete req.session.tmsg;
1755 delete req.session.tpush;
1756 delete req.session.messageid;
1757 delete req.session.passhint;
1758 delete req.session.cuserid;
1759 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1760 return;
1761 }
1762
1763 // Authenticate the user
1764 obj.authenticate(sec.rtuser, sec.rtpass, domain, function (err, userid, passhint, loginOptions) {
1765 if (userid) {
1766 // Login
1767 var user = obj.users[userid];
1768
1769 // If we have password requirements, check this here.
1770 if (!obj.common.checkPasswordRequirements(req.body.rpassword1, domain.passwordrequirements)) {
1771 parent.debug('web', 'handleResetPasswordRequest: password rejected, use a different one (1)');
1772 req.session.loginmode = 6;
1773 req.session.messageid = 105; // Password rejected, use a different one.
1774 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1775 return;
1776 }
1777
1778 // Check if the password is the same as a previous one
1779 obj.checkOldUserPasswords(domain, user, req.body.rpassword1, function (result) {
1780 if (result != 0) {
1781 // This is the same password as an older one, request a password change again
1782 parent.debug('web', 'handleResetPasswordRequest: password rejected, use a different one (2)');
1783 req.session.loginmode = 6;
1784 req.session.messageid = 105; // Password rejected, use a different one.
1785 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1786 } else {
1787 // Update the password, use a different salt.
1788 require('./pass').hash(req.body.rpassword1, function (err, salt, hash, tag) {
1789 const nowSeconds = Math.floor(Date.now() / 1000);
1790 if (err) { parent.debug('web', 'handleResetPasswordRequest: hash error.'); throw err; }
1791
1792 if (domain.passwordrequirements != null) {
1793 // Save password hint if this feature is enabled
1794 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; }
1795
1796 // Save previous password if this feature is enabled
1797 if ((typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
1798 if (user.oldpasswords == null) { user.oldpasswords = []; }
1799 user.oldpasswords.push({ salt: user.salt, hash: user.hash, start: user.passchange, end: nowSeconds });
1800 const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
1801 if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
1802 }
1803 }
1804
1805 user.salt = salt;
1806 user.hash = hash;
1807 user.passchange = user.access = nowSeconds;
1808 delete user.passtype;
1809 obj.db.SetUser(user);
1810
1811 // Event the account change
1812 var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'User password reset', domain: domain.id };
1813 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.
1814 obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
1815
1816 // Login successful
1817 parent.debug('web', 'handleResetPasswordRequest: success');
1818 req.session.userid = userid;
1819 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
1820 setSessionRandom(req);
1821 const sec = parent.decryptSessionData(req.session.e);
1822 completeLoginRequest(req, res, domain, obj.users[userid], userid, sec.tuser, sec.tpass, direct, loginOptions);
1823 }, 0);
1824 }
1825 }, 0);
1826 } else {
1827 // Failed, error out.
1828 parent.debug('web', 'handleResetPasswordRequest: failed authenticate()');
1829 delete req.session.e;
1830 delete req.session.u2f;
1831 delete req.session.loginmode;
1832 delete req.session.tuserid;
1833 delete req.session.tuser;
1834 delete req.session.tpass;
1835 delete req.session.temail;
1836 delete req.session.tsms;
1837 delete req.session.tmsg;
1838 delete req.session.tpush;
1839 delete req.session.messageid;
1840 delete req.session.passhint;
1841 delete req.session.cuserid;
1842 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1843 return;
1844 }
1845 });
1846 }
1847
1848 // Called to process an account reset request
1849 function handleResetAccountRequest(req, res, direct) {
1850 const domain = checkUserIpAddress(req, res);
1851 if (domain == null) { return; }
1852 const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false));
1853 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; }
1854 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1855 if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
1856 if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
1857
1858 // Always lowercase the email address
1859 if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
1860
1861 // Get the email from the body or session.
1862 var email = req.body.email;
1863 if ((email == null) || (email == '')) { email = req.session.temail; }
1864
1865 // Check the email string format
1866 if (!email || checkEmail(email) == false) {
1867 parent.debug('web', 'handleResetAccountRequest: Invalid email');
1868 req.session.loginmode = 3;
1869 req.session.messageid = 106; // Invalid email.
1870 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1871 } else {
1872 obj.db.GetUserWithVerifiedEmail(domain.id, email, function (err, docs) {
1873 // Remove all accounts that start with ~ since they are special accounts.
1874 var cleanDocs = [];
1875 if ((err == null) && (docs.length > 0)) {
1876 for (var i in docs) {
1877 const user = docs[i];
1878 const locked = ((user.siteadmin != null) && (user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)); // No password recovery for locked accounts
1879 const specialAccount = (user._id.split('/')[2].startsWith('~')); // No password recovery for special accounts
1880 if ((specialAccount == false) && (locked == false)) { cleanDocs.push(user); }
1881 }
1882 }
1883 docs = cleanDocs;
1884
1885 // Check if we have any account that match this email address
1886 if ((err != null) || (docs.length == 0)) {
1887 parent.debug('web', 'handleResetAccountRequest: Account not found');
1888 req.session.loginmode = 3;
1889 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.
1890 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1891 } else {
1892 // 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.
1893 var responseSent = false;
1894 for (var i in docs) {
1895 var user = docs[i];
1896 if (checkUserOneTimePasswordRequired(domain, user, req) == true) {
1897 // Second factor setup, request it now.
1898 checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result, authData) {
1899 if (result == false) {
1900 if (i == 0) {
1901
1902 // Check if 2FA is allowed for this IP address
1903 if (obj.checkAllow2Fa(req) == false) {
1904 // Wait and redirect the user
1905 setTimeout(function () {
1906 req.session.messageid = 114; // IP address blocked, try again later.
1907 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1908 }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
1909 return;
1910 }
1911
1912 // 2-step auth is required, but the token is not present or not valid.
1913 parent.debug('web', 'handleResetAccountRequest: Invalid 2FA token, try again');
1914 if ((req.body.token != null) || (req.body.hwtoken != null)) {
1915 var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
1916 var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
1917 if ((req.body.hwtoken == '**sms**') && sms2fa) {
1918 // Cause a token to be sent to the user's phone number
1919 user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
1920 obj.db.SetUser(user);
1921 parent.debug('web', 'Sending 2FA SMS for password recovery to: ' + user.phone);
1922 parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
1923 req.session.messageid = 4; // SMS sent.
1924 } else if ((req.body.hwtoken == '**msg**') && msg2fa) {
1925 // Cause a token to be sent to the user's messager account
1926 user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
1927 obj.db.SetUser(user);
1928 parent.debug('web', 'Sending 2FA message for password recovery to: ' + user.msghandle);
1929 parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req));
1930 req.session.messageid = 6; // Message sent.
1931 } else {
1932 req.session.messageid = 108; // Invalid token, try again.
1933 const ua = obj.getUserAgentInfo(req);
1934 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] });
1935 obj.setbad2Fa(req);
1936 }
1937 }
1938 req.session.loginmode = 5;
1939 req.session.temail = email;
1940 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1941 }
1942 } else {
1943 // Send email to perform recovery.
1944 delete req.session.temail;
1945 if (domain.mailserver != null) {
1946 domain.mailserver.sendAccountResetMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
1947 if (i == 0) {
1948 parent.debug('web', 'handleResetAccountRequest: Hold on, reset mail sent.');
1949 req.session.loginmode = 1;
1950 req.session.messageid = 1; // If valid, reset mail sent.
1951 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1952 }
1953 } else {
1954 if (i == 0) {
1955 parent.debug('web', 'handleResetAccountRequest: Unable to sent email.');
1956 req.session.loginmode = 3;
1957 req.session.messageid = 109; // Unable to sent email.
1958 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1959 }
1960 }
1961 }
1962 });
1963 } else {
1964 // No second factor, send email to perform recovery.
1965 if (domain.mailserver != null) {
1966 domain.mailserver.sendAccountResetMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
1967 if (i == 0) {
1968 parent.debug('web', 'handleResetAccountRequest: Hold on, reset mail sent.');
1969 req.session.loginmode = 1;
1970 req.session.messageid = 1; // If valid, reset mail sent.
1971 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1972 }
1973 } else {
1974 if (i == 0) {
1975 parent.debug('web', 'handleResetAccountRequest: Unable to sent email.');
1976 req.session.loginmode = 3;
1977 req.session.messageid = 109; // Unable to sent email.
1978 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1979 }
1980 }
1981 }
1982 }
1983 }
1984 });
1985 }
1986 }
1987
1988 // Handle account email change and email verification request
1989 function handleCheckAccountEmailRequest(req, res, direct) {
1990 const domain = checkUserIpAddress(req, res);
1991 if (domain == null) { return; }
1992 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; }
1993 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1994 if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
1995 if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
1996
1997 // Always lowercase the email address
1998 if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
1999
2000 // Get the email from the body or session.
2001 var email = req.body.email;
2002 if ((email == null) || (email == '')) { email = req.session.temail; }
2003
2004 // Check if this request is for an allows email domain
2005 if ((domain.newaccountemaildomains != null) && Array.isArray(domain.newaccountemaildomains)) {
2006 var i = -1;
2007 if (typeof req.body.email == 'string') { i = req.body.email.indexOf('@'); }
2008 if (i == -1) {
2009 parent.debug('web', 'handleCreateAccountRequest: unable to create account (1)');
2010 req.session.loginmode = 7;
2011 req.session.messageid = 106; // Invalid email.
2012 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2013 return;
2014 }
2015 var emailok = false, emaildomain = req.body.email.substring(i + 1).toLowerCase();
2016 for (var i in domain.newaccountemaildomains) { if (emaildomain == domain.newaccountemaildomains[i].toLowerCase()) { emailok = true; } }
2017 if (emailok == false) {
2018 parent.debug('web', 'handleCreateAccountRequest: unable to create account (2)');
2019 req.session.loginmode = 7;
2020 req.session.messageid = 106; // Invalid email.
2021 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2022 return;
2023 }
2024 }
2025
2026 // Check the email string format
2027 if (!email || checkEmail(email) == false) {
2028 parent.debug('web', 'handleCheckAccountEmailRequest: Invalid email');
2029 req.session.loginmode = 7;
2030 req.session.messageid = 106; // Invalid email.
2031 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2032 } else {
2033 // Check is email already exists
2034 obj.db.GetUserWithVerifiedEmail(domain.id, email, function (err, docs) {
2035 if ((err != null) || ((docs.length > 0) && (docs.find(function (u) { return (u._id === req.session.cuserid); }) < 0))) {
2036 // Email already exists
2037 req.session.messageid = 102; // Existing account with this email address.
2038 } else {
2039 // Update the user and notify of user email address change
2040 var user = obj.users[req.session.cuserid];
2041 if (user.email != email) {
2042 user.email = email;
2043 db.SetUser(user);
2044 var targets = ['*', 'server-users', user._id];
2045 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
2046 var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Account changed: ' + user.name, domain: domain.id };
2047 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.
2048 parent.DispatchEvent(targets, obj, event);
2049 }
2050
2051 // Send the verification email
2052 domain.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
2053
2054 // Send the response
2055 req.session.messageid = 2; // Email sent.
2056 }
2057 req.session.loginmode = 7;
2058 delete req.session.cuserid;
2059 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2060 });
2061 }
2062 }
2063
2064 // Called to process a web based email verification request
2065 function handleCheckMailRequest(req, res) {
2066 const domain = checkUserIpAddress(req, res);
2067 if (domain == null) { return; }
2068 if ((domain.auth == 'sspi') || (domain.auth == 'ldap') || (domain.mailserver == null)) { parent.debug('web', 'handleCheckMailRequest: failed checks.'); res.sendStatus(404); return; }
2069 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2070
2071 if (req.query.c != null) {
2072 var cookie = obj.parent.decodeCookie(req.query.c, domain.mailserver.mailCookieEncryptionKey, 30);
2073 if ((cookie != null) && (cookie.u != null) && (cookie.u.startsWith('user/')) && (cookie.e != null)) {
2074 var idsplit = cookie.u.split('/');
2075 if ((idsplit.length != 3) || (idsplit[1] != domain.id)) {
2076 parent.debug('web', 'handleCheckMailRequest: Invalid domain.');
2077 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));
2078 } else {
2079 obj.db.Get(cookie.u, function (err, docs) {
2080 if (docs.length == 0) {
2081 parent.debug('web', 'handleCheckMailRequest: Invalid username.');
2082 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));
2083 } else {
2084 var user = docs[0];
2085 if (user.email != cookie.e) {
2086 parent.debug('web', 'handleCheckMailRequest: Invalid e-mail.');
2087 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));
2088 } else {
2089 if (cookie.a == 1) {
2090 // Account email verification
2091 if (user.emailVerified == true) {
2092 parent.debug('web', 'handleCheckMailRequest: email already verified.');
2093 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));
2094 } else {
2095 obj.db.GetUserWithVerifiedEmail(domain.id, user.email, function (err, docs) {
2096 if ((docs.length > 0) && (docs.find(function (u) { return (u._id === user._id); }) < 0)) {
2097 parent.debug('web', 'handleCheckMailRequest: email already in use.');
2098 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));
2099 } else {
2100 parent.debug('web', 'handleCheckMailRequest: email verification success.');
2101
2102 // Set the verified flag
2103 obj.users[user._id].emailVerified = true;
2104 user.emailVerified = true;
2105 obj.db.SetUser(user);
2106
2107 // Event the change
2108 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 };
2109 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.
2110 obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
2111
2112 // Send the confirmation page
2113 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));
2114
2115 // Send a notification
2116 obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: 'Email verified', value: user.email, nolog: 1, id: Math.random() });
2117
2118 // Send to authLog
2119 obj.parent.authLog('https', 'Verified email address ' + user.email + ' for user ' + user.name, { useragent: req.headers['user-agent'] });
2120 }
2121 });
2122 }
2123 } else if (cookie.a == 2) {
2124 // Account reset
2125 if (user.emailVerified != true) {
2126 parent.debug('web', 'handleCheckMailRequest: email not verified.');
2127 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));
2128 } else {
2129 if (req.query.confirm == 1) {
2130 // Set a temporary password
2131 obj.crypto.randomBytes(16, function (err, buf) {
2132 var newpass = buf.toString('base64').split('=').join('').split('/').join('').split('+').join('');
2133 require('./pass').hash(newpass, function (err, salt, hash, tag) {
2134 if (err) throw err;
2135
2136 // Change the password
2137 var userinfo = obj.users[user._id];
2138 userinfo.salt = salt;
2139 userinfo.hash = hash;
2140 delete userinfo.passtype;
2141 userinfo.passchange = userinfo.access = Math.floor(Date.now() / 1000);
2142 delete userinfo.passhint;
2143 obj.db.SetUser(userinfo);
2144
2145 // Event the change
2146 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 };
2147 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.
2148 obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
2149
2150 // Send the new password
2151 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));
2152 parent.debug('web', 'handleCheckMailRequest: send temporary password.');
2153
2154 // Send to authLog
2155 obj.parent.authLog('https', 'Performed account reset for user ' + user.name);
2156 }, 0);
2157 });
2158 } else {
2159 // Display a link for the user to confirm password reset
2160 // 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.
2161 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));
2162 }
2163 }
2164 } else {
2165 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));
2166 }
2167 }
2168 }
2169 });
2170 }
2171 } else {
2172 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));
2173 }
2174 }
2175 }
2176
2177 // Called to process an agent invite GET/POST request
2178 function handleInviteRequest(req, res) {
2179 const domain = getDomain(req);
2180 if (domain == null) { parent.debug('web', 'handleInviteRequest: failed checks.'); res.sendStatus(404); return; }
2181 if (domain.agentinvitecodes != true) { nice404(req, res); return; }
2182 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2183 if ((req.body == null) || (req.body.inviteCode == null) || (req.body.inviteCode == '')) { render(req, res, getRenderPage('invite', req, domain), getRenderArgs({ messageid: 0 }, req, domain)); return; } // No invitation code
2184
2185 // Each for a device group that has this invite code.
2186 for (var i in obj.meshes) {
2187 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)) {
2188 // Send invitation link, valid for 1 minute.
2189 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=' + encodeURIComponent(req.query.key)) : '') + (req.query.hide ? ('&hide=' + encodeURIComponent(req.query.hide)) : ''));
2190 return;
2191 }
2192 }
2193
2194 render(req, res, getRenderPage('invite', req, domain), getRenderArgs({ messageid: 100 }, req, domain)); // Bad invitation code
2195 }
2196
2197 // Called to render the MSTSC (RDP) or SSH web page
2198 function handleMSTSCRequest(req, res, page) {
2199 const domain = getDomain(req);
2200 if (domain == null) { parent.debug('web', 'handleMSTSCRequest: failed checks.'); res.sendStatus(404); return; }
2201 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2202
2203 // Check if we are in maintenance mode
2204 if ((parent.config.settings.maintenancemode != null) && (req.query.loginscreen !== '1')) {
2205 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));
2206 return;
2207 }
2208
2209 // Set features we want to send to this page
2210 var features = 0;
2211 if (domain.allowsavingdevicecredentials === false) { features |= 1; }
2212
2213 // Get the logged in user if present
2214 var user = null;
2215
2216 // If there is a login token, use that
2217 if (req.query.login != null) {
2218 var ucookie = parent.decodeCookie(req.query.login, parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
2219 if ((ucookie != null) && (ucookie.a === 3) && (typeof ucookie.u == 'string')) { user = obj.users[ucookie.u]; }
2220 }
2221
2222 // If no token, see if we have an active session
2223 if ((user == null) && (req.session.userid != null)) { user = obj.users[req.session.userid]; }
2224
2225 // If still no user, see if we have a default user
2226 if ((user == null) && (obj.args.user)) { user = obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]; }
2227
2228 // No user login, exit now
2229 if (user == null) { res.sendStatus(401); return; }
2230
2231 if (req.query.ws != null) {
2232 // This is a query with a websocket relay cookie, check that the cookie is valid and use it.
2233 var rcookie = parent.decodeCookie(req.query.ws, parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
2234 if ((rcookie != null) && (rcookie.domainid == domain.id) && (rcookie.nodeid != null) && (rcookie.tcpport != null)) {
2235
2236 // Fetch the node from the database
2237 obj.db.Get(rcookie.nodeid, function (err, nodes) {
2238 if ((err != null) || (nodes.length != 1)) { res.sendStatus(404); return; }
2239 const node = nodes[0];
2240
2241 // Check if we have SSH/RDP credentials for this device
2242 var serverCredentials = 0;
2243 if (domain.allowsavingdevicecredentials !== false) {
2244 if (page == 'ssh') {
2245 if ((typeof node.ssh == 'object') && (typeof node.ssh.u == 'string') && (typeof node.ssh.p == 'string')) { serverCredentials = 1; } // Username and password
2246 else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string') && (typeof node.ssh.kp == 'string')) { serverCredentials = 2; } // Username, key and password
2247 else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string')) { serverCredentials = 3; } // Username and key. No password.
2248 else if ((typeof node.ssh == 'object') && (typeof node.ssh[user._id] == 'object') && (typeof node.ssh[user._id].u == 'string') && (typeof node.ssh[user._id].p == 'string')) { serverCredentials = 1; } // Username and password in per user format
2249 else if ((typeof node.ssh == 'object') && (typeof node.ssh[user._id] == 'object') && (typeof node.ssh[user._id].k == 'string') && (typeof node.ssh[user._id].kp == 'string')) { serverCredentials = 2; } // Username, key and password in per user format
2250 else if ((typeof node.ssh == 'object') && (typeof node.ssh[user._id] == 'object') && (typeof node.ssh[user._id].k == 'string')) { serverCredentials = 3; } // Username and key. No password. in per user format
2251 } else {
2252 if ((typeof node.rdp == 'object') && (typeof node.rdp.d == 'string') && (typeof node.rdp.u == 'string') && (typeof node.rdp.p == 'string')) { serverCredentials = 1; } // Username and password in legacy format
2253 if ((typeof node.rdp == 'object') && (typeof node.rdp[user._id] == 'object') && (typeof node.rdp[user._id].d == 'string') && (typeof node.rdp[user._id].u == 'string') && (typeof node.rdp[user._id].p == 'string')) { serverCredentials = 1; } // Username and password in per user format
2254 }
2255 }
2256
2257 // Render the page
2258 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));
2259 });
2260 return;
2261 }
2262 }
2263
2264 // Check the nodeid
2265 if (req.query.node != null) {
2266 var nodeidsplit = req.query.node.split('/');
2267 if (nodeidsplit.length == 1) {
2268 req.query.node = 'node/' + domain.id + '/' + nodeidsplit[0]; // Format the nodeid correctly
2269 } else if (nodeidsplit.length == 3) {
2270 if ((nodeidsplit[0] != 'node') || (nodeidsplit[1] != domain.id)) { req.query.node = null; } // Check the nodeid format
2271 } else {
2272 req.query.node = null; // Bad nodeid
2273 }
2274 }
2275
2276 // If there is no nodeid, exit now
2277 if (req.query.node == null) { render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: '', name: '', features: features }, req, domain)); return; }
2278
2279 // Fetch the node from the database
2280 obj.db.Get(req.query.node, function (err, nodes) {
2281 if ((err != null) || (nodes.length != 1)) { res.sendStatus(404); return; }
2282 const node = nodes[0];
2283
2284 // Check access rights, must have remote control rights
2285 if ((obj.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0) { res.sendStatus(401); return; }
2286
2287 // Figure out the target port
2288 var port = 0, serverCredentials = false;
2289 if (page == 'ssh') {
2290 // SSH port
2291 port = 22;
2292 if (typeof node.sshport == 'number') { port = node.sshport; }
2293
2294 // Check if we have SSH credentials for this device
2295 if (domain.allowsavingdevicecredentials !== false) {
2296 if ((typeof node.ssh == 'object') && (typeof node.ssh.u == 'string') && (typeof node.ssh.p == 'string')) { serverCredentials = 1; } // Username and password
2297 else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string') && (typeof node.ssh.kp == 'string')) { serverCredentials = 2; } // Username, key and password
2298 else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string')) { serverCredentials = 3; } // Username and key. No password.
2299 else if ((typeof node.ssh == 'object') && (typeof node.ssh[user._id] == 'object') && (typeof node.ssh[user._id].u == 'string') && (typeof node.ssh[user._id].p == 'string')) { serverCredentials = 1; } // Username and password in per user format
2300 else if ((typeof node.ssh == 'object') && (typeof node.ssh[user._id] == 'object') && (typeof node.ssh[user._id].k == 'string') && (typeof node.ssh[user._id].kp == 'string')) { serverCredentials = 2; } // Username, key and password in per user format
2301 else if ((typeof node.ssh == 'object') && (typeof node.ssh[user._id] == 'object') && (typeof node.ssh[user._id].k == 'string')) { serverCredentials = 3; } // Username and key. No password. in per user format
2302 }
2303 } else {
2304 // RDP port
2305 port = 3389;
2306 if (typeof node.rdpport == 'number') { port = node.rdpport; }
2307
2308 // Check if we have RDP credentials for this device
2309 if (domain.allowsavingdevicecredentials !== false) {
2310 if ((typeof node.rdp == 'object') && (typeof node.rdp.d == 'string') && (typeof node.rdp.u == 'string') && (typeof node.rdp.p == 'string')) { serverCredentials = 1; } // Username and password
2311 if ((typeof node.rdp == 'object') && (typeof node.rdp[user._id] == 'object') && (typeof node.rdp[user._id].d == 'string') && (typeof node.rdp[user._id].u == 'string') && (typeof node.rdp[user._id].p == 'string')) { serverCredentials = 1; } // Username and password in per user format
2312 }
2313 }
2314 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; } }
2315
2316 // Generate a cookie and respond
2317 var cookie = parent.encodeCookie({ userid: user._id, domainid: user.domain, nodeid: node._id, tcpport: port }, parent.loginCookieEncryptionKey);
2318 render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: cookie, name: encodeURIComponent(node.name).replace(/'/g, '%27'), serverCredentials: serverCredentials, features: features }, req, domain));
2319 });
2320 }
2321
2322 // Called to handle push-only requests
2323 function handleFirebasePushOnlyRelayRequest(req, res) {
2324 parent.debug('email', 'handleFirebasePushOnlyRelayRequest');
2325 if ((req.body == null) || (req.body.msg == null) || (obj.parent.firebase == null)) { res.sendStatus(404); return; }
2326 if (obj.parent.config.firebase.pushrelayserver == null) { res.sendStatus(404); return; }
2327 if ((typeof obj.parent.config.firebase.pushrelayserver == 'string') && (req.query.key != obj.parent.config.firebase.pushrelayserver)) { res.sendStatus(404); return; }
2328 var data = null;
2329 try { data = JSON.parse(req.body.msg) } catch (ex) { res.sendStatus(404); return; }
2330 if (typeof data != 'object') { res.sendStatus(404); return; }
2331 if (typeof data.pmt != 'string') { res.sendStatus(404); return; }
2332 if (typeof data.payload != 'object') { res.sendStatus(404); return; }
2333 if (typeof data.payload.notification != 'object') { res.sendStatus(404); return; }
2334 if (typeof data.payload.notification.title != 'string') { res.sendStatus(404); return; }
2335 if (typeof data.payload.notification.body != 'string') { res.sendStatus(404); return; }
2336 if (typeof data.options != 'object') { res.sendStatus(404); return; }
2337 if ((data.options.priority != 'Normal') && (data.options.priority != 'High')) { res.sendStatus(404); return; }
2338 if ((typeof data.options.timeToLive != 'number') || (data.options.timeToLive < 1)) { res.sendStatus(404); return; }
2339 parent.debug('email', 'handleFirebasePushOnlyRelayRequest - ok');
2340 obj.parent.firebase.sendToDevice({ pmt: data.pmt }, data.payload, data.options, function (id, err, errdesc) {
2341 if (err == null) { res.sendStatus(200); } else { res.sendStatus(500); }
2342 });
2343 }
2344
2345 // Called to handle two-way push notification relay request
2346 function handleFirebaseRelayRequest(ws, req) {
2347 parent.debug('email', 'handleFirebaseRelayRequest');
2348 if (obj.parent.firebase == null) { try { ws.close(); } catch (e) { } return; }
2349 if (obj.parent.firebase.setupRelay == null) { try { ws.close(); } catch (e) { } return; }
2350 if (obj.parent.config.firebase.relayserver == null) { try { ws.close(); } catch (e) { } return; }
2351 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; }
2352 obj.parent.firebase.setupRelay(ws);
2353 }
2354
2355 // Called to process an agent invite request
2356 function handleAgentInviteRequest(req, res) {
2357 const domain = getDomain(req);
2358 if ((domain == null) || ((req.query.m == null) && (req.query.c == null))) { parent.debug('web', 'handleAgentInviteRequest: failed checks.'); res.sendStatus(404); return; }
2359 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2360
2361 if (req.query.c != null) {
2362 // A cookie is specified in the query string, use that
2363 var cookie = obj.parent.decodeCookie(req.query.c, obj.parent.invitationLinkEncryptionKey);
2364 if (cookie == null) { res.sendStatus(404); return; }
2365 var mesh = obj.meshes[cookie.mid];
2366 if (mesh == null) { res.sendStatus(404); return; }
2367 var installflags = cookie.f;
2368 if (typeof installflags != 'number') { installflags = 0; }
2369 var showagents = cookie.ag;
2370 if (typeof showagents != 'number') { showagents = 0; }
2371 parent.debug('web', 'handleAgentInviteRequest using cookie.');
2372
2373 // Build the mobile agent URL, this is used to connect mobile devices
2374 var agentServerName = obj.getWebServerName(domain, req);
2375 if (typeof obj.args.agentaliasdns == 'string') { agentServerName = obj.args.agentaliasdns; }
2376 var xdomain = (domain.dns == null) ? domain.id : '';
2377 var agentHttpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
2378 if (obj.args.agentport != null) { agentHttpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
2379 if (obj.args.agentaliasport != null) { agentHttpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
2380 var magenturl = 'mc://' + agentServerName + ((agentHttpsPort != 443) ? (':' + agentHttpsPort) : '') + ((xdomain != '') ? ('/' + xdomain) : '') + ',' + obj.agentCertificateHashBase64 + ',' + mesh._id.split('/')[2];
2381
2382 var meshcookie = parent.encodeCookie({ m: mesh._id.split('/')[2] }, parent.invitationLinkEncryptionKey);
2383 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, assistanttype: (domain.assistanttypeagentinvite ? domain.assistanttypeagentinvite : 0) }, req, domain));
2384 } else if (req.query.m != null) {
2385 // The MeshId is specified in the query string, use that
2386 var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.m.toLowerCase()];
2387 if (mesh == null) { res.sendStatus(404); return; }
2388 var installflags = 0;
2389 if (req.query.f) { installflags = parseInt(req.query.f); }
2390 if (typeof installflags != 'number') { installflags = 0; }
2391 var showagents = 0;
2392 if (req.query.f) { showagents = parseInt(req.query.ag); }
2393 if (typeof showagents != 'number') { showagents = 0; }
2394 parent.debug('web', 'handleAgentInviteRequest using meshid.');
2395
2396 // Build the mobile agent URL, this is used to connect mobile devices
2397 var agentServerName = obj.getWebServerName(domain, req);
2398 if (typeof obj.args.agentaliasdns == 'string') { agentServerName = obj.args.agentaliasdns; }
2399 var xdomain = (domain.dns == null) ? domain.id : '';
2400 var agentHttpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
2401 if (obj.args.agentport != null) { agentHttpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
2402 if (obj.args.agentaliasport != null) { agentHttpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
2403 var magenturl = 'mc://' + agentServerName + ((agentHttpsPort != 443) ? (':' + agentHttpsPort) : '') + ((xdomain != '') ? ('/' + xdomain) : '') + ',' + obj.agentCertificateHashBase64 + ',' + mesh._id.split('/')[2];
2404
2405 var meshcookie = parent.encodeCookie({ m: mesh._id.split('/')[2] }, parent.invitationLinkEncryptionKey);
2406 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, assistanttype: (domain.assistanttypeagentinvite ? domain.assistanttypeagentinvite : 0) }, req, domain));
2407 }
2408 }
2409
2410 // Called to process an agent invite request
2411 function handleUserImageRequest(req, res) {
2412 const domain = getDomain(req);
2413 if (domain == null) { parent.debug('web', 'handleUserImageRequest: failed checks.'); res.sendStatus(404); return; }
2414 if ((req.session == null) || (req.session.userid == null)) { parent.debug('web', 'handleUserImageRequest: failed checks 2.'); res.sendStatus(404); return; }
2415 var imageUserId = req.session.userid;
2416 if ((req.query.id != null)) {
2417 var user = obj.users[req.session.userid];
2418 if ((user == null) || (user.siteadmin == null) && ((user.siteadmin & 2) == 0)) { res.sendStatus(404); return; }
2419 imageUserId = 'user/' + domain.id + '/' + req.query.id;
2420 }
2421 obj.db.Get('im' + imageUserId, function (err, docs) {
2422 if ((err != null) || (docs == null) || (docs.length != 1) || (typeof docs[0].image != 'string')) { res.sendStatus(404); return; }
2423 var imagebase64 = docs[0].image;
2424 if (imagebase64.startsWith('data:image/png;base64,')) {
2425 res.set('Content-Type', 'image/png');
2426 res.set({ 'Cache-Control': 'no-store' });
2427 res.send(Buffer.from(imagebase64.substring(22), 'base64'));
2428 } else if (imagebase64.startsWith('data:image/jpeg;base64,')) {
2429 res.set('Content-Type', 'image/jpeg');
2430 res.set({ 'Cache-Control': 'no-store' });
2431 res.send(Buffer.from(imagebase64.substring(23), 'base64'));
2432 } else {
2433 res.sendStatus(404);
2434 }
2435 });
2436 }
2437
2438 function handleDeleteAccountRequest(req, res, direct) {
2439 parent.debug('web', 'handleDeleteAccountRequest()');
2440 const domain = checkUserIpAddress(req, res);
2441 if (domain == null) { return; }
2442 if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handleDeleteAccountRequest: failed checks.'); res.sendStatus(404); return; }
2443 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2444 if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
2445 if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
2446
2447 var user = null;
2448 if (req.body.authcookie) {
2449 // If a authentication cookie is provided, decode it here
2450 var loginCookie = obj.parent.decodeCookie(req.body.authcookie, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
2451 if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { user = obj.users[loginCookie.userid]; }
2452 } else {
2453 // Check if the user is logged and we have all required parameters
2454 if (!req.session || !req.session.userid || !req.body.apassword1 || (req.body.apassword1 != req.body.apassword2) || (req.session.userid.split('/')[1] != domain.id)) {
2455 parent.debug('web', 'handleDeleteAccountRequest: required parameters not present.');
2456 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2457 return;
2458 } else {
2459 user = obj.users[req.session.userid];
2460 }
2461 }
2462 if (!user) { parent.debug('web', 'handleDeleteAccountRequest: user not found.'); res.sendStatus(404); return; }
2463 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) { parent.debug('web', 'handleDeleteAccountRequest: account settings locked.'); res.sendStatus(404); return; }
2464
2465 // Check if the password is correct
2466 obj.authenticate(user._id.split('/')[2], req.body.apassword1, domain, function (err, userid, passhint, loginOptions) {
2467 var deluser = obj.users[userid];
2468 if ((userid != null) && (deluser != null)) {
2469 // Remove all links to this user
2470 if (deluser.links != null) {
2471 for (var i in deluser.links) {
2472 if (i.startsWith('mesh/')) {
2473 // Get the device group
2474 var mesh = obj.meshes[i];
2475 if (mesh) {
2476 // Remove user from the mesh
2477 if (mesh.links[deluser._id] != null) { delete mesh.links[deluser._id]; parent.db.Set(mesh); }
2478
2479 // Notify mesh change
2480 var change = 'Removed user ' + deluser.name + ' from group ' + mesh.name;
2481 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 };
2482 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.
2483 parent.DispatchEvent(['*', mesh._id, deluser._id, user._id], obj, event);
2484 }
2485 } else if (i.startsWith('node/')) {
2486 // Get the node and the rights for this node
2487 obj.GetNodeWithRights(domain, deluser, i, function (node, rights, visible) {
2488 if ((node == null) || (node.links == null) || (node.links[deluser._id] == null)) return;
2489
2490 // Remove the link and save the node to the database
2491 delete node.links[deluser._id];
2492 if (Object.keys(node.links).length == 0) { delete node.links; }
2493 db.Set(obj.cleanDevice(node));
2494
2495 // Event the node change
2496 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) }
2497 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.
2498 parent.DispatchEvent(['*', node.meshid, node._id], obj, event);
2499 });
2500 } else if (i.startsWith('ugrp/')) {
2501 // Get the device group
2502 var ugroup = obj.userGroups[i];
2503 if (ugroup) {
2504 // Remove user from the user group
2505 if (ugroup.links[deluser._id] != null) { delete ugroup.links[deluser._id]; parent.db.Set(ugroup); }
2506
2507 // Notify user group change
2508 var change = 'Removed user ' + deluser.name + ' from user group ' + ugroup.name;
2509 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 };
2510 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.
2511 parent.DispatchEvent(['*', ugroup._id, user._id, deluser._id], obj, event);
2512 }
2513 }
2514 }
2515 }
2516
2517 obj.db.Remove('ws' + deluser._id); // Remove user web state
2518 obj.db.Remove('nt' + deluser._id); // Remove notes for this user
2519 obj.db.Remove('ntp' + deluser._id); // Remove personal notes for this user
2520 obj.db.Remove('im' + deluser._id); // Remove image for this user
2521
2522 // Delete any login tokens
2523 parent.db.GetAllTypeNodeFiltered(['logintoken-' + deluser._id], domain.id, 'logintoken', null, function (err, docs) {
2524 if ((err == null) && (docs != null)) { for (var i = 0; i < docs.length; i++) { parent.db.Remove(docs[i]._id, function () { }); } }
2525 });
2526
2527 // Delete all files on the server for this account
2528 try {
2529 var deluserpath = obj.getServerRootFilePath(deluser);
2530 if (deluserpath != null) { obj.deleteFolderRec(deluserpath); }
2531 } catch (e) { }
2532
2533 // Remove the user
2534 obj.db.Remove(deluser._id);
2535 delete obj.users[deluser._id];
2536 req.session = null;
2537 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2538 obj.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', userid: deluser._id, username: deluser.name, action: 'accountremove', msg: 'Account removed', domain: domain.id });
2539 parent.debug('web', 'handleDeleteAccountRequest: removed user.');
2540 } else {
2541 parent.debug('web', 'handleDeleteAccountRequest: auth failed.');
2542 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2543 }
2544 });
2545 }
2546
2547 // Check a user's password
2548 obj.checkUserPassword = function (domain, user, password, func) {
2549 // Check the old password
2550 if (user.passtype != null) {
2551 // IIS default clear or weak password hashing (SHA-1)
2552 require('./pass').iishash(user.passtype, password, user.salt, function (err, hash) {
2553 if (err) { parent.debug('web', 'checkUserPassword: SHA-1 fail.'); return func(false); }
2554 if (hash == user.hash) {
2555 if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { parent.debug('web', 'checkUserPassword: SHA-1 locked.'); return func(false); } // Account is locked
2556 parent.debug('web', 'checkUserPassword: SHA-1 ok.');
2557 return func(true); // Allow password change
2558 }
2559 func(false);
2560 });
2561 } else {
2562 // Default strong password hashing (pbkdf2 SHA384)
2563 require('./pass').hash(password, user.salt, function (err, hash, tag) {
2564 if (err) { parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 fail.'); return func(false); }
2565 if (hash == user.hash) {
2566 if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 locked.'); return func(false); } // Account is locked
2567 parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 ok.');
2568 return func(true); // Allow password change
2569 }
2570 func(false);
2571 }, 0);
2572 }
2573 }
2574
2575 // Check a user's old passwords
2576 // Callback: 0=OK, 1=OldPass, 2=CommonPass
2577 obj.checkOldUserPasswords = function (domain, user, password, func) {
2578 // Check how many old passwords we need to check
2579 if ((domain.passwordrequirements != null) && (typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
2580 if (user.oldpasswords != null) {
2581 const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
2582 if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
2583 }
2584 } else {
2585 delete user.oldpasswords;
2586 }
2587
2588 // If there is no old passwords, exit now.
2589 var oldPassCount = 1;
2590 if (user.oldpasswords != null) { oldPassCount += user.oldpasswords.length; }
2591 var oldPassCheckState = { response: 0, count: oldPassCount, user: user, func: func };
2592
2593 // Test against common passwords if this feature is enabled
2594 // Example of common passwords: 123456789, password123
2595 if ((domain.passwordrequirements != null) && (domain.passwordrequirements.bancommonpasswords == true)) {
2596 oldPassCheckState.count++;
2597 require('wildleek')(password).then(function (wild) {
2598 if (wild == true) { oldPassCheckState.response = 2; }
2599 if (--oldPassCheckState.count == 0) { oldPassCheckState.func(oldPassCheckState.response); }
2600 });
2601 }
2602
2603 // Try current password
2604 require('./pass').hash(password, user.salt, function oldPassCheck(err, hash, tag) {
2605 if ((err == null) && (hash == tag.user.hash)) { tag.response = 1; }
2606 if (--tag.count == 0) { tag.func(tag.response); }
2607 }, oldPassCheckState);
2608
2609 // Try each old password
2610 if (user.oldpasswords != null) {
2611 for (var i in user.oldpasswords) {
2612 const oldpassword = user.oldpasswords[i];
2613 // Default strong password hashing (pbkdf2 SHA384)
2614 require('./pass').hash(password, oldpassword.salt, function oldPassCheck(err, hash, tag) {
2615 if ((err == null) && (hash == tag.oldPassword.hash)) { tag.state.response = 1; }
2616 if (--tag.state.count == 0) { tag.state.func(tag.state.response); }
2617 }, { oldPassword: oldpassword, state: oldPassCheckState });
2618 }
2619 }
2620 }
2621
2622 // Handle password changes
2623 function handlePasswordChangeRequest(req, res, direct) {
2624 const domain = checkUserIpAddress(req, res);
2625 if (domain == null) { return; }
2626 if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handlePasswordChangeRequest: failed checks (1).'); res.sendStatus(404); return; }
2627 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2628 if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
2629 if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
2630
2631 // Check if the user is logged and we have all required parameters
2632 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)) {
2633 parent.debug('web', 'handlePasswordChangeRequest: failed checks (2).');
2634 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2635 return;
2636 }
2637
2638 // Get the current user
2639 var user = obj.users[req.session.userid];
2640 if (!user) {
2641 parent.debug('web', 'handlePasswordChangeRequest: user not found.');
2642 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2643 return;
2644 }
2645
2646 // Check account settings locked
2647 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) {
2648 parent.debug('web', 'handlePasswordChangeRequest: account settings locked.');
2649 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2650 return;
2651 }
2652
2653 // Check old password
2654 obj.checkUserPassword(domain, user, req.body.apassword1, function (result) {
2655 if (result == true) {
2656 // Check if the new password is allowed, only do this if this feature is enabled.
2657 parent.checkOldUserPasswords(domain, user, command.newpass, function (result) {
2658 if (result == 1) {
2659 parent.debug('web', 'handlePasswordChangeRequest: old password reuse attempt.');
2660 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2661 } else if (result == 2) {
2662 parent.debug('web', 'handlePasswordChangeRequest: commonly used password use attempt.');
2663 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2664 } else {
2665 // Update the password
2666 require('./pass').hash(req.body.apassword1, function (err, salt, hash, tag) {
2667 const nowSeconds = Math.floor(Date.now() / 1000);
2668 if (err) { parent.debug('web', 'handlePasswordChangeRequest: hash error.'); throw err; }
2669 if (domain.passwordrequirements != null) {
2670 // Save password hint if this feature is enabled
2671 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; }
2672
2673 // Save previous password if this feature is enabled
2674 if ((typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
2675 if (user.oldpasswords == null) { user.oldpasswords = []; }
2676 user.oldpasswords.push({ salt: user.salt, hash: user.hash, start: user.passchange, end: nowSeconds });
2677 const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
2678 if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
2679 }
2680 }
2681 user.salt = salt;
2682 user.hash = hash;
2683 user.passchange = user.access = nowSeconds;
2684 delete user.passtype;
2685
2686 obj.db.SetUser(user);
2687 req.session.viewmode = 2;
2688 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2689 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 });
2690 }, 0);
2691 }
2692 });
2693 }
2694 });
2695 }
2696
2697 // Called when a strategy login occurred
2698 // This is called after a successful Oauth to Twitter, Google, GitHub...
2699 function handleStrategyLogin(req, res) {
2700 const domain = checkUserIpAddress(req, res);
2701 if (domain == null) { return; }
2702 if ((req.user != null) && (req.user.sid != null) && (req.user.strategy != null)) {
2703 const strategy = domain.authstrategies[req.user.strategy];
2704 const groups = { 'enabled': typeof strategy.groups == 'object' }
2705 parent.authLog(req.user.strategy.toUpperCase(), `User Authorized: ${JSON.stringify(req.user)}`);
2706 if (groups.enabled) { // Groups only available for OIDC strategy currently
2707 groups.userMemberships = obj.common.convertStrArray(req.user.groups);
2708 groups.syncEnabled = (strategy.groups.sync === true || strategy.groups.sync?.filter) ? true : false;
2709 groups.syncMemberships = [];
2710 groups.siteAdminEnabled = strategy.groups.siteadmin ? true : false;
2711 groups.grantAdmin = false;
2712 groups.revokeAdmin = strategy.groups.revokeAdmin ? strategy.groups.revokeAdmin : true;
2713 groups.requiredGroups = obj.common.convertStrArray(strategy.groups.required);
2714 groups.siteAdmin = obj.common.convertStrArray(strategy.groups.siteadmin);
2715 groups.syncFilter = obj.common.convertStrArray(strategy.groups.sync?.filter);
2716
2717 // Fancy Logs
2718 let groupMessage = '';
2719 if (groups.userMemberships.length == 1) { groupMessage = ` Found membership: "${groups.userMemberships[0]}"` }
2720 else { groupMessage = ` Found ${groups.userMemberships.length} memberships: ["${groups.userMemberships.join('", "')}"]` }
2721 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}"` + groupMessage);
2722
2723 // Check user membership in required groups
2724 if (groups.requiredGroups.length > 0) {
2725 let match = false
2726 for (var i in groups.requiredGroups) {
2727 if (groups.userMemberships.indexOf(groups.requiredGroups[i]) != -1) {
2728 match = true;
2729 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Membership to required group found: "${groups.requiredGroups[i]}"`);
2730 }
2731 }
2732 if (match === false) {
2733 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Login denied. No membership to required group.`);
2734 req.session.loginmode = 1;
2735 req.session.messageid = 111; // Access Denied.
2736 res.redirect(domain.url + getQueryPortion(req));
2737 return;
2738 }
2739 }
2740
2741 // Check user membership in admin groups
2742 if (groups.siteAdminEnabled === true) {
2743 groups.grantAdmin = false;
2744 for (var i in strategy.groups.siteadmin) {
2745 if (groups.userMemberships.indexOf(strategy.groups.siteadmin[i]) >= 0) {
2746 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" User membership found in site admin group: "${strategy.groups.siteadmin[i]}"`);
2747 groups.siteAdmin = strategy.groups.siteadmin[i];
2748 groups.grantAdmin = true;
2749 break;
2750 }
2751 }
2752 }
2753
2754 // Check if we need to sync user-memberships (IdP) with user-groups (meshcentral)
2755 if (groups.syncEnabled === true) {
2756 if (groups.syncFilter.length > 0){ // config.json has specified sync.filter so loop and use it
2757 for (var i in groups.syncFilter) {
2758 if (groups.userMemberships.indexOf(groups.syncFilter[i]) >= 0) { groups.syncMemberships.push(groups.syncFilter[i]); }
2759 }
2760 } else { // config.json doesnt have sync.filter specified so we are going to sync all the users groups from oidc instead
2761 for (var i in groups.userMemberships) {
2762 groups.syncMemberships.push(groups.userMemberships[i]);
2763 }
2764 }
2765 if (groups.syncMemberships.length > 0) {
2766 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" User memberships to sync: ${groups.syncMemberships.join(', ')}`);
2767 } else {
2768 groups.syncMemberships = null;
2769 groups.syncEnabled = false;
2770 if (groups.syncFilter.length > 0){
2771 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" No sync memberships found using filters: ${groups.syncFilter.join(', ')}`);
2772 } else {
2773 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" No sync memberships found`);
2774 }
2775 }
2776 }
2777 }
2778
2779 // Check if the user already exists
2780 const userid = 'user/' + domain.id + '/' + req.user.sid;
2781 var user = obj.users[userid];
2782 if (user == null) {
2783 var newAccountAllowed = false;
2784 var newAccountRealms = null;
2785
2786 if (domain.newaccounts === true) { newAccountAllowed = true; }
2787 if (obj.common.validateStrArray(domain.newaccountrealms)) { newAccountRealms = domain.newaccountrealms; }
2788
2789 if (domain.authstrategies[req.user.strategy]) {
2790 if (domain.authstrategies[req.user.strategy].newaccounts === true) { newAccountAllowed = true; }
2791 if (obj.common.validateStrArray(domain.authstrategies[req.user.strategy].newaccountrealms)) { newAccountRealms = domain.authstrategies[req.user.strategy].newaccountrealms; }
2792 }
2793
2794 if (newAccountAllowed === true) {
2795 // Create the user
2796 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: USER: "${req.user.sid}" Creating new login user: "${userid}"`);
2797 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 };
2798 if (req.user.email != null) { user.email = req.user.email; user.emailVerified = req.user.email_verified ? req.user.email_verified : true; }
2799 if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; } // New accounts automatically assigned server rights.
2800 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.
2801 if (newAccountRealms) { user.groups = newAccountRealms; } // New accounts automatically part of some groups (Realms).
2802 obj.users[userid] = user;
2803
2804 // Auto-join any user groups
2805 var newaccountsusergroups = null;
2806 if (typeof domain.newaccountsusergroups == 'object') { newaccountsusergroups = domain.newaccountsusergroups; }
2807 if (typeof domain.authstrategies[req.user.strategy].newaccountsusergroups == 'object') { newaccountsusergroups = domain.authstrategies[req.user.strategy].newaccountsusergroups; }
2808 if (newaccountsusergroups) {
2809 for (var i in newaccountsusergroups) {
2810 var ugrpid = newaccountsusergroups[i];
2811 if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
2812 var ugroup = obj.userGroups[ugrpid];
2813 if (ugroup != null) {
2814 // Add group to the user
2815 if (user.links == null) { user.links = {}; }
2816 user.links[ugroup._id] = { rights: 1 };
2817
2818 // Add user to the group
2819 ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
2820 db.Set(ugroup);
2821
2822 // Notify user group change
2823 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 };
2824 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.
2825 parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
2826 }
2827 }
2828 }
2829
2830 if (groups.enabled === true) {
2831 // Sync the user groups if enabled
2832 if (groups.syncEnabled === true) {
2833 // Set groupType to the preset name if it exists, otherwise use the strategy name
2834 const groupType = domain.authstrategies[req.user.strategy].custom?.preset ? domain.authstrategies[req.user.strategy].custom.preset : req.user.strategy;
2835 syncExternalUserGroups(domain, user, groups.syncMemberships, groupType);
2836 }
2837 // See if the user is a member of the site admin group.
2838 if (groups.grantAdmin === true) {
2839 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Granting site admin privilages`);
2840 user.siteadmin = 0xFFFFFFFF;
2841 }
2842 }
2843
2844 // Save the user
2845 obj.db.SetUser(user);
2846
2847 // Event user creation
2848 var targets = ['*', 'server-users'];
2849 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 };
2850 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.
2851 parent.DispatchEvent(targets, obj, event);
2852
2853 req.session.userid = userid;
2854 setSessionRandom(req);
2855
2856 // Notify account login using SSO
2857 var targets = ['*', 'server-users', user._id];
2858 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
2859 const ua = obj.getUserAgentInfo(req);
2860 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' };
2861 obj.parent.DispatchEvent(targets, obj, loginEvent);
2862 } else {
2863 // New users not allowed
2864 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: LOGIN FAILED: USER: "${req.user.sid}" New accounts are not allowed`);
2865 req.session.loginmode = 1;
2866 req.session.messageid = 100; // Unable to create account.
2867 res.redirect(domain.url + getQueryPortion(req));
2868 return;
2869 }
2870 } else { // Login success
2871 // Check for basic changes
2872 var userChanged = false;
2873 if ((req.user.name != null) && (req.user.name != user.name)) { user.name = req.user.name; userChanged = true; }
2874 if ((req.user.email != null) && (req.user.email != user.email)) { user.email = req.user.email; user.emailVerified = true; userChanged = true; }
2875
2876 if (groups.enabled === true) {
2877 // Sync the user groups if enabled
2878 if (groups.syncEnabled === true) {
2879 syncExternalUserGroups(domain, user, groups.syncMemberships, req.user.strategy)
2880 }
2881 // See if the user is a member of the site admin group.
2882 if (groups.siteAdminEnabled === true) {
2883 if (groups.grantAdmin === true) {
2884 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Granting site admin privilages`);
2885 if (user.siteadmin !== 0xFFFFFFFF) { user.siteadmin = 0xFFFFFFFF; userChanged = true; }
2886 } else if ((groups.revokeAdmin === true) && (user.siteadmin === 0xFFFFFFFF)) {
2887 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Revoking site admin privilages.`);
2888 delete user.siteadmin;
2889 userChanged = true;
2890 }
2891 }
2892 }
2893
2894 // Update db record for user if there are changes detected
2895 if (userChanged) {
2896 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: CHANGED: USER: "${req.user.sid}" Updating user database entry`);
2897 obj.db.SetUser(user);
2898
2899 // Event user change
2900 var targets = ['*', 'server-users'];
2901 var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Account changed', domain: domain.id };
2902 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.
2903 parent.DispatchEvent(targets, obj, event);
2904 }
2905 req.session.userid = userid;
2906 setSessionRandom(req);
2907
2908 // Notify account login using SSO
2909 var targets = ['*', 'server-users', user._id];
2910 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
2911 const ua = obj.getUserAgentInfo(req);
2912 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' };
2913 obj.parent.DispatchEvent(targets, obj, loginEvent);
2914 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: LOGIN SUCCESS: USER: "${req.user.sid}"`);
2915 }
2916 } else if (req.session && req.session.userid && obj.users[req.session.userid]) {
2917 parent.authLog('handleStrategyLogin', `User Already Authorised "${(req.session.passport && req.session.passport.user) ? req.session.passport.user : req.session.userid }"`);
2918 } else {
2919 parent.authLog('handleStrategyLogin', `LOGIN FAILED: REQUEST CONTAINS NO USER OR SID`);
2920 }
2921 //res.redirect(domain.url); // This does not handle cookie correctly.
2922 res.set('Content-Type', 'text/html');
2923 let url = domain.url;
2924 if (Object.keys(req.query).length > 0) { url += "?" + Object.keys(req.query).map(function(key) { return encodeURIComponent(key) + "=" + encodeURIComponent(req.query[key]); }).join("&"); }
2925
2926 // check for relaystate is set, test against configured server name and accepted query params
2927 if(req.body && req.body.RelayState !== undefined){
2928 var relayState = decodeURIComponent(req.body.RelayState);
2929 var serverName = (obj.getWebServerName(domain, req)).replaceAll('.','\\.');
2930
2931 var regexstr = `(?<=https:\\/\\/(?:.+?\\.)?${serverName}\\/?)` +
2932 `.*((?<=([\\?&])gotodevicename=(.{64})|` +
2933 `gotonode=(.{64})|` +
2934 `gotodeviceip=(((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4})|` +
2935 `gotodeviceip=(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::([0-9a-fA-F]{1,4}:){1,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:)` +
2936 `lang=(.{5})|` +
2937 `sitestyle=(\\d+)|` +
2938 `user=(.{64})|` +
2939 `pass=(.{256})|` +
2940 `key=|` +
2941 `locale=|` +
2942 `gotomesh=(.{64})|` +
2943 `gotouser=(.{0,64})|` +
2944 `gotougrp=(.{64})|` +
2945 `debug=|` +
2946 `filter=|` +
2947 `webrtc=|` +
2948 `hide=|` +
2949 `viewmode=(\\d+)(?=[\\&]|\\b)))`;
2950
2951 var regex = new RegExp(regexstr);
2952 if(regex.test(relayState)){
2953 url = relayState;
2954 }
2955 }
2956
2957 res.end('<html><head><meta http-equiv="refresh" content=0;url="' + url + '"></head><body></body></html>');
2958 }
2959
2960 // Indicates that any request to "/" should render "default" or "login" depending on login state
2961 function handleRootRequest(req, res, direct) {
2962 const domain = checkUserIpAddress(req, res);
2963 if (domain == null) { return; }
2964 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2965 if (!obj.args) { parent.debug('web', 'handleRootRequest: no obj.args.'); res.sendStatus(500); return; }
2966
2967 // If a HTTP header is required, check new UserRequiredHttpHeader
2968 if (domain.userrequiredhttpheader && (typeof domain.userrequiredhttpheader == 'object')) { var ok = false; for (var i in req.headers) { if (domain.userrequiredhttpheader[i.toLowerCase()] == req.headers[i]) { ok = true; } } if (ok == false) { res.sendStatus(404); return; } }
2969
2970 // If the session is expired, clear it.
2971 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]; } }
2972
2973 // Check if we are in maintenance mode
2974 if ((parent.config.settings.maintenancemode != null) && (req.query.loginscreen !== '1')) {
2975 parent.debug('web', 'handleLoginRequest: Server under maintenance.');
2976 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));
2977 return;
2978 }
2979
2980 // If set and there is no user logged in, redirect the root page. Make sure not to redirect if /login is used
2981 if ((typeof domain.unknownuserrootredirect == 'string') && ((req.session == null) || (req.session.userid == null))) {
2982 var q = new URL(req.url, 'http://localhost');
2983 if (!q.pathname.endsWith('/login')) { res.redirect(domain.unknownuserrootredirect + getQueryPortion(req)); return; }
2984 }
2985
2986 if ((domain.sspi != null) && ((req.query.login == null) || (obj.parent.loginCookieEncryptionKey == null))) {
2987 // Login using SSPI
2988 domain.sspi.authenticate(req, res, function (err) {
2989 if ((err != null) || (req.connection.user == null)) {
2990 obj.parent.authLog('https', 'Failed SSPI-auth for ' + req.connection.user + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'] });
2991 parent.debug('web', 'handleRootRequest: SSPI auth required.');
2992 try { res.sendStatus(401); } catch (ex) { } // sspi.authenticate() should already have responded to this request.
2993 } else {
2994 parent.debug('web', 'handleRootRequest: SSPI auth ok.');
2995 handleRootRequestEx(req, res, domain, direct);
2996 }
2997 });
2998 } else if (req.query.user && req.query.pass) {
2999 // User credentials are being passed in the URL. WARNING: Putting credentials in a URL is bad security... but people are requesting this option.
3000 obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid, passhint, loginOptions) {
3001 // 2FA is not supported in URL authentication method. If user has 2FA enabled, this login method fails.
3002 var user = obj.users[userid];
3003 if ((err == null) && checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true) {
3004 handleRootRequestEx(req, res, domain, direct);
3005 } else if ((userid != null) && (err == null)) {
3006 // Login success
3007 parent.debug('web', 'handleRootRequest: user/pass in URL auth ok.');
3008 req.session.userid = userid;
3009 delete req.session.currentNode;
3010 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
3011 setSessionRandom(req);
3012 obj.parent.authLog('https', 'Accepted password for ' + userid + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'], sessionid: req.session.x });
3013 handleRootRequestEx(req, res, domain, direct);
3014 } else {
3015 // Login failed
3016 handleRootRequestEx(req, res, domain, direct);
3017 }
3018 });
3019 } else if ((req.session != null) && (typeof req.session.loginToken == 'string')) {
3020 // Check if the loginToken is still valid
3021 obj.db.Get('logintoken-' + req.session.loginToken, function (err, docs) {
3022 if ((err != null) || (docs == null) || (docs.length != 1) || (docs[0].tokenUser != req.session.loginToken)) { for (var i in req.session) { delete req.session[i]; } }
3023 handleRootRequestEx(req, res, domain, direct); // Login using a different system
3024 });
3025 } else {
3026 // Login using a different system
3027 handleRootRequestEx(req, res, domain, direct);
3028 }
3029 }
3030
3031 function handleRootRequestEx(req, res, domain, direct) {
3032 var nologout = false, user = null;
3033 res.set({ 'Cache-Control': 'no-store' });
3034
3035 // Check if we have an incomplete domain name in the path
3036 if ((domain.id != '') && (domain.dns == null) && (req.url.split('/').length == 2)) {
3037 parent.debug('web', 'handleRootRequestEx: incomplete domain name in the path.');
3038 res.redirect(domain.url + getQueryPortion(req)); // BAD***
3039 return;
3040 }
3041
3042 if (obj.args.nousers == true) {
3043 // If in single user mode, setup things here.
3044 delete req.session.loginmode;
3045 req.session.userid = 'user/' + domain.id + '/~';
3046 delete req.session.currentNode;
3047 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
3048 setSessionRandom(req);
3049 if (obj.users[req.session.userid] == null) {
3050 // Create the dummy user ~ with impossible password
3051 parent.debug('web', 'handleRootRequestEx: created dummy user in nouser mode.');
3052 obj.users[req.session.userid] = { type: 'user', _id: req.session.userid, name: '~', email: '~', domain: domain.id, siteadmin: 4294967295 };
3053 obj.db.SetUser(obj.users[req.session.userid]);
3054 }
3055 } else if (obj.args.user && obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]) {
3056 // If a default user is active, setup the session here.
3057 parent.debug('web', 'handleRootRequestEx: auth using default user.');
3058 delete req.session.loginmode;
3059 req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase();
3060 delete req.session.currentNode;
3061 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
3062 setSessionRandom(req);
3063 } else if (req.query.login && (obj.parent.loginCookieEncryptionKey != null)) {
3064 var loginCookie = obj.parent.decodeCookie(req.query.login, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
3065 //if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // If the cookie is bound to an IP address, check here.
3066 if ((loginCookie != null) && (loginCookie.a == 3) && (loginCookie.u != null) && (loginCookie.u.split('/')[1] == domain.id)) {
3067 // If a login cookie was provided, setup the session here.
3068 parent.debug('web', 'handleRootRequestEx: cookie auth ok.');
3069 delete req.session.loginmode;
3070 req.session.userid = loginCookie.u;
3071 delete req.session.currentNode;
3072 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
3073 setSessionRandom(req);
3074 } else {
3075 parent.debug('web', 'handleRootRequestEx: cookie auth failed.');
3076 }
3077 } else if (domain.sspi != null) {
3078 // SSPI login (Windows only)
3079 //console.log(req.connection.user, req.connection.userSid);
3080 if ((req.connection.user == null) || (req.connection.userSid == null)) {
3081 parent.debug('web', 'handleRootRequestEx: SSPI no user auth.');
3082 res.sendStatus(404); return;
3083 } else {
3084 nologout = true;
3085 req.session.userid = 'user/' + domain.id + '/' + req.connection.user.toLowerCase();
3086 req.session.usersid = req.connection.userSid;
3087 req.session.usersGroups = req.connection.userGroups;
3088 delete req.session.currentNode;
3089 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
3090 setSessionRandom(req);
3091 obj.parent.authLog('https', 'Accepted SSPI-auth for ' + req.connection.user + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'], sessionid: req.session.x });
3092
3093 // Check if this user exists, create it if not.
3094 user = obj.users[req.session.userid];
3095 if ((user == null) || (user.sid != req.session.usersid)) {
3096 // Create the domain user
3097 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) };
3098 if (domain.newaccountsrights) { user2.siteadmin = domain.newaccountsrights; }
3099 if (obj.common.validateStrArray(domain.newaccountrealms)) { user2.groups = domain.newaccountrealms; }
3100 for (var i in obj.users) { if (obj.users[i].domain == domain.id) { usercount++; } }
3101 if (usercount == 0) { user2.siteadmin = 4294967295; } // If this is the first user, give the account site admin.
3102
3103 // Auto-join any user groups
3104 if (typeof domain.newaccountsusergroups == 'object') {
3105 for (var i in domain.newaccountsusergroups) {
3106 var ugrpid = domain.newaccountsusergroups[i];
3107 if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
3108 var ugroup = obj.userGroups[ugrpid];
3109 if (ugroup != null) {
3110 // Add group to the user
3111 if (user2.links == null) { user2.links = {}; }
3112 user2.links[ugroup._id] = { rights: 1 };
3113
3114 // Add user to the group
3115 ugroup.links[user2._id] = { userid: user2._id, name: user2.name, rights: 1 };
3116 db.Set(ugroup);
3117
3118 // Notify user group change
3119 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 };
3120 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.
3121 parent.DispatchEvent(['*', ugroup._id, user2._id], obj, event);
3122 }
3123 }
3124 }
3125
3126 obj.users[req.session.userid] = user2;
3127 obj.db.SetUser(user2);
3128 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 };
3129 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.
3130 obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
3131 parent.debug('web', 'handleRootRequestEx: SSPI new domain user.');
3132 }
3133 }
3134 }
3135
3136 // Figure out the minimal password requirement
3137 var passRequirements = null;
3138 if (domain.passwordrequirements != null) {
3139 if (domain.passrequirementstr == null) {
3140 var passRequirements = {};
3141 if (typeof domain.passwordrequirements.min == 'number') { passRequirements.min = domain.passwordrequirements.min; }
3142 if (typeof domain.passwordrequirements.max == 'number') { passRequirements.max = domain.passwordrequirements.max; }
3143 if (typeof domain.passwordrequirements.upper == 'number') { passRequirements.upper = domain.passwordrequirements.upper; }
3144 if (typeof domain.passwordrequirements.lower == 'number') { passRequirements.lower = domain.passwordrequirements.lower; }
3145 if (typeof domain.passwordrequirements.numeric == 'number') { passRequirements.numeric = domain.passwordrequirements.numeric; }
3146 if (typeof domain.passwordrequirements.nonalpha == 'number') { passRequirements.nonalpha = domain.passwordrequirements.nonalpha; }
3147 domain.passwordrequirementsstr = encodeURIComponent(JSON.stringify(passRequirements));
3148 }
3149 passRequirements = domain.passwordrequirementsstr;
3150 }
3151
3152 // If a user exists and is logged in, serve the default app, otherwise server the login app.
3153 if (req.session && req.session.userid && obj.users[req.session.userid]) {
3154 const user = obj.users[req.session.userid];
3155
3156 // Check if we are in maintenance mode
3157 if ((parent.config.settings.maintenancemode != null) && (user.siteadmin != 4294967295)) {
3158 req.session.messageid = 115; // Server under maintenance
3159 req.session.loginmode = 1;
3160 res.redirect(domain.url);
3161 return;
3162 }
3163
3164 // If the request has a "meshmessengerid", redirect to MeshMessenger
3165 // This situation happens when you get a push notification for a chat session, but are not logged in.
3166 if (req.query.meshmessengerid != null) {
3167 res.redirect(domain.url + 'messenger?id=' + encodeURIComponent(req.query.meshmessengerid) + ((req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : ''));
3168 return;
3169 }
3170
3171 const xdbGetFunc = function dbGetFunc(err, states) {
3172 if (dbGetFunc.req.session.userid.split('/')[1] != domain.id) { // Check if the session is for the correct domain
3173 parent.debug('web', 'handleRootRequestEx: incorrect domain.');
3174 dbGetFunc.req.session = null;
3175 dbGetFunc.res.redirect(domain.url + getQueryPortion(dbGetFunc.req)); // BAD***
3176 return;
3177 }
3178
3179 // Check if this is a locked account
3180 if ((dbGetFunc.user.siteadmin != null) && ((dbGetFunc.user.siteadmin & 32) != 0) && (dbGetFunc.user.siteadmin != 0xFFFFFFFF)) {
3181 // Locked account
3182 parent.debug('web', 'handleRootRequestEx: locked account.');
3183 delete dbGetFunc.req.session.userid;
3184 delete dbGetFunc.req.session.currentNode;
3185 delete dbGetFunc.req.session.passhint;
3186 delete dbGetFunc.req.session.cuserid;
3187 dbGetFunc.req.session.messageid = 110; // Account locked.
3188 dbGetFunc.res.redirect(domain.url + getQueryPortion(dbGetFunc.req)); // BAD***
3189 return;
3190 }
3191
3192 var viewmode = 1;
3193 if (dbGetFunc.req.session.viewmode) {
3194 viewmode = dbGetFunc.req.session.viewmode;
3195 delete dbGetFunc.req.session.viewmode;
3196 } else if (dbGetFunc.req.query.viewmode) {
3197 viewmode = dbGetFunc.req.query.viewmode;
3198 }
3199 var currentNode = '';
3200 if (dbGetFunc.req.session.currentNode) {
3201 currentNode = dbGetFunc.req.session.currentNode;
3202 delete dbGetFunc.req.session.currentNode;
3203 } else if (dbGetFunc.req.query.node) {
3204 currentNode = 'node/' + domain.id + '/' + dbGetFunc.req.query.node;
3205 }
3206 var logoutcontrols = {};
3207 if (obj.args.nousers != true) { logoutcontrols.name = user.name; }
3208
3209 // Give the web page a list of supported server features for this domain and user
3210 const allFeatures = obj.getDomainUserFeatures(domain, dbGetFunc.user, dbGetFunc.req);
3211
3212 // Create a authentication cookie
3213 const authCookie = obj.parent.encodeCookie({ userid: dbGetFunc.user._id, domainid: domain.id, ip: req.clientIp }, obj.parent.loginCookieEncryptionKey);
3214 const authRelayCookie = obj.parent.encodeCookie({ ruserid: dbGetFunc.user._id, x: req.session.x }, obj.parent.loginCookieEncryptionKey);
3215
3216 // Send the main web application
3217 var extras = (dbGetFunc.req.query.key != null) ? ('&key=' + dbGetFunc.req.query.key) : '';
3218 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
3219 var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
3220
3221 // Clean up the U2F challenge if needed
3222 if (dbGetFunc.req.session.u2f) { delete dbGetFunc.req.session.u2f; };
3223 if (dbGetFunc.req.session.e) {
3224 const sec = parent.decryptSessionData(dbGetFunc.req.session.e);
3225 if (sec.u2f != null) { delete sec.u2f; dbGetFunc.req.session.e = parent.encryptSessionData(sec); }
3226 }
3227
3228 // Intel AMT Scanning options
3229 var amtscanoptions = '';
3230 if (typeof domain.amtscanoptions == 'string') { amtscanoptions = encodeURIComponent(domain.amtscanoptions); }
3231 else if (obj.common.validateStrArray(domain.amtscanoptions)) { domain.amtscanoptions = domain.amtscanoptions.join(','); amtscanoptions = encodeURIComponent(domain.amtscanoptions); }
3232
3233 // Fetch the web state
3234 parent.debug('web', 'handleRootRequestEx: success.');
3235
3236 var webstate = '{}';
3237 if ((err == null) && (states != null) && (Array.isArray(states)) && (states.length == 1) && (states[0].state != null)) { webstate = obj.filterUserWebState(states[0].state); }
3238 if ((webstate == '{}') && (typeof domain.defaultuserwebstate == 'object')) { webstate = JSON.stringify(domain.defaultuserwebstate); } // User has no web state, use defaults.
3239 if (typeof domain.forceduserwebstate == 'object') { // Forces initial user web state if present, use it.
3240 var webstate2 = {};
3241 try { if (webstate != '{}') { webstate2 = JSON.parse(webstate); } } catch (ex) { }
3242 for (var i in domain.forceduserwebstate) { webstate2[i] = domain.forceduserwebstate[i]; }
3243 webstate = JSON.stringify(webstate2);
3244 }
3245
3246 // Custom user interface
3247 var customui = '';
3248 if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); }
3249
3250 // Custom files (CSS and JS)
3251 var customFiles = '';
3252 if (domain.customFiles != null) {
3253 customFiles = encodeURIComponent(JSON.stringify(domain.customFiles));
3254 } else if (domain.customfiles != null) {
3255 customFiles = encodeURIComponent(JSON.stringify(domain.customfiles));
3256 }
3257
3258 // Server features
3259 var serverFeatures = 255;
3260 if (domain.myserver === false) { serverFeatures = 0; } // 64 = Show "My Server" tab
3261 else if (typeof domain.myserver == 'object') {
3262 if (domain.myserver.backup !== true) { serverFeatures -= 1; } // Disallow simple server backups
3263 if (domain.myserver.restore !== true) { serverFeatures -= 2; } // Disallow simple server restore
3264 if (domain.myserver.upgrade !== true) { serverFeatures -= 4; } // Disallow server upgrade
3265 if (domain.myserver.errorlog !== true) { serverFeatures -= 8; } // Disallow show server crash log
3266 if (domain.myserver.console !== true) { serverFeatures -= 16; } // Disallow server console
3267 if (domain.myserver.trace !== true) { serverFeatures -= 32; } // Disallow server tracing
3268 if (domain.myserver.config !== true) { serverFeatures -= 128; } // Disallow server configuration
3269 }
3270 if (obj.db.databaseType != 1) { // If not using NeDB, we can't backup using the simple system.
3271 if ((serverFeatures & 1) != 0) { serverFeatures -= 1; } // Disallow server backups
3272 if ((serverFeatures & 2) != 0) { serverFeatures -= 2; } // Disallow simple server restore
3273 }
3274
3275 // Get WebRTC configuration
3276 var webRtcConfig = null;
3277 if (obj.parent.config.settings && obj.parent.config.settings.webrtcconfig && (typeof obj.parent.config.settings.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(obj.parent.config.settings.webrtcconfig)).replace(/'/g, '%27'); }
3278 else if (args.webrtcconfig && (typeof args.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(args.webrtcconfig)).replace(/'/g, '%27'); }
3279
3280 // Load default page style or new modern ui
3281 var uiViewMode = 'default';
3282 var webstateJSON = JSON.parse(webstate);
3283 if (req.query.sitestyle != null) {
3284 if (req.query.sitestyle == 3) { uiViewMode = 'default3'; }
3285 } else if (webstateJSON && webstateJSON.uiViewMode == 3) {
3286 uiViewMode = 'default3';
3287 } else if (domain.sitestyle == 3) {
3288 uiViewMode = 'default3';
3289 }
3290 // Refresh the session
3291 render(dbGetFunc.req, dbGetFunc.res, getRenderPage(uiViewMode, dbGetFunc.req, domain), getRenderArgs({
3292 authCookie: authCookie,
3293 authRelayCookie: authRelayCookie,
3294 viewmode: viewmode,
3295 currentNode: currentNode,
3296 logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27'),
3297 domain: domain.id,
3298 debuglevel: parent.debugLevel,
3299 serverDnsName: obj.getWebServerName(domain, req),
3300 serverRedirPort: args.redirport,
3301 serverPublicPort: httpsPort,
3302 serverfeatures: serverFeatures,
3303 features: allFeatures.features,
3304 features2: allFeatures.features2,
3305 features3: allFeatures.features3,
3306 sessiontime: (args.sessiontime) ? args.sessiontime : 60,
3307 mpspass: args.mpspass,
3308 passRequirements: passRequirements,
3309 customui: customui,
3310 customFiles: customFiles,
3311 webcerthash: Buffer.from(obj.webCertificateFullHashs[domain.id], 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'),
3312 footer: (domain.footer == null) ? '' : obj.common.replacePlaceholders(domain.footer, {
3313 'serverversion': obj.parent.currentVer,
3314 'servername': obj.getWebServerName(domain, req),
3315 'agentsessions': Object.keys(parent.webserver.wsagents).length,
3316 'connectedusers': Object.keys(parent.webserver.wssessions).length,
3317 'userssessions': Object.keys(parent.webserver.wssessions2).length,
3318 'relaysessions': parent.webserver.relaySessionCount,
3319 'relaycount': Object.keys(parent.webserver.wsrelays).length
3320 }),
3321 webstate: encodeURIComponent(webstate).replace(/'/g, '%27'),
3322 amtscanoptions: amtscanoptions,
3323 pluginHandler: (parent.pluginHandler == null) ? 'null' : parent.pluginHandler.prepExports(),
3324 webRelayPort: ((args.relaydns != null) ? ((typeof args.aliasport == 'number') ? args.aliasport : args.port) : ((parent.webrelayserver != null) ? ((typeof args.relayaliasport == 'number') ? args.relayaliasport : parent.webrelayserver.port) : 0)),
3325 webRelayDns: ((args.relaydns != null) ? args.relaydns[0] : ''),
3326 hidePowerTimeline: (domain.hidepowertimeline ? 'true' : 'false'),
3327 showNotesPanel: (domain.shownotespanel ? 'true' : 'false'),
3328 userSessionsSort: (domain.usersessionssort ? domain.usersessionssort : 'SessionId'),
3329 webrtcconfig: webRtcConfig,
3330 collapseGroups: (domain.collapsegroups ? 'true' : 'false')
3331 }, dbGetFunc.req, domain, uiViewMode), user);
3332 }
3333 xdbGetFunc.req = req;
3334 xdbGetFunc.res = res;
3335 xdbGetFunc.user = user;
3336 obj.db.Get('ws' + user._id, xdbGetFunc);
3337 } else {
3338 // Send back the login application
3339 // If this is a 2 factor auth request, look for a hardware key challenge.
3340 // Normal login 2 factor request
3341 if (req.session && (req.session.loginmode == 4)) {
3342 const sec = parent.decryptSessionData(req.session.e);
3343 if ((sec != null) && (typeof sec.tuserid == 'string')) {
3344 const user = obj.users[sec.tuserid];
3345 if (user != null) {
3346 parent.debug('web', 'handleRootRequestEx: sending 2FA challenge.');
3347 getHardwareKeyChallenge(req, domain, user, function (hwchallenge) { handleRootRequestLogin(req, res, domain, hwchallenge, passRequirements); });
3348 return;
3349 }
3350 }
3351 }
3352 // Password recovery 2 factor request
3353 if (req.session && (req.session.loginmode == 5) && (req.session.temail)) {
3354 obj.db.GetUserWithVerifiedEmail(domain.id, req.session.temail, function (err, docs) {
3355 if ((err != null) || (docs.length == 0)) {
3356 parent.debug('web', 'handleRootRequestEx: password recover 2FA fail.');
3357 req.session = null;
3358 res.redirect(domain.url + getQueryPortion(req)); // BAD***
3359 } else {
3360 var user = obj.users[docs[0]._id];
3361 if (user != null) {
3362 parent.debug('web', 'handleRootRequestEx: password recover 2FA challenge.');
3363 getHardwareKeyChallenge(req, domain, user, function (hwchallenge) { handleRootRequestLogin(req, res, domain, hwchallenge, passRequirements); });
3364 } else {
3365 parent.debug('web', 'handleRootRequestEx: password recover 2FA no user.');
3366 req.session = null;
3367 res.redirect(domain.url + getQueryPortion(req)); // BAD***
3368 }
3369 }
3370 });
3371 return;
3372 }
3373 handleRootRequestLogin(req, res, domain, '', passRequirements);
3374 }
3375 }
3376
3377 // Return a list of server supported features for a given domain and user
3378 obj.getDomainUserFeatures = function (domain, user, req) {
3379 var features = 0;
3380 var features2 = 0;
3381 var features3 = 0;
3382 if (obj.args.wanonly == true) { features += 0x00000001; } // WAN-only mode
3383 if (obj.args.lanonly == true) { features += 0x00000002; } // LAN-only mode
3384 if (obj.args.nousers == true) { features += 0x00000004; } // Single user mode
3385 if (domain.userQuota == -1) { features += 0x00000008; } // No server files mode
3386 if (obj.args.mpstlsoffload) { features += 0x00000010; } // No mutual-auth CIRA
3387 if ((parent.config.settings.allowframing != null) || (domain.allowframing != null) || (parent.config.settings.allowedframingorigins != null) || (domain.allowedframingorigins != null)) { features += 0x00000020; } // Allow site within iframe
3388 if ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true)) { features += 0x00000040; } // Email invites
3389 if (obj.args.webrtc == true) { features += 0x00000080; } // Enable WebRTC (Default false for now)
3390 // 0x00000100 --> This feature flag is free for future use.
3391 if (obj.args.allowhighqualitydesktop !== false) { features += 0x00000200; } // Enable AllowHighQualityDesktop (Default true)
3392 if ((obj.args.lanonly == true) || (obj.args.mpsport == 0)) { features += 0x00000400; } // No CIRA
3393 if ((obj.parent.serverSelfWriteAllowed == true) && (user != null) && ((user.siteadmin & 0x00000010) != 0)) { features += 0x00000800; } // Server can self-write (Allows self-update)
3394 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
3395 if (domain.agentnoproxy === true) { features += 0x00002000; } // Indicates that agents should be installed without using a HTTP proxy
3396 if ((parent.config.settings.no2factorauth !== true) && domain.yubikey && domain.yubikey.id && domain.yubikey.secret && (user._id.split('/')[2][0] != '~')) { features += 0x00004000; } // Indicates Yubikey support
3397 if (domain.geolocation == true) { features += 0x00008000; } // Enable geo-location features
3398 if ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true)) { features += 0x00010000; } // Enable password hints
3399 if (parent.config.settings.no2factorauth !== true) { features += 0x00020000; } // Enable WebAuthn/FIDO2 support
3400 if ((obj.args.nousers != true) && (domain.passwordrequirements != null) && (domain.passwordrequirements.force2factor === true) && (user._id.split('/')[2][0] != '~')) {
3401 // Check if we can skip 2nd factor auth because of the source IP address
3402 var skip2factor = false;
3403 if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
3404 for (var i in domain.passwordrequirements.skip2factor) {
3405 if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) { skip2factor = true; }
3406 }
3407 }
3408 if (skip2factor == false) { features += 0x00040000; } // Force 2-factor auth
3409 }
3410 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.
3411 if (domain.amtacmactivation) { features += 0x00100000; } // Intel AMT ACM activation/upgrade is possible
3412 if (domain.usernameisemail) { features += 0x00200000; } // Username is email address
3413 if (parent.mqttbroker != null) { features += 0x00400000; } // This server supports MQTT channels
3414 if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null)) { features += 0x00800000; } // using email for 2FA is allowed
3415 if (domain.agentinvitecodes == true) { features += 0x01000000; } // Support for agent invite codes
3416 if (parent.smsserver != null) { features += 0x02000000; } // SMS messaging is supported
3417 if ((parent.smsserver != null) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false))) { features += 0x04000000; } // SMS 2FA is allowed
3418 if (domain.sessionrecording != null) { features += 0x08000000; } // Server recordings enabled
3419 if (domain.urlswitching === false) { features += 0x10000000; } // Disables the URL switching feature
3420 if (domain.novnc === false) { features += 0x20000000; } // Disables noVNC
3421 if (domain.mstsc === false) { features += 0x40000000; } // Disables MSTSC.js
3422 if (obj.isTrustedCert(domain) == false) { features += 0x80000000; } // Indicate we are not using a trusted certificate
3423 if (obj.parent.amtManager != null) { features2 += 0x00000001; } // Indicates that the Intel AMT manager is active
3424 if (obj.parent.firebase != null) { features2 += 0x00000002; } // Indicates the server supports Firebase push messaging
3425 if ((obj.parent.firebase != null) && (obj.parent.firebase.pushOnly != true)) { features2 += 0x00000004; } // Indicates the server supports Firebase two-way push messaging
3426 if (obj.parent.webpush != null) { features2 += 0x00000008; } // Indicates web push is enabled
3427 if (((obj.args.noagentupdate == 1) || (obj.args.noagentupdate == true))) { features2 += 0x00000010; } // No agent update
3428 if (parent.amtProvisioningServer != null) { features2 += 0x00000020; } // Intel AMT LAN provisioning server
3429 if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.push2factor != false)) && (obj.parent.firebase != null)) { features2 += 0x00000040; } // Indicates device push notification 2FA is enabled
3430 if ((typeof domain.passwordrequirements != 'object') || ((domain.passwordrequirements.logintokens !== false) && ((Array.isArray(domain.passwordrequirements.logintokens) == false) || ((domain.passwordrequirements.logintokens.indexOf(user._id) >= 0) || (user.links && Object.keys(user.links).some(key => domain.passwordrequirements.logintokens.indexOf(key) >= 0)) )))) { features2 += 0x00000080; } // Indicates login tokens are allowed
3431 if (req.session.loginToken != null) { features2 += 0x00000100; } // LoginToken mode, no account changes.
3432 if (domain.ssh == true) { features2 += 0x00000200; } // SSH is enabled
3433 if (domain.localsessionrecording === false) { features2 += 0x00000400; } // Disable local recording feature
3434 if (domain.clipboardget == false) { features2 += 0x00000800; } // Disable clipboard get
3435 if (domain.clipboardset == false) { features2 += 0x00001000; } // Disable clipboard set
3436 if ((typeof domain.desktop == 'object') && (domain.desktop.viewonly == true)) { features2 += 0x00002000; } // Indicates remote desktop is viewonly
3437 if (domain.mailserver != null) { features2 += 0x00004000; } // Indicates email server is active
3438 if (domain.devicesearchbarserverandclientname) { features2 += 0x00008000; } // Search bar will find both server name and client name
3439 if (domain.ipkvm) { features2 += 0x00010000; } // Indicates support for IP KVM device groups
3440 if ((domain.passwordrequirements) && (domain.passwordrequirements.otp2factor == false)) { features2 += 0x00020000; } // Indicates support for OTP 2FA is disabled
3441 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.backupcode2factor === false)) { features2 += 0x00040000; } // Indicates 2FA backup codes are disabled
3442 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.single2factorwarning === false)) { features2 += 0x00080000; } // Indicates no warning if a single 2FA is in use
3443 if (domain.nightmode === 1) { features2 += 0x00100000; } // Always night mode
3444 if (domain.nightmode === 2) { features2 += 0x00200000; } // Always day mode
3445 if (domain.allowsavingdevicecredentials == false) { features2 += 0x00400000; } // Do not allow device credentials to be saved on the server
3446 if ((typeof domain.files == 'object') && (domain.files.sftpconnect === false)) { features2 += 0x00800000; } // Remove the "SFTP Connect" button in the "Files" tab when the device is agent managed
3447 if ((typeof domain.terminal == 'object') && (domain.terminal.sshconnect === false)) { features2 += 0x01000000; } // Remove the "SSH Connect" button in the "Terminal" tab when the device is agent managed
3448 if ((parent.msgserver != null) && (parent.msgserver.providers != 0)) { features2 += 0x02000000; } // User messaging server is enabled
3449 if ((parent.msgserver != null) && (parent.msgserver.providers != 0) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false))) { features2 += 0x04000000; } // User messaging 2FA is allowed
3450 if (domain.scrolltotop == true) { features2 += 0x08000000; } // Show the "Scroll to top" button
3451 if (domain.devicesearchbargroupname === true) { features2 += 0x10000000; } // Search bar will find by group name too
3452 if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.duo2factor != false)) && (typeof domain.duo2factor == 'object') && (typeof domain.duo2factor.integrationkey == 'string') && (typeof domain.duo2factor.secretkey == 'string') && (typeof domain.duo2factor.apihostname == 'string')) { features2 += 0x20000000; } // using Duo for 2FA is allowed
3453 if (domain.showmodernuitoggle == true) { features2 += 0x40000000; } // Indicates that the new UI should be shown
3454 if (domain.sitestyle === 3) { features2 |= 0x80000000; } // Indicates that Modern UI is forced (siteStyle = 3)
3455 if ((typeof domain.desktop == 'object') && (domain.desktop.disableconnectall == true)) { features3 += 0x00000001; } // Disable "Connect All" button when multiple sessions are active on a device
3456 if (domain.upninsteadofuser === true) { features3 += 0x00000002; } // Show UPN instead of username in General tab
3457 return { features: features, features2: features2, features3: features3 };
3458 }
3459
3460 function handleRootRequestLogin(req, res, domain, hardwareKeyChallenge, passRequirements) {
3461 parent.debug('web', 'handleRootRequestLogin()');
3462 var features = 0;
3463 if ((parent.config != null) && (parent.config.settings != null) && ((parent.config.settings.allowframing == true) || (typeof parent.config.settings.allowframing == 'string') || (parent.config.settings.allowedframingorigins != null) || (domain != null && domain.allowedframingorigins != null))) { features += 32; } // Allow site within iframe
3464 if (domain.usernameisemail) { features += 0x00200000; } // Username is email address
3465 var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
3466 var loginmode = 0;
3467 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.
3468
3469 // Format an error message if needed
3470 var passhint = null, msgid = 0;
3471 if (req.session != null) {
3472 msgid = req.session.messageid;
3473 if ((msgid == 5) || (loginmode == 7) || ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true))) { passhint = EscapeHtml(req.session.passhint); }
3474 delete req.session.messageid;
3475 delete req.session.passhint;
3476 }
3477 const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false));
3478 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'))
3479
3480 // Check if we are allowed to create new users using the login screen
3481 var newAccountsAllowed = true;
3482 if ((domain.newaccounts !== 1) && (domain.newaccounts !== true)) { for (var i in obj.users) { if (obj.users[i].domain == domain.id) { newAccountsAllowed = false; break; } } }
3483 if (parent.config.settings.maintenancemode != null) { newAccountsAllowed = false; }
3484
3485 // Encrypt the hardware key challenge state if needed
3486 var hwstate = null;
3487 if (hardwareKeyChallenge && req.session) {
3488 const sec = parent.decryptSessionData(req.session.e);
3489 hwstate = obj.parent.encodeCookie({ u: sec.tuser, p: sec.tpass, c: sec.u2f }, obj.parent.loginCookieEncryptionKey)
3490 }
3491
3492 // Check if we can use OTP tokens with email. We can't use email for 2FA password recovery (loginmode 5).
3493 var otpemail = (loginmode != 5) && (domain.mailserver != null) && (req.session != null) && ((req.session.temail === 1) || (typeof req.session.temail == 'string'));
3494 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; }
3495 var otpduo = (req.session != null) && (req.session.tduo === 1);
3496 if (((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.duo2factor == false)) || (typeof domain.duo2factor != 'object')) { otpduo = false; }
3497 var otpsms = (parent.smsserver != null) && (req.session != null) && (req.session.tsms === 1);
3498 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; }
3499 var otpmsg = (parent.msgserver != null) && (req.session != null) && (req.session.tmsg === 1);
3500 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.msg2factor == false)) { otpmsg = false; }
3501 var otppush = (parent.firebase != null) && (req.session != null) && (req.session.tpush === 1);
3502 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.push2factor == false)) { otppush = false; }
3503 const autofido = ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.autofido2fa == true)); // See if FIDO should be automatically prompted if user account has it.
3504
3505 // See if we support two-factor trusted cookies
3506 var twoFactorCookieDays = 30;
3507 if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
3508
3509 // See what authentication strategies we have
3510 var authStrategies = [];
3511 if (typeof domain.authstrategies == 'object') {
3512 if (typeof domain.authstrategies.twitter == 'object') { authStrategies.push('twitter'); }
3513 if (typeof domain.authstrategies.google == 'object') { authStrategies.push('google'); }
3514 if (typeof domain.authstrategies.github == 'object') { authStrategies.push('github'); }
3515 if (typeof domain.authstrategies.azure == 'object') { authStrategies.push('azure'); }
3516 if (typeof domain.authstrategies.oidc == 'object') {
3517 if (obj.common.validateObject(domain.authstrategies.oidc.custom) && obj.common.validateString(domain.authstrategies.oidc.custom.preset)) {
3518 authStrategies.push('oidc-' + domain.authstrategies.oidc.custom.preset);
3519 } else {
3520 authStrategies.push('oidc');
3521 }
3522 }
3523 if (typeof domain.authstrategies.intel == 'object') { authStrategies.push('intel'); }
3524 if (typeof domain.authstrategies.jumpcloud == 'object') { authStrategies.push('jumpcloud'); }
3525 if (typeof domain.authstrategies.saml == 'object') { authStrategies.push('saml'); }
3526 }
3527
3528 // Custom user interface
3529 var customui = '';
3530 if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); }
3531
3532 // Custom files (CSS and JS)
3533 var customFiles = '';
3534 if (domain.customFiles != null) {
3535 customFiles = encodeURIComponent(JSON.stringify(domain.customFiles));
3536 } else if (domain.customfiles != null) {
3537 customFiles = encodeURIComponent(JSON.stringify(domain.customfiles));
3538 }
3539
3540 // Get two-factor screen timeout
3541 var twoFactorTimeout = 300000; // Default is 5 minutes, 0 for no timeout.
3542 if ((typeof domain.passwordrequirements == 'object') && (typeof domain.passwordrequirements.twofactortimeout == 'number')) {
3543 twoFactorTimeout = domain.passwordrequirements.twofactortimeout * 1000;
3544 }
3545
3546 // Setup CAPTCHA if needed
3547 var newAccountCaptcha = '', newAccountCaptchaImage = '';
3548 if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
3549 newAccountCaptcha = obj.parent.encodeCookie({ type: 'newAccount', captcha: require('svg-captcha').randomText(5) }, obj.parent.loginCookieEncryptionKey);
3550 newAccountCaptchaImage = 'newAccountCaptcha.ashx?x=' + newAccountCaptcha;
3551 }
3552
3553 // Check for flash errors from passport.js and make the array unique
3554 var flashErrors = [];
3555 if (req.session.flash && req.session.flash.error) {
3556 flashErrors = obj.common.uniqueArray(req.session.flash.error);
3557 req.session.flash = null;
3558 }
3559
3560 // Render the login page
3561 // Allow configurable OIDC login button text via domain.authstrategies.oidc.custom
3562 var oidcButtonIcon, oidcButtonIcon2x, oidcButtonText;
3563 if (obj.common.validateObject(domain.authstrategies) && obj.common.validateObject(domain.authstrategies.oidc) && obj.common.validateObject(domain.authstrategies.oidc.custom)) {
3564 if (obj.common.validateUrl(domain.authstrategies.oidc.custom.buttoniconurl)) {
3565 oidcButtonIcon = domain.authstrategies.oidc.custom.buttoniconurl;
3566 if (obj.common.validateUrl(domain.authstrategies.oidc.custom.buttoniconurl2x)) {
3567 oidcButtonIcon2x = domain.authstrategies.oidc.custom.buttoniconurl2x + ' 2x';
3568 } else {
3569 oidcButtonIcon2x = domain.authstrategies.oidc.custom.buttoniconurl + ' 2x';
3570 }
3571 } else {
3572 switch (domain.authstrategies.oidc.custom.preset) {
3573 case 'azure':
3574 oidcButtonIcon = "images/login/azure32.png";
3575 oidcButtonIcon2x = "images/login/azure64.png 2x";
3576 break;
3577 case 'google':
3578 oidcButtonIcon = "images/login/google32.png";
3579 oidcButtonIcon2x = "images/login/google64.png 2x";
3580 break;
3581 default:
3582 oidcButtonIcon = "images/login/oidc32.png";
3583 oidcButtonIcon2x = "images/login/oidc64.png 2x";
3584 }
3585 }
3586
3587 if (obj.common.validateString(domain.authstrategies.oidc.custom.buttontext, 1, 128)) {
3588 oidcButtonText = domain.authstrategies.oidc.custom.buttontext;
3589 }
3590 }
3591 render(req, res,
3592 getRenderPage((domain.sitestyle >= 2) ? 'login2' : 'login', req, domain),
3593 getRenderArgs({
3594 loginmode: loginmode,
3595 rootCertLink: getRootCertLink(domain),
3596 newAccount: newAccountsAllowed, // True if new accounts are allowed from the login page
3597 newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1), // 1 if new account creation requires password
3598 newAccountCaptcha: newAccountCaptcha, // If new account creation requires a CAPTCHA, this string will not be empty
3599 newAccountCaptchaImage: newAccountCaptchaImage, // Set to the URL of the CAPTCHA image
3600 serverDnsName: obj.getWebServerName(domain, req),
3601 serverPublicPort: httpsPort,
3602 passlogin: (typeof domain.showpasswordlogin == 'boolean') ? domain.showpasswordlogin : true,
3603 emailcheck: emailcheck,
3604 features: features,
3605 sessiontime: (args.sessiontime) ? args.sessiontime : 60, // Session time in minutes, 60 minutes is the default
3606 passRequirements: passRequirements,
3607 customui: customui,
3608 customFiles: customFiles,
3609 footer: (domain.loginfooter == null) ? '' : obj.common.replacePlaceholders(domain.loginfooter, {
3610 'serverversion': obj.parent.currentVer,
3611 'servername': obj.getWebServerName(domain, req),
3612 'agentsessions': Object.keys(parent.webserver.wsagents).length,
3613 'connectedusers': Object.keys(parent.webserver.wssessions).length,
3614 'userssessions': Object.keys(parent.webserver.wssessions2).length,
3615 'relaysessions': parent.webserver.relaySessionCount,
3616 'relaycount': Object.keys(parent.webserver.wsrelays).length
3617 }),
3618 hkey: encodeURIComponent(hardwareKeyChallenge).replace(/'/g, '%27'),
3619 messageid: msgid,
3620 flashErrors: JSON.stringify(flashErrors).replace(/"/g, '\\"'),
3621 passhint: passhint,
3622
3623 welcometext: domain.welcometext ? encodeURIComponent(obj.common.replacePlaceholders(domain.welcometext, {
3624 'serverversion': obj.parent.currentVer,
3625 'servername': obj.getWebServerName(domain, req),
3626 'agentsessions': Object.keys(parent.webserver.wsagents).length,
3627 'connectedusers': Object.keys(parent.webserver.wssessions).length,
3628 'userssessions': Object.keys(parent.webserver.wssessions2).length,
3629 'relaysessions': parent.webserver.relaySessionCount,
3630 'relaycount': Object.keys(parent.webserver.wsrelays).length
3631 })).split('\'').join('\\\'') : null,
3632 welcomePictureFullScreen: ((typeof domain.welcomepicturefullscreen == 'boolean') ? domain.welcomepicturefullscreen : false),
3633 hwstate: hwstate,
3634 otpemail: otpemail,
3635 otpduo: otpduo,
3636 otpsms: otpsms,
3637 otpmsg: otpmsg,
3638 otppush: otppush,
3639 autofido: autofido,
3640 twoFactorCookieDays: twoFactorCookieDays,
3641 authStrategies: authStrategies.join(','),
3642 oidcButtonText: oidcButtonText || '',
3643 oidcButtonIcon: oidcButtonIcon || 'images/login/oidc32.png',
3644 oidcButtonIcon2x: oidcButtonIcon2x || 'images/login/oidc64.png 2x',
3645 loginpicture: (typeof domain.loginpicture == 'string'),
3646 tokenTimeout: twoFactorTimeout, // Two-factor authentication screen timeout in milliseconds,
3647 renderLanguages: obj.renderLanguages,
3648 showLanguageSelect: domain.showlanguageselect ? domain.showlanguageselect : false,
3649 }, req, domain, (domain.sitestyle >= 2) ? 'login2' : 'login'));
3650 }
3651
3652 // Handle a post request on the root
3653 function handleRootPostRequest(req, res) {
3654 const domain = checkUserIpAddress(req, res);
3655 if (domain == null) { return; }
3656 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.end("Not Found"); return; } // Check 3FA URL key
3657 if (req.body == null) { req.body = {}; }
3658 parent.debug('web', 'handleRootPostRequest, action: ' + req.body.action);
3659
3660 // If a HTTP header is required, check new UserRequiredHttpHeader
3661 if (domain.userrequiredhttpheader && (typeof domain.userrequiredhttpheader == 'object')) { var ok = false; for (var i in req.headers) { if (domain.userrequiredhttpheader[i.toLowerCase()] == req.headers[i]) { ok = true; } } if (ok == false) { res.sendStatus(404); return; } }
3662
3663 switch (req.body.action) {
3664 case 'login': { handleLoginRequest(req, res, true); break; }
3665 case 'tokenlogin': {
3666 if (req.body.hwstate) {
3667 var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 10);
3668 if (cookie != null) { req.session.e = parent.encryptSessionData({ tuser: cookie.u, tpass: cookie.p, u2f: cookie.c }); }
3669 }
3670 handleLoginRequest(req, res, true); break;
3671 }
3672 case 'pushlogin': {
3673 if (req.body.hwstate) {
3674 var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 1);
3675 if ((cookie != null) && (typeof cookie.u == 'string') && (cookie.d == domain.id) && (cookie.a == 'pushAuth')) {
3676 // Push authentication is a success, login the user
3677 req.session = { userid: cookie.u };
3678
3679 // Check if we need to remember this device
3680 if ((req.body.remembertoken === 'on') && ((domain.twofactorcookiedurationdays == null) || (domain.twofactorcookiedurationdays > 0))) {
3681 var maxCookieAge = domain.twofactorcookiedurationdays;
3682 if (typeof maxCookieAge != 'number') { maxCookieAge = 30; }
3683 const twoFactorCookie = obj.parent.encodeCookie({ userid: cookie.u, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey);
3684 res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: parent.config.settings.sessionsamesite, secure: true });
3685 }
3686 var user = obj.users[cookie.u];
3687 // Notify account login
3688 var targets = ['*', 'server-users', user._id];
3689 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
3690 const ua = obj.getUserAgentInfo(req);
3691 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: 'pushlogin' };
3692 obj.parent.DispatchEvent(targets, obj, loginEvent);
3693 handleRootRequestEx(req, res, domain);
3694 return;
3695 }
3696 }
3697 handleLoginRequest(req, res, true); break;
3698 }
3699 case 'changepassword': { handlePasswordChangeRequest(req, res, true); break; }
3700 case 'deleteaccount': { handleDeleteAccountRequest(req, res, true); break; }
3701 case 'createaccount': { handleCreateAccountRequest(req, res, true); break; }
3702 case 'resetpassword': { handleResetPasswordRequest(req, res, true); break; }
3703 case 'resetaccount': { handleResetAccountRequest(req, res, true); break; }
3704 case 'checkemail': { handleCheckAccountEmailRequest(req, res, true); break; }
3705 default: { handleLoginRequest(req, res, true); break; }
3706 }
3707 }
3708
3709 // Return true if it looks like we are using a real TLS certificate.
3710 obj.isTrustedCert = function (domain) {
3711 if ((domain != null) && (typeof domain.trustedcert == 'boolean')) return domain.trustedcert; // If the status of the cert specified, use that.
3712 if (typeof obj.args.trustedcert == 'boolean') return obj.args.trustedcert; // If the status of the cert specified, use that.
3713 if (obj.args.tlsoffload != null) return true; // We are using TLS offload, a real cert is likely used.
3714 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.
3715 if ((typeof obj.certificates.WebIssuer == 'string') && (obj.certificates.WebIssuer.indexOf('MeshCentralRoot-') == 0)) return false; // Our cert is issued by self-signed cert.
3716 if (obj.certificates.CommonName.indexOf('.') == -1) return false; // Our cert is named with a fake name
3717 return true; // This is a guess
3718 }
3719
3720 // Get the link to the root certificate if needed
3721 function getRootCertLink(domain) {
3722 // Check if the HTTPS certificate is issued from MeshCentralRoot, if so, add download link to root certificate.
3723 if (obj.isTrustedCert(domain) == false) {
3724 // Get the domain suffix
3725 var xdomain = (domain.dns == null) ? domain.id : '';
3726 if (xdomain != '') xdomain += '/';
3727 return '<a href=/' + xdomain + 'MeshServerRootCert.cer title="Download the root certificate for this server">Root Certificate</a>';
3728 }
3729 return '';
3730 }
3731
3732 // Serve the xterm page
3733 function handleXTermRequest(req, res) {
3734 const domain = checkUserIpAddress(req, res);
3735 if (domain == null) { return; }
3736 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
3737
3738 parent.debug('web', 'handleXTermRequest: sending xterm');
3739 res.set({ 'Cache-Control': 'no-store' });
3740 if (req.session && req.session.userid) {
3741 if (req.session.userid.split('/')[1] != domain.id) { res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain
3742 var user = obj.users[req.session.userid];
3743 if ((user == null) || (req.query.nodeid == null)) { res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the user exists
3744
3745 // Check permissions
3746 obj.GetNodeWithRights(domain, user, req.query.nodeid, function (node, rights, visible) {
3747 if ((node == null) || ((rights & 8) == 0) || ((rights != 0xFFFFFFFF) && ((rights & 512) != 0))) { res.redirect(domain.url + getQueryPortion(req)); return; }
3748
3749 var logoutcontrols = { name: user.name };
3750 var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : '';
3751 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
3752
3753 // Create a authentication cookie
3754 const authCookie = obj.parent.encodeCookie({ userid: user._id, domainid: domain.id, ip: req.clientIp }, obj.parent.loginCookieEncryptionKey);
3755 const authRelayCookie = obj.parent.encodeCookie({ ruserid: user._id, domainid: domain.id }, obj.parent.loginCookieEncryptionKey);
3756 var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
3757 render(req, res, getRenderPage('xterm', req, domain), getRenderArgs({ serverDnsName: obj.getWebServerName(domain, req), serverRedirPort: args.redirport, serverPublicPort: httpsPort, authCookie: authCookie, authRelayCookie: authRelayCookie, logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27'), name: EscapeHtml(node.name) }, req, domain));
3758 });
3759 } else {
3760 res.redirect(domain.url + getQueryPortion(req));
3761 return;
3762 }
3763 }
3764
3765 // Handle new account Captcha GET
3766 function handleNewAccountCaptchaRequest(req, res) {
3767 const domain = checkUserIpAddress(req, res);
3768 if (domain == null) { return; }
3769 if ((domain.newaccountscaptcha == null) || (domain.newaccountscaptcha === false) || (req.query.x == null)) { res.sendStatus(404); return; }
3770 const c = obj.parent.decodeCookie(req.query.x, obj.parent.loginCookieEncryptionKey);
3771 if ((c == null) || (c.type !== 'newAccount') || (typeof c.captcha != 'string')) { res.sendStatus(404); return; }
3772 res.type('svg');
3773 res.status(200).end(require('svg-captcha')(c.captcha, {}));
3774 }
3775
3776 // Handle Captcha GET
3777 function handleCaptchaGetRequest(req, res) {
3778 const domain = checkUserIpAddress(req, res);
3779 if (domain == null) { return; }
3780 if (parent.crowdSecBounser == null) { res.sendStatus(404); return; }
3781 parent.crowdSecBounser.applyCaptcha(req, res, function () { res.redirect((((domain.id == '') && (domain.dns == null)) ? '/' : ('/' + domain.id))); });
3782 }
3783
3784 // Handle Captcha POST
3785 function handleCaptchaPostRequest(req, res) {
3786 if (parent.crowdSecBounser == null) { res.sendStatus(404); return; }
3787 const domain = checkUserIpAddress(req, res);
3788 if (domain == null) { return; }
3789 req.originalUrl = (((domain.id == '') && (domain.dns == null)) ? '/' : ('/' + domain.id));
3790 parent.crowdSecBounser.applyCaptcha(req, res, function () { res.redirect(req.originalUrl); });
3791 }
3792
3793 // Render the terms of service.
3794 function handleTermsRequest(req, res) {
3795 const domain = checkUserIpAddress(req, res);
3796 if (domain == null) { return; }
3797 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
3798
3799 // See if term.txt was loaded from the database
3800 if ((parent.configurationFiles != null) && (parent.configurationFiles['terms.txt'] != null)) {
3801 // Send the terms from the database
3802 res.set({ 'Cache-Control': 'no-store' });
3803 if (req.session && req.session.userid) {
3804 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
3805 var user = obj.users[req.session.userid];
3806 var logoutcontrols = { name: user.name };
3807 var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : '';
3808 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
3809 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));
3810 } else {
3811 render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(parent.configurationFiles['terms.txt'].toString()).split('\'').join('\\\''), logoutControls: encodeURIComponent('{}') }, req, domain));
3812 }
3813 } else {
3814 // See if there is a terms.txt file in meshcentral-data
3815 var p = obj.path.join(obj.parent.datapath, 'terms.txt');
3816 if (obj.fs.existsSync(p)) {
3817 obj.fs.readFile(p, 'utf8', function (err, data) {
3818 if (err != null) { parent.debug('web', 'handleTermsRequest: no terms.txt'); res.sendStatus(404); return; }
3819
3820 // Send the terms from terms.txt
3821 res.set({ 'Cache-Control': 'no-store' });
3822 if (req.session && req.session.userid) {
3823 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
3824 var user = obj.users[req.session.userid];
3825 var logoutcontrols = { name: user.name };
3826 var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : '';
3827 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
3828 render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(data).split('\'').join('\\\''), logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain));
3829 } else {
3830 render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(data).split('\'').join('\\\''), logoutControls: encodeURIComponent('{}') }, req, domain));
3831 }
3832 });
3833 } else {
3834 // Send the default terms
3835 parent.debug('web', 'handleTermsRequest: sending default terms');
3836 res.set({ 'Cache-Control': 'no-store' });
3837 if (req.session && req.session.userid) {
3838 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
3839 var user = obj.users[req.session.userid];
3840 var logoutcontrols = { name: user.name };
3841 var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : '';
3842 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
3843 render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain));
3844 } else {
3845 render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ logoutControls: encodeURIComponent('{}') }, req, domain));
3846 }
3847 }
3848 }
3849 }
3850
3851 // Render the messenger application.
3852 function handleMessengerRequest(req, res) {
3853 const domain = getDomain(req);
3854 if (domain == null) { parent.debug('web', 'handleMessengerRequest: no domain'); res.sendStatus(404); return; }
3855 parent.debug('web', 'handleMessengerRequest()');
3856
3857 // Check if we are in maintenance mode
3858 if (parent.config.settings.maintenancemode != null) {
3859 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));
3860 return;
3861 }
3862
3863 // Check if this session is for a user
3864 if (req.query.id == null) { res.sendStatus(404); return; }
3865 var idSplit = decodeURIComponent(req.query.id).split('/');
3866 if ((idSplit.length != 7) || (idSplit[0] != 'meshmessenger')) { res.sendStatus(404); return; }
3867 if ((idSplit[1] == 'user') && (idSplit[4] == 'user')) {
3868 // This is a user to user conversation, both users must be logged in.
3869 var user1 = idSplit[1] + '/' + idSplit[2] + '/' + idSplit[3]
3870 var user2 = idSplit[4] + '/' + idSplit[5] + '/' + idSplit[6]
3871 if (!req.session || !req.session.userid) {
3872 // Redirect to login page
3873 if (req.query.key != null) { res.redirect(domain.url + '?key=' + encodeURIComponent(req.query.key) + '&meshmessengerid=' + encodeURIComponent(req.query.id)); } else { res.redirect(domain.url + '?meshmessengerid=' + encodeURIComponent(req.query.id)); }
3874 return;
3875 }
3876 if ((req.session.userid != user1) && (req.session.userid != user2)) { res.sendStatus(404); return; }
3877 }
3878
3879 // Get WebRTC configuration
3880 var webRtcConfig = null;
3881 if (obj.parent.config.settings && obj.parent.config.settings.webrtcconfig && (typeof obj.parent.config.settings.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(obj.parent.config.settings.webrtcconfig)).replace(/'/g, '%27'); }
3882 else if (args.webrtcconfig && (typeof args.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(args.webrtcconfig)).replace(/'/g, '%27'); }
3883
3884 // Setup other options
3885 var options = { webrtcconfig: webRtcConfig };
3886 if (typeof domain.meshmessengertitle == 'string') { options.meshMessengerTitle = domain.meshmessengertitle; } else { options.meshMessengerTitle = '!'; }
3887
3888 // Get the userid and name
3889 if ((domain.meshmessengertitle != null) && (req.query.id != null) && (req.query.id.startsWith('meshmessenger/node'))) {
3890 if (idSplit.length == 7) {
3891 const user = obj.users[idSplit[4] + '/' + idSplit[5] + '/' + idSplit[6]];
3892 if (user != null) {
3893 if (domain.meshmessengertitle.indexOf('{0}') >= 0) { options.username = encodeURIComponent(user.realname ? user.realname : user.name).replace(/'/g, '%27'); }
3894 if (domain.meshmessengertitle.indexOf('{1}') >= 0) { options.userid = encodeURIComponent(user.name).replace(/'/g, '%27'); }
3895 }
3896 }
3897 }
3898
3899 // Render the page
3900 res.set({ 'Cache-Control': 'no-store' });
3901 render(req, res, getRenderPage('messenger', req, domain), getRenderArgs(options, req, domain));
3902 }
3903
3904 // Handle messenger image request
3905 function handleMessengerImageRequest(req, res) {
3906 const domain = getDomain(req);
3907 if (domain == null) { parent.debug('web', 'handleMessengerImageRequest: no domain'); res.sendStatus(404); return; }
3908 parent.debug('web', 'handleMessengerImageRequest()');
3909
3910 // Check if we are in maintenance mode
3911 if (parent.config.settings.maintenancemode != null) { res.sendStatus(404); return; }
3912
3913 //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
3914 if (domain.meshmessengerpicture) {
3915 // Use the configured messenger logo picture
3916 try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.meshmessengerpicture)); return; } catch (ex) { }
3917 }
3918
3919 var imagefile = 'images/messenger.png';
3920 if (domain.webpublicpath != null) {
3921 obj.fs.exists(obj.path.join(domain.webpublicpath, imagefile), function (exists) {
3922 if (exists) {
3923 // Use the domain logo picture
3924 try { res.sendFile(obj.path.join(domain.webpublicpath, imagefile)); } catch (ex) { res.sendStatus(404); }
3925 } else {
3926 // Use the default logo picture
3927 try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
3928 }
3929 });
3930 } else if (parent.webPublicOverridePath) {
3931 obj.fs.exists(obj.path.join(obj.parent.webPublicOverridePath, imagefile), function (exists) {
3932 if (exists) {
3933 // Use the override logo picture
3934 try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, imagefile)); } catch (ex) { res.sendStatus(404); }
3935 } else {
3936 // Use the default logo picture
3937 try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
3938 }
3939 });
3940 } else {
3941 // Use the default logo picture
3942 try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
3943 }
3944 }
3945
3946 // Returns the server root certificate encoded in base64
3947 function getRootCertBase64() {
3948 var rootcert = obj.certificates.root.cert;
3949 var i = rootcert.indexOf('-----BEGIN CERTIFICATE-----\r\n');
3950 if (i >= 0) { rootcert = rootcert.substring(i + 29); }
3951 i = rootcert.indexOf('-----END CERTIFICATE-----');
3952 if (i >= 0) { rootcert = rootcert.substring(i, 0); }
3953 return Buffer.from(rootcert, 'base64').toString('base64');
3954 }
3955
3956 // Returns the mesh server root certificate
3957 function handleRootCertRequest(req, res) {
3958 const domain = getDomain(req);
3959 if (domain == null) { parent.debug('web', 'handleRootCertRequest: no domain'); res.sendStatus(404); return; }
3960 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
3961 if ((obj.userAllowedIp != null) && (checkIpAddressEx(req, res, obj.userAllowedIp, false) === false)) { parent.debug('web', 'handleRootCertRequest: invalid ip'); return; } // Check server-wide IP filter only.
3962 parent.debug('web', 'handleRootCertRequest()');
3963 setContentDispositionHeader(res, 'application/octet-stream', certificates.RootName + '.cer', null, 'rootcert.cer');
3964 res.send(Buffer.from(getRootCertBase64(), 'base64'));
3965 }
3966
3967 // Return a customised mainifest.json for PWA
3968 function handleManifestRequest(req, res){
3969 const domain = checkUserIpAddress(req);
3970 if (domain == null) { parent.debug('web', 'handleManifestRequest: no domain'); res.sendStatus(404); return; }
3971 parent.debug('web', 'handleManifestRequest()');
3972 var manifest = {
3973 "name": (domain.title != null) ? domain.title : 'MeshCentral',
3974 "short_name": (domain.title != null) ? domain.title : 'MeshCentral',
3975 "description": "Open source web based, remote computer management.",
3976 "scope": ".",
3977 "start_url": "/",
3978 "display": "fullscreen",
3979 "orientation": "any",
3980 "theme_color": "#ffffff",
3981 "background_color": "#ffffff",
3982 "icons": [{
3983 "src": "pwalogo.png",
3984 "sizes": "512x512",
3985 "type": "image/png"
3986 }]
3987 };
3988 res.json(manifest);
3989 }
3990
3991 // Handle user public file downloads
3992 function handleDownloadUserFiles(req, res) {
3993 const domain = checkUserIpAddress(req, res);
3994 if (domain == null) { return; }
3995 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
3996
3997 if (obj.common.validateString(req.path, 1, 4096) == false) { res.sendStatus(404); return; }
3998 var domainname = 'domain', spliturl = decodeURIComponent(req.path).split('/'), filename = '';
3999 if (spliturl[1] != 'userfiles') { spliturl.splice(1,1); } // remove domain.id from url for domains without dns
4000 if ((spliturl.length < 3) || (obj.common.IsFilenameValid(spliturl[2]) == false) || (domain.userQuota == -1)) { res.sendStatus(404); return; }
4001 if (domain.id != '') { domainname = 'domain-' + domain.id; }
4002 var path = obj.path.join(obj.filespath, domainname + '/user-' + spliturl[2] + '/Public');
4003 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; } }
4004
4005 var stat = null;
4006 try { stat = obj.fs.statSync(path); } catch (e) { }
4007 if ((stat != null) && ((stat.mode & 0x004000) == 0)) {
4008 if (req.query.download == 1) {
4009 setContentDispositionHeader(res, 'application/octet-stream', filename, null, 'file.bin');
4010 try { res.sendFile(obj.path.resolve(__dirname, path)); } catch (e) { res.sendStatus(404); }
4011 } else {
4012 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));
4013 }
4014 } else {
4015 render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'download2' : 'download', req, domain), getRenderArgs({ rootCertLink: getRootCertLink(domain), messageid: 2 }, req, domain));
4016 }
4017 }
4018
4019 // Handle device file request
4020 function handleDeviceFile(req, res) {
4021 const domain = getDomain(req, res);
4022 if (domain == null) { return; }
4023 if ((req.query.c == null) || (req.query.f == null)) { res.sendStatus(404); return; }
4024
4025 // Check the inbound desktop sharing cookie
4026 var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
4027 if ((c == null) || (c.domainid !== domain.id)) { res.sendStatus(404); return; }
4028
4029 // Check userid
4030 const user = obj.users[c.userid];
4031 if ((c == user)) { res.sendStatus(404); return; }
4032
4033 // If this cookie has restricted usages, check that it's allowed to perform downloads
4034 if (Array.isArray(c.usages) && (c.usages.indexOf(10) < 0)) { res.sendStatus(404); return; } // Check protocol #10
4035
4036 if (c.nid != null) { req.query.n = c.nid.split('/')[2]; } // This cookie is restricted to a specific nodeid.
4037 if (req.query.n == null) { res.sendStatus(404); return; }
4038
4039 // Check if this user has permission to manage this computer
4040 obj.GetNodeWithRights(domain, user, 'node/' + domain.id + '/' + req.query.n, function (node, rights, visible) {
4041 if ((node == null) || ((rights & MESHRIGHT_REMOTECONTROL) == 0) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device
4042
4043 // All good, start the file transfer
4044 req.query.id = getRandomLowerCase(12);
4045 obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, null, res, req, domain, user, node.meshid, node._id);
4046 });
4047 }
4048
4049 // Handle download of a server file by an agent
4050 function handleAgentDownloadFile(req, res) {
4051 const domain = checkAgentIpAddress(req, res);
4052 if (domain == null) { return; }
4053 if (req.query.c == null) { res.sendStatus(404); return; }
4054
4055 // Check the inbound desktop sharing cookie
4056 var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 5); // 5 minute timeout
4057 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; }
4058
4059 // Send the file back
4060 try { res.sendFile(obj.path.join(obj.filespath, 'tmp', c.f)); return; } catch (ex) { res.sendStatus(404); }
4061 }
4062
4063 // Handle logo request
4064 function handleLogoRequest(req, res) {
4065 const domain = checkUserIpAddress(req, res);
4066 if (domain == null) { return; }
4067
4068 //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
4069 if (domain.titlepicture) {
4070 if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.titlepicture] != null)) {
4071 // Use the logo in the database
4072 res.set({ 'Content-Type': domain.titlepicture.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
4073 res.send(parent.configurationFiles[domain.titlepicture]);
4074 return;
4075 } else {
4076 // Use the logo on file
4077 try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.titlepicture)); return; } catch (ex) { }
4078 }
4079 }
4080
4081 if ((domain.webpublicpath != null) && (obj.fs.existsSync(obj.path.join(domain.webpublicpath, 'images/logoback.png')))) {
4082 // Use the domain logo picture
4083 try { res.sendFile(obj.path.join(domain.webpublicpath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
4084 } else if (parent.webPublicOverridePath && obj.fs.existsSync(obj.path.join(obj.parent.webPublicOverridePath, 'images/logoback.png'))) {
4085 // Use the override logo picture
4086 try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
4087 } else {
4088 // Use the default logo picture
4089 try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
4090 }
4091 }
4092
4093 // Handle login logo request
4094 function handleLoginLogoRequest(req, res) {
4095 const domain = checkUserIpAddress(req, res);
4096 if (domain == null) { return; }
4097
4098 //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
4099 if (domain.loginpicture) {
4100 if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.loginpicture] != null)) {
4101 // Use the logo in the database
4102 res.set({ 'Content-Type': domain.loginpicture.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
4103 res.send(parent.configurationFiles[domain.loginpicture]);
4104 return;
4105 } else {
4106 // Use the logo on file
4107 try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.loginpicture)); return; } catch (ex) { res.sendStatus(404); }
4108 }
4109 } else {
4110 res.sendStatus(404);
4111 }
4112 }
4113
4114 // Handle PWA logo request
4115 function handlePWALogoRequest(req, res) {
4116 const domain = checkUserIpAddress(req, res);
4117 if (domain == null) { return; }
4118
4119 //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
4120 if (domain.pwalogo) {
4121 if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.pwalogo] != null)) {
4122 // Use the logo in the database
4123 res.set({ 'Content-Type': domain.pwalogo.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
4124 res.send(parent.configurationFiles[domain.pwalogo]);
4125 return;
4126 } else {
4127 // Use the logo on file
4128 try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.pwalogo)); return; } catch (ex) { }
4129 }
4130 }
4131
4132 if ((domain.webpublicpath != null) && (obj.fs.existsSync(obj.path.join(domain.webpublicpath, 'android-chrome-512x512.png')))) {
4133 // Use the domain logo picture
4134 try { res.sendFile(obj.path.join(domain.webpublicpath, 'android-chrome-512x512.png')); } catch (ex) { res.sendStatus(404); }
4135 } else if (parent.webPublicOverridePath && obj.fs.existsSync(obj.path.join(obj.parent.webPublicOverridePath, 'android-chrome-512x512.png'))) {
4136 // Use the override logo picture
4137 try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, 'android-chrome-512x512.png')); } catch (ex) { res.sendStatus(404); }
4138 } else {
4139 // Use the default logo picture
4140 try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'android-chrome-512x512.png')); } catch (ex) { res.sendStatus(404); }
4141 }
4142 }
4143
4144 // Handle translation request
4145 function handleTranslationsRequest(req, res) {
4146 const domain = checkUserIpAddress(req, res);
4147 if (domain == null) { return; }
4148 //if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
4149 if ((obj.userAllowedIp != null) && (checkIpAddressEx(req, res, obj.userAllowedIp, false) === false)) { return; } // Check server-wide IP filter only.
4150
4151 var user = null;
4152 if (obj.args.user != null) {
4153 // A default user is active
4154 user = obj.users['user/' + domain.id + '/' + obj.args.user];
4155 if (!user) { parent.debug('web', 'handleTranslationsRequest: user not found.'); res.sendStatus(401); return; }
4156 } else {
4157 // Check if the user is logged and we have all required parameters
4158 if (!req.session || !req.session.userid) { parent.debug('web', 'handleTranslationsRequest: failed checks (2).'); res.sendStatus(401); return; }
4159
4160 // Get the current user
4161 user = obj.users[req.session.userid];
4162 if (!user) { parent.debug('web', 'handleTranslationsRequest: user not found.'); res.sendStatus(401); return; }
4163 if (user.siteadmin != 0xFFFFFFFF) { parent.debug('web', 'handleTranslationsRequest: user not site administrator.'); res.sendStatus(401); return; }
4164 }
4165
4166 var data = '';
4167 req.setEncoding('utf8');
4168 req.on('data', function (chunk) { data += chunk; });
4169 req.on('end', function () {
4170 try { data = JSON.parse(data); } catch (ex) { data = null; }
4171 if (data == null) { res.sendStatus(404); return; }
4172 if (data.action == 'getTranslations') {
4173 if (obj.fs.existsSync(obj.path.join(obj.parent.datapath, 'translate.json'))) {
4174 // Return the translation file (JSON)
4175 try { res.sendFile(obj.path.join(obj.parent.datapath, 'translate.json')); } catch (ex) { res.sendStatus(404); }
4176 } else if (obj.fs.existsSync(obj.path.join(__dirname, 'translate', 'translate.json'))) {
4177 // Return the default translation file (JSON)
4178 try { res.sendFile(obj.path.join(__dirname, 'translate', 'translate.json')); } catch (ex) { res.sendStatus(404); }
4179 } else { res.sendStatus(404); }
4180 } else if (data.action == 'setTranslations') {
4181 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 })); } });
4182 } else if (data.action == 'translateServer') {
4183 if (obj.pendingTranslation === true) { res.send(JSON.stringify({ response: 'Server is already performing a translation.' })); return; }
4184 const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
4185 if (nodeVersion < 8) { res.send(JSON.stringify({ response: 'Server requires NodeJS 8.x or better.' })); return; }
4186 var translateFile = obj.path.join(obj.parent.datapath, 'translate.json');
4187 if (obj.fs.existsSync(translateFile) == false) { translateFile = obj.path.join(__dirname, 'translate', 'translate.json'); }
4188 if (obj.fs.existsSync(translateFile) == false) { res.send(JSON.stringify({ response: 'Unable to find translate.js file on the server.' })); return; }
4189 res.send(JSON.stringify({ response: 'ok' }));
4190 console.log('Started server translation...');
4191 obj.pendingTranslation = true;
4192 var child = require('child_process').spawn(process.argv[0],['translate.js', 'translateall', translateFile], { timeout: 300000, cwd: obj.path.join(__dirname, 'translate') });
4193 var stdout = '', stderr = '';
4194 child.stdout.on('data', function(d) { stdout += d; });
4195 child.stderr.on('data', function(d) { stderr += d; });
4196 child.on('close', function(error) {
4197 delete obj.pendingTranslation;
4198 if (error) { console.log('Server translation error', error); }
4199 // console.log('stdout', stdout);
4200 if (stderr) { console.log('Server translation stderr', stderr); }
4201 //console.log('Server restart...'); // Perform a server restart
4202 //process.exit(0);
4203 console.log('Server translation completed.');
4204 stdout = null, stderr = null;
4205 });
4206 } else {
4207 // Unknown request
4208 res.sendStatus(404);
4209 }
4210 });
4211 }
4212
4213 // Handle welcome image request
4214 function handleWelcomeImageRequest(req, res) {
4215 const domain = checkUserIpAddress(req, res);
4216 if (domain == null) { return; }
4217
4218 //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
4219 if (domain.welcomepicture) {
4220 if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.welcomepicture] != null)) {
4221 // Use the welcome image in the database
4222 res.set({ 'Content-Type': domain.welcomepicture.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
4223 res.send(parent.configurationFiles[domain.welcomepicture]);
4224 return;
4225 }
4226
4227 // Use the configured logo picture
4228 try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.welcomepicture)); return; } catch (ex) { }
4229 }
4230
4231 var imagefile = 'images/mainwelcome.jpg';
4232 if (domain.sitestyle >= 2) { imagefile = 'images/login/back.png'; }
4233 if (domain.webpublicpath != null) {
4234 obj.fs.exists(obj.path.join(domain.webpublicpath, imagefile), function (exists) {
4235 if (exists) {
4236 // Use the domain logo picture
4237 try { res.sendFile(obj.path.join(domain.webpublicpath, imagefile)); } catch (ex) { res.sendStatus(404); }
4238 } else {
4239 // Use the default logo picture
4240 try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
4241 }
4242 });
4243 } else if (parent.webPublicOverridePath) {
4244 obj.fs.exists(obj.path.join(obj.parent.webPublicOverridePath, imagefile), function (exists) {
4245 if (exists) {
4246 // Use the override logo picture
4247 try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, imagefile)); } catch (ex) { res.sendStatus(404); }
4248 } else {
4249 // Use the default logo picture
4250 try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
4251 }
4252 });
4253 } else {
4254 // Use the default logo picture
4255 try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
4256 }
4257 }
4258
4259 // Download a session recording
4260 function handleGetRecordings(req, res) {
4261 const domain = checkUserIpAddress(req, res);
4262 if (domain == null) return;
4263
4264 // Check the query
4265 if ((domain.sessionrecording == null) || (req.query.file == null) || (obj.common.IsFilenameValid(req.query.file) !== true) || (!req.query.file.endsWith('.mcrec') && !req.query.file.endsWith('.txt'))) { res.sendStatus(401); return; }
4266
4267 // Get the recording path
4268 var recordingsPath = null;
4269 if (domain.sessionrecording.filepath) { recordingsPath = domain.sessionrecording.filepath; } else { recordingsPath = parent.recordpath; }
4270 if (recordingsPath == null) { res.sendStatus(401); return; }
4271
4272 // Get the user and check user rights
4273 var authUserid = null;
4274 if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
4275 if (authUserid == null) { res.sendStatus(401); return; }
4276 const user = obj.users[authUserid];
4277 if (user == null) { res.sendStatus(401); return; }
4278 if ((user.siteadmin & 512) == 0) { res.sendStatus(401); return; } // Check if we have right to get recordings
4279
4280 // Send the recorded file
4281 setContentDispositionHeader(res, 'application/octet-stream', req.query.file, null, 'recording.mcrec');
4282 try { res.sendFile(obj.path.join(recordingsPath, req.query.file)); } catch (ex) { res.sendStatus(404); }
4283 }
4284
4285 // Stream a session recording
4286 function handleGetRecordingsWebSocket(ws, req) {
4287 var domain = checkAgentIpAddress(ws, req);
4288 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; }
4289
4290 // Check the query
4291 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; }
4292
4293 // Get the recording path
4294 var recordingsPath = null;
4295 if (domain.sessionrecording.filepath) { recordingsPath = domain.sessionrecording.filepath; } else { recordingsPath = parent.recordpath; }
4296 if (recordingsPath == null) { try { ws.close(); } catch (ex) { } return; }
4297
4298 // Get the user and check user rights
4299 var authUserid = null;
4300 if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
4301 if (authUserid == null) { try { ws.close(); } catch (ex) { } return; }
4302 const user = obj.users[authUserid];
4303 if (user == null) { try { ws.close(); } catch (ex) { } return; }
4304 if ((user.siteadmin & 512) == 0) { try { ws.close(); } catch (ex) { } return; } // Check if we have right to get recordings
4305 const filefullpath = obj.path.join(recordingsPath, req.query.file);
4306
4307 obj.fs.stat(filefullpath, function (err, stats) {
4308 if (err) {
4309 try { ws.close(); } catch (ex) { } // File does not exist
4310 } else {
4311 obj.fs.open(filefullpath, 'r', function (err, fd) {
4312 if (err == null) {
4313 // When data is received from the web socket
4314 ws.on('message', function (msg) {
4315 if (typeof msg != 'string') return;
4316 var command;
4317 try { command = JSON.parse(msg); } catch (e) { return; }
4318 if ((command == null) || (typeof command.action != 'string')) return;
4319 switch (command.action) {
4320 case 'get': {
4321 const buffer = Buffer.alloc(8 + command.size);
4322 //buffer.writeUInt32BE((command.ptr >> 32), 0);
4323 buffer.writeUInt32BE((command.ptr & 0xFFFFFFFF), 4);
4324 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); });
4325 break;
4326 }
4327 }
4328 });
4329
4330 // If error, do nothing
4331 ws.on('error', function (err) { try { ws.close(); } catch (ex) { } obj.fs.close(fd, function (err) { }); });
4332
4333 // If the web socket is closed
4334 ws.on('close', function (req) { try { ws.close(); } catch (ex) { } obj.fs.close(fd, function (err) { }); });
4335
4336 ws.send(JSON.stringify({ "action": "info", "name": req.query.file, "size": stats.size }));
4337 } else {
4338 try { ws.close(); } catch (ex) { }
4339 }
4340 });
4341 }
4342 });
4343 }
4344
4345 // Serve the player page
4346 function handlePlayerRequest(req, res) {
4347 const domain = checkUserIpAddress(req, res);
4348 if (domain == null) { return; }
4349
4350 parent.debug('web', 'handlePlayerRequest: sending player');
4351 res.set({ 'Cache-Control': 'no-store' });
4352 render(req, res, getRenderPage('player', req, domain), getRenderArgs({}, req, domain));
4353 }
4354
4355 // Serve the guest sharing page
4356 function handleSharingRequest(req, res) {
4357 const domain = getDomain(req, res);
4358 if (domain == null) { return; }
4359 if (req.query.c == null) { res.sendStatus(404); return; }
4360 if (domain.guestdevicesharing === false) { res.sendStatus(404); return; } // This feature is not allowed.
4361
4362 // Check the inbound guest sharing cookie
4363 var c = obj.parent.decodeCookie(req.query.c, obj.parent.invitationLinkEncryptionKey, 9999999999); // Decode cookies with unlimited time.
4364 if (c == null) { res.sendStatus(404); return; }
4365
4366 if (c.a === 5) {
4367 // This is the older style sharing cookie with everything encoded within it.
4368 // This cookie style gives a very large URL, so it's not used anymore.
4369 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; }
4370 handleSharingRequestEx(req, res, domain, c);
4371 return;
4372 }
4373 if (c.a === 6) {
4374 // This is the new style sharing cookie, just encodes the pointer to the sharing information in the database.
4375 // Gives a much more compact URL.
4376 if (typeof c.pid != 'string') { res.sendStatus(404); return; }
4377
4378 // Check the expired time, expire message.
4379 if ((c.e != null) && (c.e <= Date.now())) { res.status(404); 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; }
4380
4381 obj.db.Get('deviceshare-' + c.pid, function (err, docs) {
4382 if ((err != null) || (docs == null) || (docs.length != 1)) { res.sendStatus(404); return; }
4383 const doc = docs[0];
4384
4385 // If this is a recurrent share, check if we are at the correct time to make use of it
4386 if (typeof doc.recurring == 'number') {
4387 const now = Date.now();
4388 if (now >= doc.startTime) { // We don't want to move the validity window before the start time
4389 const deltaTime = (now - doc.startTime);
4390 if (doc.recurring === 1) {
4391 // This moves the start time to the next valid daily window
4392 const oneDay = (24 * 60 * 60 * 1000);
4393 var addition = Math.floor(deltaTime / oneDay);
4394 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.
4395 doc.startTime += (addition * oneDay);
4396 } else if (doc.recurring === 2) {
4397 // This moves the start time to the next valid weekly window
4398 const oneWeek = (7 * 24 * 60 * 60 * 1000);
4399 var addition = Math.floor(deltaTime / oneWeek);
4400 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.
4401 doc.startTime += (addition * oneWeek);
4402 }
4403 }
4404 }
4405
4406 // Generate an old style cookie from the information in the database
4407 var cookie = { a: 5, p: doc.p, gn: doc.guestName, nid: doc.nodeid, cf: doc.consent, pid: doc.publicid, k: doc.extrakey ? doc.extrakey : null, port: doc.port };
4408 if (doc.userid) { cookie.uid = doc.userid; }
4409 if ((cookie.userid == null) && (cookie.pid.startsWith('AS:node/'))) { cookie.nouser = 1; }
4410 if (doc.startTime != null) {
4411 if (doc.expireTime != null) { cookie.start = doc.startTime; cookie.expire = doc.expireTime; }
4412 else if (doc.duration != null) { cookie.start = doc.startTime; cookie.expire = doc.startTime + (doc.duration * 60000); }
4413 }
4414 if (doc.viewOnly === true) { cookie.vo = 1; }
4415 handleSharingRequestEx(req, res, domain, cookie);
4416 });
4417 return;
4418 }
4419 res.sendStatus(404); return;
4420 }
4421
4422 // Serve the guest sharing page
4423 function handleSharingRequestEx(req, res, domain, c) {
4424 // Check the expired time, expire message.
4425 if ((c.expire != null) && (c.expire <= Date.now())) { res.status(404); 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; }
4426
4427 // Check the public id
4428 obj.db.GetAllTypeNodeFiltered([c.nid], domain.id, 'deviceshare', null, function (err, docs) {
4429 // Check if any sharing links are present, expire message.
4430 if ((err != null) || (docs.length == 0)) { res.status(404); 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; }
4431
4432 // Search for the device share public identifier, expire message.
4433 var found = false;
4434 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; } }
4435 if (found == false) { res.status(404); 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; }
4436
4437 // Get information about this node
4438 obj.db.Get(c.nid, function (err, nodes) {
4439 if ((err != null) || (nodes == null) || (nodes.length != 1)) { res.sendStatus(404); return; }
4440 var node = nodes[0];
4441
4442 // Check the start time, not yet valid message.
4443 if ((c.start != null) && (c.expire != null) && ((c.start > Date.now()) || (c.start > c.expire))) { res.status(404); 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; }
4444
4445 // If this is a web relay share, check if this feature is active
4446 if ((c.p == 8) || (c.p == 16)) {
4447 // This is a HTTP or HTTPS share
4448 var webRelayPort = ((args.relaydns != null) ? ((typeof args.aliasport == 'number') ? args.aliasport : args.port) : ((parent.webrelayserver != null) ? ((typeof args.relayaliasport == 'number') ? args.relayaliasport : parent.webrelayserver.port) : 0));
4449 if (webRelayPort == 0) { res.sendStatus(404); return; }
4450
4451 // Create the authentication cookie
4452 const authCookieData = { userid: c.uid, domainid: domain.id, nid: c.nid, ip: req.clientIp, p: c.p, gn: c.gn, r: 8, expire: c.expire, pid: c.pid, port: c.port };
4453 if ((authCookieData.userid == null) && (authCookieData.pid.startsWith('AS:node/'))) { authCookieData.nouser = 1; }
4454 const authCookie = obj.parent.encodeCookie(authCookieData, obj.parent.loginCookieEncryptionKey);
4455
4456 // Redirect to a URL
4457 var webRelayDns = (args.relaydns != null) ? args.relaydns[0] : obj.getWebServerName(domain, req);
4458 var url = 'https://' + webRelayDns + ':' + webRelayPort + '/control-redirect.ashx?n=' + c.nid + '&p=' + c.port + '&appid=' + c.p + '&c=' + authCookie;
4459 if (c.addr != null) { url += '&addr=' + c.addr; }
4460 if (c.pid != null) { url += '&relayid=' + c.pid; }
4461 parent.debug('web', 'handleSharingRequest: Redirecting guest to HTTP relay page for \"' + c.uid + '\", guest \"' + c.gn + '\".');
4462 res.redirect(url);
4463 } else {
4464 // Looks good, let's create the outbound session cookies.
4465 // This is a desktop, terminal or files share. We need to display the sharing page.
4466 // Consent flags are 1 = Notify, 8 = Prompt, 64 = Privacy Bar.
4467 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 };
4468 if ((authCookieData.userid == null) && (authCookieData.pid.startsWith('AS:node/'))) { authCookieData.nouser = 1; }
4469 if (c.k != null) { authCookieData.k = c.k; }
4470 const authCookie = obj.parent.encodeCookie(authCookieData, obj.parent.loginCookieEncryptionKey);
4471
4472 // Server features
4473 var features2 = 0;
4474 if (obj.args.allowhighqualitydesktop !== false) { features2 += 1; } // Enable AllowHighQualityDesktop (Default true)
4475
4476 // Lets respond by sending out the desktop viewer.
4477 var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
4478 parent.debug('web', 'handleSharingRequest: Sending guest sharing page for \"' + c.uid + '\", guest \"' + c.gn + '\".');
4479 res.set({ 'Cache-Control': 'no-store' });
4480 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, req), 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));
4481 }
4482 });
4483 });
4484 }
4485
4486 // Handle domain redirection
4487 obj.handleDomainRedirect = function (req, res) {
4488 const domain = checkUserIpAddress(req, res);
4489 if (domain == null) { return; }
4490 if (domain.redirects == null) { res.sendStatus(404); return; }
4491 var urlArgs = '', urlName = null, splitUrl = req.originalUrl.split('?');
4492 if (splitUrl.length > 1) { urlArgs = '?' + splitUrl[1]; }
4493 if ((splitUrl.length > 0) && (splitUrl[0].length > 1)) { urlName = splitUrl[0].substring(1).toLowerCase(); }
4494 if ((urlName == null) || (domain.redirects[urlName] == null) || (urlName[0] == '_')) { res.sendStatus(404); return; }
4495 if (domain.redirects[urlName] == '~showversion') {
4496 // Show the current version
4497 res.end('MeshCentral v' + obj.parent.currentVer);
4498 } else {
4499 // Perform redirection
4500 res.redirect(domain.redirects[urlName] + urlArgs + getQueryPortion(req));
4501 }
4502 }
4503
4504 // Take a "user/domain/userid/path/file" format and return the actual server disk file path if access is allowed
4505 obj.getServerFilePath = function (user, domain, path) {
4506 var splitpath = path.split('/'), serverpath = obj.path.join(obj.filespath, 'domain'), filename = '';
4507 if ((splitpath.length < 3) || (splitpath[0] != 'user' && splitpath[0] != 'mesh') || (splitpath[1] != domain.id)) return null; // Basic validation
4508 var objid = splitpath[0] + '/' + splitpath[1] + '/' + splitpath[2];
4509 if (splitpath[0] == 'user' && (objid != user._id)) return null; // User validation, only self allowed
4510 if (splitpath[0] == 'mesh') { if ((obj.GetMeshRights(user, objid) & 32) == 0) { return null; } } // Check mesh server file rights
4511 if (splitpath[1] != '') { serverpath += '-' + splitpath[1]; } // Add the domain if needed
4512 serverpath += ('/' + splitpath[0] + '-' + splitpath[2]);
4513 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
4514 return { fullpath: obj.path.resolve(obj.filespath, serverpath), path: serverpath, name: filename, quota: obj.getQuota(objid, domain) };
4515 };
4516
4517 // Return the maximum number of bytes allowed in the user account "My Files".
4518 obj.getQuota = function (objid, domain) {
4519 if (objid == null) return 0;
4520 if (objid.startsWith('user/')) {
4521 var user = obj.users[objid];
4522 if (user == null) return 0;
4523 if (user.siteadmin == 0xFFFFFFFF) return null; // Administrators have no user limit
4524 if ((user.quota != null) && (typeof user.quota == 'number')) { return user.quota; }
4525 if ((domain != null) && (domain.userquota != null) && (typeof domain.userquota == 'number')) { return domain.userquota; }
4526 return null; // By default, the user will have no limit
4527 } else if (objid.startsWith('mesh/')) {
4528 var mesh = obj.meshes[objid];
4529 if (mesh == null) return 0;
4530 if ((mesh.quota != null) && (typeof mesh.quota == 'number')) { return mesh.quota; }
4531 if ((domain != null) && (domain.meshquota != null) && (typeof domain.meshquota == 'number')) { return domain.meshquota; }
4532 return null; // By default, the mesh will have no limit
4533 }
4534 return 0;
4535 };
4536
4537 // Download a file from the server
4538 function handleDownloadFile(req, res) {
4539 const domain = checkUserIpAddress(req, res);
4540 if (domain == null) { return; }
4541 if ((req.query.link == null) || (req.session == null) || (req.session.userid == null) || (domain == null) || (domain.userQuota == -1)) { res.sendStatus(404); return; }
4542 const user = obj.users[req.session.userid];
4543 if (user == null) { res.sendStatus(404); return; }
4544 const file = obj.getServerFilePath(user, domain, req.query.link);
4545 if (file == null) { res.sendStatus(404); return; }
4546 setContentDispositionHeader(res, 'application/octet-stream', file.name, null, 'file.bin');
4547 obj.fs.exists(file.fullpath, function (exists) { if (exists == true) { res.sendFile(file.fullpath); } else { res.sendStatus(404); } });
4548 }
4549
4550 // Download the MeshCommander web page
4551 function handleMeshCommander(req, res) {
4552 const domain = checkUserIpAddress(req, res);
4553 if (domain == null) { return; }
4554 if ((req.session == null) || (req.session.userid == null)) { res.sendStatus(404); return; }
4555
4556 // Find the correct MeshCommander language to send
4557 const acceptableLanguages = obj.getLanguageCodes(req);
4558 const commandLanguageTranslations = { 'en': '', 'de': '-de', 'es': '-es', 'fr': '-fr', 'it': '-it', 'ja': '-ja', 'ko': '-ko', 'nl': '-nl', 'pt': '-pt', 'ru': '-ru', 'zh-chs': '-zh-chs', 'zh-cht': '-zh-chs' };
4559 for (var i in acceptableLanguages) {
4560 const meshCommanderLanguage = commandLanguageTranslations[acceptableLanguages[i]];
4561 if (meshCommanderLanguage != null) {
4562 try { res.sendFile(obj.parent.path.join(parent.webPublicPath, 'commander' + meshCommanderLanguage + '.htm')); } catch (ex) { }
4563 return;
4564 }
4565 }
4566
4567 // Send out the default english MeshCommander
4568 try { res.sendFile(obj.parent.path.join(parent.webPublicPath, 'commander.htm')); } catch (ex) { }
4569 }
4570
4571 // Upload a MeshCore.js file to the server
4572 function handleUploadMeshCoreFile(req, res) {
4573 const domain = checkUserIpAddress(req, res);
4574 if (domain == null) { return; }
4575 if (domain.id !== '') { res.sendStatus(401); return; }
4576
4577 var authUserid = null;
4578 if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
4579
4580 const multiparty = require('multiparty');
4581 const form = new multiparty.Form();
4582 form.parse(req, function (err, fields, files) {
4583 // If an authentication cookie is embedded in the form, use that.
4584 if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
4585 var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
4586 if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
4587 if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
4588 }
4589 if (authUserid == null) { res.sendStatus(401); return; }
4590 if ((fields == null) || (fields.attrib == null) || (fields.attrib.length != 1)) { res.sendStatus(404); return; }
4591
4592 // Get the user
4593 const user = obj.users[authUserid];
4594 if (user == null) { res.sendStatus(401); return; } // Check this user exists
4595
4596 // Get the node and check node rights
4597 const nodeid = fields.attrib[0];
4598 obj.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
4599 if ((node == null) || (rights != 0xFFFFFFFF) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device
4600 for (var i in files.files) {
4601 var file = files.files[i];
4602 const uploadTempPath = resolveSafeUploadTempPath(file.path);
4603 if (uploadTempPath == null) { res.sendStatus(400); return; }
4604 obj.fs.readFile(uploadTempPath, 'utf8', function (err, data) {
4605 if (err != null) return;
4606 data = obj.common.IntToStr(0) + data; // Add the 4 bytes encoding type & flags (Set to 0 for raw)
4607 obj.sendMeshAgentCore(user, domain, fields.attrib[0], 'custom', data); // Upload the core
4608 try { obj.fs.unlinkSync(uploadTempPath); } catch (e) { }
4609 });
4610 }
4611 res.send('');
4612 });
4613 });
4614 }
4615
4616 // Upload a MeshCore.js file to the server
4617 function handleOneClickRecoveryFile(req, res) {
4618 const domain = checkUserIpAddress(req, res);
4619 if (domain == null) { return; }
4620 if (domain.id !== '') { res.sendStatus(401); return; }
4621
4622 var authUserid = null;
4623 if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
4624
4625 const multiparty = require('multiparty');
4626 const form = new multiparty.Form();
4627 form.parse(req, function (err, fields, files) {
4628 // If an authentication cookie is embedded in the form, use that.
4629 if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
4630 var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
4631 if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
4632 if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
4633 }
4634 if (authUserid == null) { res.sendStatus(401); return; }
4635 if ((fields == null) || (fields.attrib == null) || (fields.attrib.length != 1)) { res.sendStatus(404); return; }
4636
4637 // Get the user
4638 const user = obj.users[authUserid];
4639 if (user == null) { res.sendStatus(401); return; } // Check this user exists
4640
4641 // Get the node and check node rights
4642 const nodeid = fields.attrib[0];
4643 obj.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
4644 if ((node == null) || (rights != 0xFFFFFFFF) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device
4645 for (var i in files.files) {
4646 var file = files.files[i];
4647 const uploadTempPath = resolveSafeUploadTempPath(file.path);
4648 if (uploadTempPath == null) { res.sendStatus(400); return; }
4649
4650 // Event Intel AMT One Click Recovery, this will cause Intel AMT wake operations on this and other servers.
4651 parent.DispatchEvent('*', obj, { action: 'oneclickrecovery', userid: user._id, username: user.name, nodeids: [node._id], domain: domain.id, nolog: 1, file: uploadTempPath });
4652
4653 //try { obj.fs.unlinkSync(uploadTempPath); } catch (e) { } // TODO: Remove this file after 30 minutes.
4654 }
4655 res.send('');
4656 });
4657 });
4658 }
4659
4660 // Upload a file to the server
4661 function getCustomIconUserKey(user) {
4662 if ((user == null) || (typeof user._id !== 'string') || (user._id.length === 0)) { return null; }
4663 return obj.crypto.createHash('sha256').update(user._id).digest('hex');
4664 }
4665
4666 function getCustomIconUserDir(user) {
4667 const userKey = getCustomIconUserKey(user);
4668 if (userKey == null) { return null; }
4669 return obj.path.join(obj.parent.datapath, 'icons', 'custom', userKey);
4670 }
4671
4672 // Maximum accepted custom icon upload size, in bytes.
4673 const customIconMaxFileSize = 10485760;
4674 // Maximum accepted width or height for uploaded PNG/JPEG sidebar icons, in pixels.
4675 const customIconMaxDimension = 64;
4676 // Image extensions accepted for uploaded custom sidebar icons.
4677 const customIconAllowedExtensions = new Set(['.svg', '.png', '.jpg', '.jpeg']);
4678
4679 /**
4680 * Return the HTTP response MIME type for a stored custom icon filename.
4681 *
4682 * @param {string} iconName Filename or path segment for the stored custom icon.
4683 * @returns {string|null} MIME type for supported icons, or null for unsupported extensions.
4684 */
4685 function getCustomIconMimeType(iconName) {
4686 const lower = iconName.toLowerCase();
4687 if (lower.endsWith('.svg')) { return 'image/svg+xml'; }
4688 if (lower.endsWith('.png')) { return 'image/png'; }
4689 if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) { return 'image/jpeg'; }
4690 return null;
4691 }
4692
4693 /**
4694 * Reject SVG content that can execute script or load external content.
4695 *
4696 * @param {string} svgContent Raw UTF-8 SVG content from the uploaded file.
4697 * @returns {string|null} SVG content safe to store, or null if it is invalid or unsafe.
4698 */
4699 function cleanSvg(svgContent) {
4700 if (typeof svgContent !== 'string') { return null; }
4701 const cleaned = (svgContent.charCodeAt(0) === 0xFEFF) ? svgContent.substring(1) : svgContent;
4702 if (cleaned.search(/<svg[\s>]/i) < 0) { return null; }
4703 if (cleaned.search(/<\s*(script|foreignObject|iframe|object|embed|applet|link|meta)\b/i) >= 0) { return null; }
4704 if (cleaned.search(/\s+on[a-z0-9_-]+\s*=/i) >= 0) { return null; }
4705 if (cleaned.search(/\s+(href|xlink:href|src)\s*=\s*(['"]?)\s*(?!#)/i) >= 0) { return null; }
4706 return cleaned;
4707 }
4708
4709 /**
4710 * Check if a JPEG marker is a Start Of Frame marker that contains image dimensions.
4711 *
4712 * @param {number} marker JPEG marker byte after the 0xFF prefix.
4713 * @returns {boolean} True when the marker segment contains width and height fields.
4714 */
4715 function isJpegStartOfFrameMarker(marker) {
4716 return ((marker >= 0xC0) && (marker <= 0xC3)) || ((marker >= 0xC5) && (marker <= 0xC7)) || ((marker >= 0xC9) && (marker <= 0xCB)) || ((marker >= 0xCD) && (marker <= 0xCF));
4717 }
4718
4719 /**
4720 * Read JPEG dimensions from header bytes without fully decoding the image.
4721 *
4722 * @param {Buffer} data Initial bytes from the uploaded JPEG file.
4723 * @returns {{width:number,height:number}|null} Parsed dimensions, or null if the JPEG header is invalid or incomplete.
4724 */
4725 function getJpegDimensions(data) {
4726 // JPEG files must start with the SOI marker.
4727 if ((data.length < 4) || (data[0] !== 0xFF) || (data[1] !== 0xD8)) { return null; }
4728 // Start scanning after the SOI marker.
4729 var offset = 2;
4730 while (offset + 9 < data.length) {
4731 // Each JPEG segment starts with a marker prefix.
4732 if (data[offset] !== 0xFF) { return null; }
4733 // Skip fill bytes before the marker value.
4734 while ((offset < data.length) && (data[offset] === 0xFF)) { offset++; }
4735 const marker = data[offset++];
4736 // SOI/EOI and restart markers do not carry segment lengths.
4737 if ((marker === 0xD8) || (marker === 0xD9)) { continue; }
4738 if ((marker >= 0xD0) && (marker <= 0xD7)) { continue; }
4739 // Remaining markers should include a two-byte segment length.
4740 if (offset + 2 > data.length) { return null; }
4741 const segmentLength = data.readUInt16BE(offset);
4742 if (segmentLength < 2) { return null; }
4743 if (isJpegStartOfFrameMarker(marker)) {
4744 // SOF payload layout: precision, height, width.
4745 if (offset + 7 > data.length) { return null; }
4746 return { width: data.readUInt16BE(offset + 5), height: data.readUInt16BE(offset + 3) };
4747 }
4748 // Move to the next marker segment.
4749 offset += segmentLength;
4750 }
4751 return null;
4752 }
4753
4754 /**
4755 * Validate the raster image signature and extract dimensions for supported custom icon formats.
4756 *
4757 * @param {Buffer} data Initial bytes from the uploaded icon file.
4758 * @param {string} extension Lowercase extension from the original uploaded filename.
4759 * @returns {{width:number,height:number}|null} Parsed raster dimensions, or null if the signature/type is invalid.
4760 */
4761 function getCustomIconDimensions(data, extension) {
4762 // PNG dimensions are fixed in the IHDR chunk at byte offsets 16 and 20.
4763 if ((extension === '.png') && (data.length >= 24) && (data[0] === 0x89) && (data[1] === 0x50) && (data[2] === 0x4E) && (data[3] === 0x47) && (data[4] === 0x0D) && (data[5] === 0x0A) && (data[6] === 0x1A) && (data[7] === 0x0A)) {
4764 return { width: data.readUInt32BE(16), height: data.readUInt32BE(20) };
4765 }
4766 // JPEG dimensions are stored in the first SOF marker segment.
4767 if (((extension === '.jpg') || (extension === '.jpeg')) && (data.length >= 3) && (data[0] === 0xFF) && (data[1] === 0xD8) && (data[2] === 0xFF)) {
4768 return getJpegDimensions(data);
4769 }
4770 return null;
4771 }
4772
4773 /**
4774 * Enforce custom icon upload policy before moving the temp file into persistent storage.
4775 * SVG files are checked for active content; PNG/JPEG files are signature and dimension checked.
4776 *
4777 * @param {string} iconTempPath Safe resolved path to the uploaded temp file.
4778 * @param {string} extension Lowercase extension from the original uploaded filename.
4779 * @param {function(string|null):void} callback Called with null on success, or a user-safe error message on failure.
4780 */
4781 function validateCustomIconFile(iconTempPath, extension, callback) {
4782 obj.fs.stat(iconTempPath, function (statErr, stats) {
4783 if (statErr) { callback('Unable to read uploaded icon.'); return; }
4784 if ((stats == null) || (stats.isFile() !== true)) { callback('Invalid icon file.'); return; }
4785 // Reject empty and oversized uploads before reading any file content.
4786 if ((stats.size < 4) || (stats.size > customIconMaxFileSize)) { callback('Icon files must be non-empty and ' + (customIconMaxFileSize / 1048576) + ' MB or smaller.'); return; }
4787 if (extension === '.svg') {
4788 obj.fs.readFile(iconTempPath, 'utf8', function (readErr, svgContent) {
4789 if (readErr) { callback('Unable to read uploaded icon.'); return; }
4790 const cleanedSvg = cleanSvg(svgContent);
4791 if (cleanedSvg == null) { callback('Invalid SVG icon file.'); return; }
4792 obj.fs.writeFile(iconTempPath, cleanedSvg, 'utf8', function (writeErr) {
4793 callback(writeErr ? 'Unable to clean uploaded SVG icon.' : null);
4794 });
4795 });
4796 return;
4797 }
4798 obj.fs.open(iconTempPath, 'r', function (openErr, fd) {
4799 if (openErr) { callback('Unable to read uploaded icon.'); return; }
4800 // Reading the first 64 KB is enough for normal PNG headers and JPEG SOF markers.
4801 const header = Buffer.alloc(Math.min(stats.size, 65536));
4802 obj.fs.read(fd, header, 0, header.length, 0, function (readErr, bytesRead) {
4803 obj.fs.close(fd, function () { });
4804 if (readErr) { callback('Unable to read uploaded icon.'); return; }
4805 const dimensions = getCustomIconDimensions(header.slice(0, bytesRead), extension);
4806 if (dimensions == null) { callback('The uploaded icon does not match its file type.'); return; }
4807 if ((dimensions.width < 1) || (dimensions.height < 1) || (dimensions.width > customIconMaxDimension) || (dimensions.height > customIconMaxDimension)) { callback('Icon images must be ' + customIconMaxDimension + ' x ' + customIconMaxDimension + ' pixels or smaller.'); return; }
4808 callback(null);
4809 });
4810 });
4811 });
4812 }
4813
4814 function handleCustomIconUpload(req, res) {
4815 const domain = checkUserIpAddress(req, res);
4816 if (domain == null) { return; }
4817 if ((req.session == null) || (typeof req.session.userid !== 'string')) { res.sendStatus(401); return; }
4818 const user = obj.users[req.session.userid];
4819 if (user == null) { res.sendStatus(401); return; }
4820
4821 const multiparty = require('multiparty');
4822 const form = new multiparty.Form({ maxFilesSize: customIconMaxFileSize });
4823 form.parse(req, function (err, fields, files) {
4824 if (err) { res.status(400).json({ success: false, error: (err.status === 413) ? 'Icon files must be non-empty and ' + (customIconMaxFileSize / 1048576) + ' MB or smaller.' : 'Invalid form submission.' }); return; }
4825
4826 const allowedTypes = { myDevices: 1, myAccount: 1, myEvents: 1, myFiles: 1, myUsers: 1, myServer: 1 };
4827 const iconType = (fields && fields.iconType && fields.iconType[0]) ? fields.iconType[0] : null;
4828 if ((typeof iconType !== 'string') || (allowedTypes[iconType] !== 1)) { res.status(400).json({ success: false, error: 'Invalid icon type.' }); return; }
4829
4830 const iconFile = (files && files.iconFile && files.iconFile[0]) ? files.iconFile[0] : null;
4831 if ((iconFile == null) || (typeof iconFile.path !== 'string')) { res.status(400).json({ success: false, error: 'Missing icon file.' }); return; }
4832 const iconTempPath = resolveSafeUploadTempPath(iconFile.path);
4833 if (iconTempPath == null) { res.status(400).json({ success: false, error: 'Invalid icon file location.' }); return; }
4834
4835 const cleanupTempFile = function () { try { obj.fs.unlink(iconTempPath, function () { }); } catch (ex) { } };
4836
4837 const extension = obj.path.extname(iconFile.originalFilename || '').toLowerCase();
4838 if (customIconAllowedExtensions.has(extension) === false) { cleanupTempFile(); res.status(400).json({ success: false, error: 'Only SVG, PNG and JPEG icon files are supported.' }); return; }
4839
4840 const iconsRoot = obj.path.join(obj.parent.datapath, 'icons');
4841 const customDir = obj.path.join(iconsRoot, 'custom');
4842 const userCustomDir = getCustomIconUserDir(user);
4843 const userKey = getCustomIconUserKey(user);
4844 if ((userCustomDir == null) || (userKey == null)) { cleanupTempFile(); res.status(500).json({ success: false, error: 'Unable to prepare user icons directory.' }); return; }
4845 try { obj.fs.mkdirSync(iconsRoot); } catch (ex) { if (ex.code !== 'EEXIST') { cleanupTempFile(); res.status(500).json({ success: false, error: 'Unable to prepare icons directory.' }); return; } }
4846 try { obj.fs.mkdirSync(customDir); } catch (ex) { if (ex.code !== 'EEXIST') { cleanupTempFile(); res.status(500).json({ success: false, error: 'Unable to prepare icons directory.' }); return; } }
4847 try { obj.fs.mkdirSync(userCustomDir); } catch (ex) { if (ex.code !== 'EEXIST') { cleanupTempFile(); res.status(500).json({ success: false, error: 'Unable to prepare user icons directory.' }); return; } }
4848
4849 const previousIcon = (fields && fields.previousIcon && fields.previousIcon[0]) ? fields.previousIcon[0] : null;
4850 const previousInfo = resolveCustomIconPath(previousIcon, user);
4851
4852 const newFilename = iconType + '-' + Date.now().toString(36) + '-' + Math.random().toString(36).substring(2, 8) + extension;
4853 const destinationPath = obj.path.join(userCustomDir, newFilename);
4854
4855 const respondSuccess = function () {
4856 if ((previousInfo != null) && (previousInfo.isOwned === true)) {
4857 try { obj.fs.unlinkSync(previousInfo.diskPath); } catch (ex) { }
4858 }
4859 res.json({ success: true, path: domain.url + 'icons/custom/' + userKey + '/' + newFilename });
4860 };
4861
4862 validateCustomIconFile(iconTempPath, extension, function (validationError) {
4863 if (validationError != null) { cleanupTempFile(); res.status(400).json({ success: false, error: validationError }); return; }
4864 obj.fs.rename(iconTempPath, destinationPath, function (renameErr) {
4865 if (renameErr == null) { respondSuccess(); return; }
4866 if ((renameErr != null) && (renameErr.code === 'EXDEV')) {
4867 obj.common.copyFile(iconTempPath, destinationPath, function (copyErr) {
4868 cleanupTempFile();
4869 if (copyErr) { res.status(500).json({ success: false, error: 'Failed to save uploaded icon.' }); return; }
4870 respondSuccess();
4871 });
4872 } else {
4873 cleanupTempFile();
4874 res.status(500).json({ success: false, error: 'Failed to save uploaded icon.' });
4875 }
4876 });
4877 });
4878 });
4879 }
4880
4881 function resolveCustomIconPath(requestPath, user) {
4882 if (typeof requestPath !== 'string') { return null; }
4883 if (requestPath.startsWith('http://') || requestPath.startsWith('https://') || requestPath.startsWith('data:')) { return null; }
4884 const pathOnly = requestPath.split('?')[0].split('#')[0];
4885 const marker = '/icons/custom/';
4886 const markerIndex = pathOnly.indexOf(marker);
4887 if (markerIndex < 0) { return null; }
4888 const relativePath = pathOnly.substring(markerIndex + marker.length);
4889 if ((relativePath.length === 0) || (relativePath.indexOf('\\') !== -1)) { return null; }
4890 const pathParts = relativePath.split('/');
4891 if ((pathParts.length !== 1) && (pathParts.length !== 2)) { return null; }
4892 for (var i = 0; i < pathParts.length; i++) {
4893 if ((pathParts[i].length === 0) || (obj.common.IsFilenameValid(pathParts[i]) !== true)) { return null; }
4894 }
4895
4896 var ownerKey = null, iconName = null, diskPath = null, isOwned = false;
4897 const iconsRoot = obj.path.join(obj.parent.datapath, 'icons', 'custom');
4898 if (pathParts.length === 1) {
4899 iconName = pathParts[0];
4900 diskPath = obj.path.join(iconsRoot, iconName);
4901 } else {
4902 ownerKey = pathParts[0];
4903 iconName = pathParts[1];
4904 diskPath = obj.path.join(iconsRoot, ownerKey, iconName);
4905 const currentUserKey = getCustomIconUserKey(user);
4906 isOwned = (currentUserKey != null) && (ownerKey === currentUserKey);
4907 }
4908
4909 const lower = iconName.toLowerCase();
4910 if ((lower.endsWith('.svg') === false) && (lower.endsWith('.png') === false) && (lower.endsWith('.jpg') === false) && (lower.endsWith('.jpeg') === false)) { return null; }
4911 return { ownerKey: ownerKey, iconName: iconName, diskPath: diskPath, isOwned: isOwned, isLegacy: (pathParts.length === 1) };
4912 }
4913
4914 function handleCustomIconDelete(req, res) {
4915 const domain = checkUserIpAddress(req, res);
4916 if (domain == null) { return; }
4917 if ((req.session == null) || (typeof req.session.userid !== 'string')) { res.sendStatus(401); return; }
4918 const user = obj.users[req.session.userid];
4919 if (user == null) { res.sendStatus(401); return; }
4920
4921 const iconPath = (req.body && (typeof req.body.iconPath === 'string')) ? req.body.iconPath : null;
4922 const iconInfo = resolveCustomIconPath(iconPath, user);
4923 if ((iconInfo == null) || (iconInfo.isOwned !== true)) { res.status(400).json({ success: false, error: 'Invalid icon path.' }); return; }
4924
4925 obj.fs.unlink(iconInfo.diskPath, function (err) {
4926 if (err && (err.code !== 'ENOENT')) { res.status(500).json({ success: false, error: 'Failed to delete icon.' }); return; }
4927 res.json({ success: true });
4928 });
4929 }
4930
4931 function handleCustomIconDownload(req, res) {
4932 const domain = getDomain(req);
4933 if (domain == null) { res.sendStatus(404); return; }
4934 if ((req.session == null) || (typeof req.session.userid !== 'string')) { res.sendStatus(401); return; }
4935 const user = obj.users[req.session.userid];
4936 if (user == null) { res.sendStatus(401); return; }
4937
4938 if ((req.params == null) || (typeof req.params[0] !== 'string')) { res.sendStatus(404); return; }
4939 const iconInfo = resolveCustomIconPath('/icons/custom/' + req.params[0], user);
4940 if (iconInfo == null) { res.sendStatus(404); return; }
4941 if ((iconInfo.isLegacy !== true) && (iconInfo.isOwned !== true)) { res.sendStatus(404); return; }
4942 const contentType = getCustomIconMimeType(iconInfo.iconName);
4943 if (contentType == null) { res.sendStatus(404); return; }
4944
4945 obj.fs.readFile(iconInfo.diskPath, function (err, data) {
4946 if (err) { res.sendStatus(404); return; }
4947 const headers = { 'Content-Type': contentType, 'X-Content-Type-Options': 'nosniff' };
4948 if (contentType === 'image/svg+xml') { headers['Content-Security-Policy'] = "default-src 'none'; style-src 'unsafe-inline'; script-src 'none'; object-src 'none'; base-uri 'none'"; }
4949 res.set(headers);
4950 res.send(data);
4951 });
4952 }
4953
4954 function handleUploadFile(req, res) {
4955 const domain = checkUserIpAddress(req, res);
4956 if (domain == null) { return; }
4957 if (domain.userQuota == -1) { res.sendStatus(401); return; }
4958 var authUserid = null;
4959 if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
4960 const multiparty = require('multiparty');
4961 const form = new multiparty.Form();
4962 form.parse(req, function (err, fields, files) {
4963 // If an authentication cookie is embedded in the form, use that.
4964 if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
4965 var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
4966 if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
4967 if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
4968 }
4969 if (authUserid == null) { res.sendStatus(401); return; }
4970
4971 // Get the user
4972 const user = obj.users[authUserid];
4973 if ((user == null) || (user.siteadmin & 8) == 0) { res.sendStatus(401); return; } // Check if we have file rights
4974
4975 if ((fields == null) || (fields.link == null) || (fields.link.length != 1)) { /*console.log('UploadFile, Invalid Fields:', fields, files);*/ console.log('err4'); res.sendStatus(404); return; }
4976 var xfile = null;
4977 try { xfile = obj.getServerFilePath(user, domain, decodeURIComponent(fields.link[0])); } catch (ex) { }
4978 if (xfile == null) { res.sendStatus(404); return; }
4979 // Get total bytes in the path
4980 var totalsize = readTotalFileSize(xfile.fullpath);
4981 if ((xfile.quota == null) || (totalsize < xfile.quota)) { // Check if the quota is not already broken
4982 if (fields.name != null) {
4983
4984 // See if we need to create the folder
4985 var domainx = 'domain';
4986 if (domain.id.length > 0) { domainx = 'domain-' + usersplit[1]; }
4987 try { obj.fs.mkdirSync(obj.parent.filespath); } catch (ex) { }
4988 try { obj.fs.mkdirSync(obj.parent.path.join(obj.parent.filespath, domainx)); } catch (ex) { }
4989 try { obj.fs.mkdirSync(xfile.fullpath); } catch (ex) { }
4990
4991 // Upload method where all the file data is within the fields.
4992 var names = fields.name[0].split('*'), sizes = fields.size[0].split('*'), types = fields.type[0].split('*'), datas = fields.data[0].split('*');
4993 if ((names.length == sizes.length) && (types.length == datas.length) && (names.length == types.length)) {
4994 for (var i = 0; i < names.length; i++) {
4995 var originalName = names[i];
4996 var safeName = obj.path.basename(originalName);
4997 if ((safeName !== originalName) || (obj.common.IsFilenameValid(safeName) == false)) { res.sendStatus(404); return; }
4998 var filedata = Buffer.from(datas[i].split(',')[1], 'base64');
4999 if ((xfile.quota == null) || ((totalsize + filedata.length) < xfile.quota)) { // Check if quota would not be broken if we add this file
5000 // Create the user folder if needed
Showing first 5,000 of 10,886 lines. View raw