EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
webserver.js
Go to the documentation of this file.
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.
17function 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
30if (!String.prototype.startsWith) { String.prototype.startsWith = function (searchString, position) { position = position || 0; return this.substr(position, searchString.length) === searchString; }; }
31if (!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
34module.exports.CreateWebServer = function (parent, db, args, certificates, doneFunc) {
35 var obj = {}, i = 0;
36
37 // Modules
38 obj.fs = require('fs');
39 obj.net = require('net');
40 obj.tls = require('tls');
41 obj.path = require('path');
42 obj.bodyParser = require('body-parser');
43 obj.exphbs = require('express-handlebars');
44 obj.crypto = require('crypto');
45 obj.common = require('./common.js');
46 obj.express = require('express');
47 obj.meshAgentHandler = require('./meshagent.js');
48 obj.meshRelayHandler = require('./meshrelay.js');
49 obj.meshDeviceFileHandler = require('./meshdevicefile.js');
50 obj.meshDesktopMultiplexHandler = require('./meshdesktopmultiplex.js');
51 obj.meshIderHandler = require('./amt/amt-ider.js');
52 obj.meshUserHandler = require('./meshuser.js');
53 obj.interceptor = require('./interceptor');
54 obj.uaparser = require('ua-parser-js');
55 obj.uaclienthints = require('ua-client-hints-js');
56 const constants = (obj.crypto.constants ? obj.crypto.constants : require('constants')); // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead.
57
58 // Setup WebAuthn / FIDO2
59 obj.webauthn = require('./webauthn.js').CreateWebAuthnModule();
60
61 if (process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']) {
62 obj.httpsProxyAgent = new (require('https-proxy-agent').HttpsProxyAgent)(process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']);
63 }
64
65 // Variables
66 obj.args = args;
67 obj.parent = parent;
68 obj.filespath = parent.filespath;
69 obj.db = db;
70 obj.app = obj.express();
71 if (obj.args.agentport) { obj.agentapp = obj.express(); }
72 if (args.compression === true) {
73 obj.app.use(require('compression')({ filter: function (req, res) {
74 if (req.path == '/devicefile.ashx') return false; // Don't compress device file transfers to show file sizes
75 if ((args.relaydns != null) && (obj.args.relaydns.indexOf(req.hostname) >= 0)) return false; // Don't compress DNS relay requests
76 return require('compression').filter(req, res);
77 }}));
78 }
79 obj.app.disable('x-powered-by');
80 obj.tlsServer = null;
81 obj.tcpServer = null;
82 obj.certificates = certificates;
83 obj.users = {}; // UserID --> User
84 obj.meshes = {}; // MeshID --> Mesh (also called device group)
85 obj.userGroups = {}; // UGrpID --> User Group
86 obj.useNodeDefaultTLSCiphers = args.usenodedefaulttlsciphers; // Use TLS ciphers provided by node
87 obj.tlsCiphers = args.tlsciphers; // List of TLS ciphers to use
88 obj.userAllowedIp = args.userallowedip; // List of allowed IP addresses for users
89 obj.agentAllowedIp = args.agentallowedip; // List of allowed IP addresses for agents
90 obj.agentBlockedIp = args.agentblockedip; // List of blocked IP addresses for agents
91 obj.tlsSniCredentials = null;
92 obj.dnsDomains = {};
93 obj.relaySessionCount = 0;
94 obj.relaySessionErrorCount = 0;
95 obj.blockedUsers = 0;
96 obj.blockedAgents = 0;
97 obj.renderPages = null;
98 obj.renderLanguages = [];
99 obj.destroyedSessions = {}; // userid/req.session.x --> destroyed session time
100
101 // Web relay sessions
102 var webRelayNextSessionId = 1;
103 var webRelaySessions = {} // UserId/SessionId/Host --> Web Relay Session
104 var webRelayCleanupTimer = null;
105
106 // Monitor web relay session removals
107 parent.AddEventDispatch(['server-shareremove'], obj);
108 obj.HandleEvent = function (source, event, ids, id) {
109 if (event.action == 'removedDeviceShare') {
110 for (var relaySessionId in webRelaySessions) {
111 // A share was removed that matches an active session, close the web relay session.
112 if (webRelaySessions[relaySessionId].xpublicid === event.publicid) { webRelaySessions[relaySessionId].close(); }
113 }
114 }
115 }
116
117 // Mesh Rights
118 const MESHRIGHT_EDITMESH = 0x00000001;
119 const MESHRIGHT_MANAGEUSERS = 0x00000002;
120 const MESHRIGHT_MANAGECOMPUTERS = 0x00000004;
121 const MESHRIGHT_REMOTECONTROL = 0x00000008;
122 const MESHRIGHT_AGENTCONSOLE = 0x00000010;
123 const MESHRIGHT_SERVERFILES = 0x00000020;
124 const MESHRIGHT_WAKEDEVICE = 0x00000040;
125 const MESHRIGHT_SETNOTES = 0x00000080;
126 const MESHRIGHT_REMOTEVIEWONLY = 0x00000100;
127 const MESHRIGHT_NOTERMINAL = 0x00000200;
128 const MESHRIGHT_NOFILES = 0x00000400;
129 const MESHRIGHT_NOAMT = 0x00000800;
130 const MESHRIGHT_DESKLIMITEDINPUT = 0x00001000;
131 const MESHRIGHT_LIMITEVENTS = 0x00002000;
132 const MESHRIGHT_CHATNOTIFY = 0x00004000;
133 const MESHRIGHT_UNINSTALL = 0x00008000;
134 const MESHRIGHT_NODESKTOP = 0x00010000;
135 const MESHRIGHT_REMOTECOMMAND = 0x00020000;
136 const MESHRIGHT_RESETOFF = 0x00040000;
137 const MESHRIGHT_GUESTSHARING = 0x00080000;
138 const MESHRIGHT_ADMIN = 0xFFFFFFFF;
139
140 // Site rights
141 const SITERIGHT_SERVERBACKUP = 0x00000001;
142 const SITERIGHT_MANAGEUSERS = 0x00000002;
143 const SITERIGHT_SERVERRESTORE = 0x00000004;
144 const SITERIGHT_FILEACCESS = 0x00000008;
145 const SITERIGHT_SERVERUPDATE = 0x00000010;
146 const SITERIGHT_LOCKED = 0x00000020;
147 const SITERIGHT_NONEWGROUPS = 0x00000040;
148 const SITERIGHT_NOMESHCMD = 0x00000080;
149 const SITERIGHT_USERGROUPS = 0x00000100;
150 const SITERIGHT_RECORDINGS = 0x00000200;
151 const SITERIGHT_LOCKSETTINGS = 0x00000400;
152 const SITERIGHT_ALLEVENTS = 0x00000800;
153 const SITERIGHT_NONEWDEVICES = 0x00001000;
154 const SITERIGHT_ADMIN = 0xFFFFFFFF;
155
156 // Setup SSPI authentication if needed
157 if ((obj.parent.platform == 'win32') && (obj.args.nousers != true) && (obj.parent.config != null) && (obj.parent.config.domains != null)) {
158 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 }); } }
159 }
160
161 // Perform hash on web certificate and agent certificate
162 obj.webCertificateHash = parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.web.cert);
163 obj.webCertificateHashs = { '': obj.webCertificateHash };
164 obj.webCertificateHashBase64 = Buffer.from(obj.webCertificateHash, 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
165 obj.webCertificateFullHash = parent.certificateOperations.getCertHashBinary(obj.certificates.web.cert);
166 obj.webCertificateFullHashs = { '': obj.webCertificateFullHash };
167 obj.webCertificateExpire = { '': parent.certificateOperations.getCertificateExpire(parent.certificates.web.cert) };
168 obj.agentCertificateHashHex = parent.certificateOperations.getPublicKeyHash(obj.certificates.agent.cert);
169 obj.agentCertificateHashBase64 = Buffer.from(obj.agentCertificateHashHex, 'hex').toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
170 obj.agentCertificateAsn1 = parent.certificateOperations.forge.asn1.toDer(parent.certificateOperations.forge.pki.certificateToAsn1(parent.certificateOperations.forge.pki.certificateFromPem(parent.certificates.agent.cert))).getBytes();
171 obj.defaultWebCertificateHash = obj.certificates.webdefault ? parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.webdefault.cert) : null;
172 obj.defaultWebCertificateFullHash = obj.certificates.webdefault ? parent.certificateOperations.getCertHashBinary(obj.certificates.webdefault.cert) : null;
173
174 // Compute the hash of all of the web certificates for each domain
175 for (var i in obj.parent.config.domains) {
176 if (obj.parent.config.domains[i].certhash != null) {
177 // If the web certificate hash is provided, use it.
178 obj.webCertificateHashs[i] = obj.webCertificateFullHashs[i] = Buffer.from(obj.parent.config.domains[i].certhash, 'hex').toString('binary');
179 if (obj.parent.config.domains[i].certkeyhash != null) { obj.webCertificateHashs[i] = Buffer.from(obj.parent.config.domains[i].certkeyhash, 'hex').toString('binary'); }
180 delete obj.webCertificateExpire[i]; // Expire time is not provided
181 } else if ((obj.parent.config.domains[i].dns != null) && (obj.parent.config.domains[i].certs != null)) {
182 // If the domain has a different DNS name, use a different certificate hash.
183 // Hash the full certificate
184 obj.webCertificateFullHashs[i] = parent.certificateOperations.getCertHashBinary(obj.parent.config.domains[i].certs.cert);
185 obj.webCertificateExpire[i] = Date.parse(parent.certificateOperations.forge.pki.certificateFromPem(obj.parent.config.domains[i].certs.cert).validity.notAfter);
186 try {
187 // Decode a RSA certificate and hash the public key.
188 obj.webCertificateHashs[i] = parent.certificateOperations.getPublicKeyHashBinary(obj.parent.config.domains[i].certs.cert);
189 } catch (ex) {
190 // This may be a ECDSA certificate, hash the entire cert.
191 obj.webCertificateHashs[i] = obj.webCertificateFullHashs[i];
192 }
193 } else if ((obj.parent.config.domains[i].dns != null) && (obj.certificates.dns[i] != null)) {
194 // If this domain has a DNS and a matching DNS cert, use it. This case works for wildcard certs.
195 obj.webCertificateFullHashs[i] = parent.certificateOperations.getCertHashBinary(obj.certificates.dns[i].cert);
196 obj.webCertificateHashs[i] = parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.dns[i].cert);
197 obj.webCertificateExpire[i] = Date.parse(parent.certificateOperations.forge.pki.certificateFromPem(obj.certificates.dns[i].cert).validity.notAfter);
198 } else if (i != '') {
199 // For any other domain, use the default cert.
200 obj.webCertificateFullHashs[i] = obj.webCertificateFullHashs[''];
201 obj.webCertificateHashs[i] = obj.webCertificateHashs[''];
202 obj.webCertificateExpire[i] = obj.webCertificateExpire[''];
203 }
204 }
205
206 // If we are running the legacy swarm server, compute the hash for that certificate
207 if (parent.certificates.swarmserver != null) {
208 obj.swarmCertificateAsn1 = parent.certificateOperations.forge.asn1.toDer(parent.certificateOperations.forge.pki.certificateToAsn1(parent.certificateOperations.forge.pki.certificateFromPem(parent.certificates.swarmserver.cert))).getBytes();
209 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' });
210 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' });
211 }
212
213 // Main lists
214 obj.wsagents = {}; // NodeId --> Agent
215 obj.wsagentsWithBadWebCerts = {}; // NodeId --> Agent
216 obj.wsagentsDisconnections = {};
217 obj.wsagentsDisconnectionsTimer = null;
218 obj.duplicateAgentsLog = {};
219 obj.wssessions = {}; // UserId --> Array Of Sessions
220 obj.wssessions2 = {}; // "UserId + SessionRnd" --> Session (Note that the SessionId is the UserId + / + SessionRnd)
221 obj.wsPeerSessions = {}; // ServerId --> Array Of "UserId + SessionRnd"
222 obj.wsPeerSessions2 = {}; // "UserId + SessionRnd" --> ServerId
223 obj.wsPeerSessions3 = {}; // ServerId --> UserId --> [ SessionId ]
224 obj.sessionsCount = {}; // Merged session counters, used when doing server peering. UserId --> SessionCount
225 obj.wsrelays = {}; // Id -> Relay
226 obj.desktoprelays = {}; // Id -> Desktop Multiplexer Relay
227 obj.wsPeerRelays = {}; // Id -> { ServerId, Time }
228 var tlsSessionStore = {}; // Store TLS session information for quick resume.
229 var tlsSessionStoreCount = 0; // Number of cached TLS session information in store.
230
231 // Setup randoms
232 obj.crypto.randomBytes(48, function (err, buf) { obj.httpAuthRandom = buf; });
233 obj.crypto.randomBytes(16, function (err, buf) { obj.httpAuthRealm = buf.toString('hex'); });
234 obj.crypto.randomBytes(48, function (err, buf) { obj.relayRandom = buf; });
235
236 // Get non-english web pages and emails
237 getRenderList();
238 getEmailLanguageList();
239
240 // Setup DNS domain TLS SNI credentials
241 {
242 var dnscount = 0;
243 obj.tlsSniCredentials = {};
244 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++; } }
245 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; }
246 }
247 function TlsSniCallback(name, cb) {
248 var c = obj.tlsSniCredentials[name];
249 if (c != null) {
250 cb(null, c);
251 } else {
252 cb(null, obj.tlsSniCredentials['']);
253 }
254 }
255
256 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; }
257 //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; }
258 // Fetch all users from the database, keep this in memory
259 obj.db.GetAllType('user', function (err, docs) {
260 obj.common.unEscapeAllLinksFieldName(docs);
261 var domainUserCount = {}, i = 0;
262 for (i in parent.config.domains) { domainUserCount[i] = 0; }
263 for (i in docs) { var u = obj.users[docs[i]._id] = docs[i]; domainUserCount[u.domain]++; }
264 for (i in parent.config.domains) {
265 if ((parent.config.domains[i].share == null) && (domainUserCount[i] == 0)) {
266 // If newaccounts is set to no new accounts, but no accounts exists, temporarily allow account creation.
267 //if ((parent.config.domains[i].newaccounts === 0) || (parent.config.domains[i].newaccounts === false)) { parent.config.domains[i].newaccounts = 2; }
268 console.log('Server ' + ((i == '') ? '' : (i + ' ')) + 'has no users, next new account will be site administrator.');
269 }
270 }
271
272 // Fetch all device groups (meshes) from the database, keep this in memory
273 // As we load things in memory, we will also be doing some cleaning up.
274 // We will not save any clean up in the database right now, instead it will be saved next time there is a change.
275 obj.db.GetAllType('mesh', function (err, docs) {
276 obj.common.unEscapeAllLinksFieldName(docs);
277 for (var i in docs) { obj.meshes[docs[i]._id] = docs[i]; } // Get all meshes, including deleted ones.
278
279 // Fetch all user groups from the database, keep this in memory
280 obj.db.GetAllType('ugrp', function (err, docs) {
281 obj.common.unEscapeAllLinksFieldName(docs);
282
283 // Perform user group link cleanup
284 for (var i in docs) {
285 const ugrp = docs[i];
286 if (ugrp.links != null) {
287 for (var j in ugrp.links) {
288 if (j.startsWith('user/') && (obj.users[j] == null)) { delete ugrp.links[j]; } // User group has a link to a user that does not exist
289 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
290 }
291 }
292 obj.userGroups[docs[i]._id] = docs[i]; // Get all user groups
293 }
294
295 // Mapping between users and groups
296 for (var ugrpId in obj.userGroups) {
297 const ugrp = obj.userGroups[ugrpId];
298 if (ugrp.links != null) {
299 for (var userId in ugrp.links) {
300 if (userId.startsWith('user/') && (obj.users[userId] != null)) {
301 const user = obj.users[userId];
302 if (user.links == null) { user.links = {}; }
303 if (user.links[ugrpId] == null) {
304 // Adding group link to user
305 user.links[ugrpId] = { rights: ugrp.links[userId].rights || 1 };
306 }
307 }
308 }
309 }
310 }
311
312 // Perform device group link cleanup
313 for (var i in obj.meshes) {
314 const mesh = obj.meshes[i];
315 if (mesh.links != null) {
316 for (var j in mesh.links) {
317 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
318 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
319 }
320 }
321 }
322
323 // Perform user link cleanup
324 for (var i in obj.users) {
325 const user = obj.users[i];
326 if (user.links != null) {
327 for (var j in user.links) {
328 if (j.startsWith('ugrp/') && (obj.userGroups[j] == null)) { delete user.links[j]; } // User has a link to a user group that does not exist
329 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
330 //else if (j.startsWith('node/') && (obj.nodes[j] == null)) { delete user.links[j]; } // TODO
331 }
332 //if (Object.keys(user.links).length == 0) { delete user.links; }
333 }
334 }
335
336 // We loaded the users, device groups and user group state, start the server
337 serverStart();
338 });
339 });
340 });
341
342 // Clean up a device, used before saving it in the database
343 obj.cleanDevice = function (device) {
344 // Check device links, if a link points to an unknown user, remove it.
345 if (device.links != null) {
346 for (var j in device.links) {
347 if ((obj.users[j] == null) && (obj.userGroups[j] == null)) {
348 delete device.links[j];
349 if (Object.keys(device.links).length == 0) { delete device.links; }
350 }
351 }
352 }
353 return device;
354 }
355
356 // Return statistics about this web server
357 obj.getStats = function () {
358 return {
359 users: Object.keys(obj.users).length,
360 meshes: Object.keys(obj.meshes).length,
361 dnsDomains: Object.keys(obj.dnsDomains).length,
362 relaySessionCount: obj.relaySessionCount,
363 relaySessionErrorCount: obj.relaySessionErrorCount,
364 wsagents: Object.keys(obj.wsagents).length,
365 wsagentsDisconnections: Object.keys(obj.wsagentsDisconnections).length,
366 wsagentsDisconnectionsTimer: Object.keys(obj.wsagentsDisconnectionsTimer).length,
367 wssessions: Object.keys(obj.wssessions).length,
368 wssessions2: Object.keys(obj.wssessions2).length,
369 wsPeerSessions: Object.keys(obj.wsPeerSessions).length,
370 wsPeerSessions2: Object.keys(obj.wsPeerSessions2).length,
371 wsPeerSessions3: Object.keys(obj.wsPeerSessions3).length,
372 sessionsCount: Object.keys(obj.sessionsCount).length,
373 wsrelays: Object.keys(obj.wsrelays).length,
374 wsPeerRelays: Object.keys(obj.wsPeerRelays).length,
375 tlsSessionStore: Object.keys(tlsSessionStore).length,
376 blockedUsers: obj.blockedUsers,
377 blockedAgents: obj.blockedAgents
378 };
379 }
380
381 // Agent counters
382 obj.agentStats = {
383 createMeshAgentCount: 0,
384 agentClose: 0,
385 agentBinaryUpdate: 0,
386 agentMeshCoreBinaryUpdate: 0,
387 coreIsStableCount: 0,
388 verifiedAgentConnectionCount: 0,
389 clearingCoreCount: 0,
390 updatingCoreCount: 0,
391 recoveryCoreIsStableCount: 0,
392 meshDoesNotExistCount: 0,
393 invalidPkcsSignatureCount: 0,
394 invalidRsaSignatureCount: 0,
395 invalidJsonCount: 0,
396 unknownAgentActionCount: 0,
397 agentBadWebCertHashCount: 0,
398 agentBadSignature1Count: 0,
399 agentBadSignature2Count: 0,
400 agentMaxSessionHoldCount: 0,
401 invalidDomainMeshCount: 0,
402 invalidMeshTypeCount: 0,
403 invalidDomainMesh2Count: 0,
404 invalidMeshType2Count: 0,
405 duplicateAgentCount: 0,
406 maxDomainDevicesReached: 0,
407 agentInTrouble: 0,
408 agentInBigTrouble: 0
409 }
410 obj.getAgentStats = function () { return obj.agentStats; }
411
412 // Traffic counters
413 obj.trafficStats = {
414 httpRequestCount: 0,
415 httpWebSocketCount: 0,
416 httpIn: 0,
417 httpOut: 0,
418 relayCount: {},
419 relayIn: {},
420 relayOut: {},
421 localRelayCount: {},
422 localRelayIn: {},
423 localRelayOut: {},
424 AgentCtrlIn: 0,
425 AgentCtrlOut: 0,
426 LMSIn: 0,
427 LMSOut: 0,
428 CIRAIn: 0,
429 CIRAOut: 0
430 }
431 obj.trafficStats.time = Date.now();
432 obj.getTrafficStats = function () { return obj.trafficStats; }
433 obj.getTrafficDelta = function (oldTraffic) { // Return the difference between the old and new data along with the delta time.
434 const data = obj.common.Clone(obj.trafficStats);
435 data.time = Date.now();
436 const delta = calcDelta(oldTraffic ? oldTraffic : {}, data);
437 if (oldTraffic && oldTraffic.time) { delta.delta = (data.time - oldTraffic.time); }
438 delta.time = data.time;
439 return { current: data, delta: delta }
440 }
441 function calcDelta(oldData, newData) { // Recursive function that computes the difference of all numbers
442 const r = {};
443 for (var i in newData) {
444 if (typeof newData[i] == 'object') { r[i] = calcDelta(oldData[i] ? oldData[i] : {}, newData[i]); }
445 if (typeof newData[i] == 'number') { if (typeof oldData[i] == 'number') { r[i] = (newData[i] - oldData[i]); } else { r[i] = newData[i]; } }
446 }
447 return r;
448 }
449
450 // Keep a record of the last agent issues.
451 obj.getAgentIssues = function () { return obj.agentIssues; }
452 obj.setAgentIssue = function (agent, issue) { obj.agentIssues.push([new Date().toLocaleString(), agent.remoteaddrport, issue]); while (obj.setAgentIssue.length > 50) { obj.agentIssues.shift(); } }
453 obj.agentIssues = [];
454
455 // Authenticate the user
456 obj.authenticate = function (name, pass, domain, fn) {
457 if ((typeof (name) != 'string') || (typeof (pass) != 'string') || (typeof (domain) != 'object')) { fn(new Error('invalid fields')); return; }
458 if (name.startsWith('~t:')) {
459 // Login token, try to fetch the token from the database
460 obj.db.Get('logintoken-' + name, function (err, docs) {
461 if (err != null) { fn(err); return; }
462 if ((docs == null) || (docs.length != 1)) { fn(new Error('login token not found')); return; }
463 const loginToken = docs[0];
464 if ((loginToken.expire != 0) && (loginToken.expire < Date.now())) { fn(new Error('login token expired')); return; }
465
466 // Default strong password hashing (pbkdf2 SHA384)
467 require('./pass').hash(pass, loginToken.salt, function (err, hash, tag) {
468 if (err) return fn(err);
469 if (hash == loginToken.hash) {
470 // Login username and password are valid.
471 var user = obj.users[loginToken.userid];
472 if (!user) { fn(new Error('cannot find user')); return; }
473 if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
474
475 // Successful login token authentication
476 var loginOptions = { tokenName: loginToken.name, tokenUser: loginToken.tokenUser };
477 if (loginToken.expire != 0) { loginOptions.expire = loginToken.expire; }
478 return fn(null, user._id, null, loginOptions);
479 }
480 fn(new Error('invalid password'));
481 }, 0);
482 });
483 } else if (domain.auth == 'ldap') {
484 // This method will handle LDAP login
485 const ldapHandler = function ldapHandlerFunc(err, xxuser) {
486 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; }
487
488 // Save this LDAP user to file if needed
489 if (typeof domain.ldapsaveusertofile == 'string') {
490 obj.fs.appendFile(domain.ldapsaveusertofile, JSON.stringify(xxuser) + '\r\n\r\n', function (err) { });
491 }
492
493 // Work on getting the userid for this LDAP user
494 var shortname = null;
495 var username = xxuser['displayName'];
496 if (typeof domain.ldapusername == 'string') {
497 if (domain.ldapusername.indexOf('{{{') >= 0) { username = assembleStringFromObject(domain.ldapusername, xxuser); } else { username = xxuser[domain.ldapusername]; }
498 } else { username = xxuser['displayName'] ? xxuser['displayName'] : xxuser['name']; }
499 if (domain.ldapuserbinarykey) {
500 // Use a binary key as the userid
501 if (xxuser[domain.ldapuserbinarykey]) { shortname = Buffer.from(xxuser[domain.ldapuserbinarykey], 'binary').toString('hex').toLowerCase(); }
502 } else if (domain.ldapuserkey) {
503 // Use a string key as the userid
504 if (xxuser[domain.ldapuserkey]) { shortname = xxuser[domain.ldapuserkey]; }
505 } else {
506 // Use the default key as the userid
507 if (xxuser['objectSid']) { shortname = Buffer.from(xxuser['objectSid'], 'binary').toString('hex').toLowerCase(); }
508 else if (xxuser['objectGUID']) { shortname = Buffer.from(xxuser['objectGUID'], 'binary').toString('hex').toLowerCase(); }
509 else if (xxuser['name']) { shortname = xxuser['name']; }
510 else if (xxuser['cn']) { shortname = xxuser['cn']; }
511 }
512 if (shortname == null) { fn(new Error('no user identifier')); if (ldapHandlerFunc.ldapobj) { try { ldapHandlerFunc.ldapobj.close(); } catch (ex) { console.log(ex); } } return; }
513 if (username == null) { username = shortname; }
514 var userid = 'user/' + domain.id + '/' + shortname;
515
516 // Get the list of groups this user is a member of.
517 var userMemberships = xxuser[(typeof domain.ldapusergroups == 'string') ? domain.ldapusergroups : 'memberOf'];
518 if (typeof userMemberships == 'string') { userMemberships = [userMemberships]; }
519 if (Array.isArray(userMemberships) == false) { userMemberships = []; }
520
521 // See if the user is required to be part of an LDAP user group in order to log into this server.
522 if (typeof domain.ldapuserrequiredgroupmembership == 'string') { domain.ldapuserrequiredgroupmembership = [domain.ldapuserrequiredgroupmembership]; }
523 if (Array.isArray(domain.ldapuserrequiredgroupmembership)) {
524 // Look for a matching LDAP user group
525 var userMembershipMatch = false;
526 for (var i in domain.ldapuserrequiredgroupmembership) { if (userMemberships.indexOf(domain.ldapuserrequiredgroupmembership[i]) >= 0) { userMembershipMatch = true; } }
527 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
528 }
529
530 // Check if user is in an site administrator group
531 var siteAdminGroup = null;
532 if (typeof domain.ldapsiteadmingroups == 'string') { domain.ldapsiteadmingroups = [domain.ldapsiteadmingroups]; }
533 if (Array.isArray(domain.ldapsiteadmingroups)) {
534 siteAdminGroup = false;
535 for (var i in domain.ldapsiteadmingroups) {
536 if (userMemberships.indexOf(domain.ldapsiteadmingroups[i]) >= 0) { siteAdminGroup = domain.ldapsiteadmingroups[i]; }
537 }
538 }
539
540 // See if we need to sync LDAP user memberships with user groups
541 if (domain.ldapsyncwithusergroups === true) { domain.ldapsyncwithusergroups = {}; }
542 if (typeof domain.ldapsyncwithusergroups == 'object') {
543 // LDAP user memberships sync is enabled, see if there are any filters to apply
544 if (typeof domain.ldapsyncwithusergroups.filter == 'string') { domain.ldapsyncwithusergroups.filter = [domain.ldapsyncwithusergroups.filter]; }
545 if (Array.isArray(domain.ldapsyncwithusergroups.filter)) {
546 const g = [];
547 for (var i in userMemberships) {
548 var match = false;
549 for (var j in domain.ldapsyncwithusergroups.filter) {
550 if (userMemberships[i].indexOf(domain.ldapsyncwithusergroups.filter[j]) >= 0) { match = true; }
551 }
552 if (match) { g.push(userMemberships[i]); }
553 }
554 userMemberships = g;
555 }
556 } else {
557 // LDAP user memberships sync is disabled, sync the user with empty membership
558 userMemberships = [];
559 }
560
561 // Get the email address for this LDAP user
562 var email = null;
563 if (domain.ldapuseremail) { email = xxuser[domain.ldapuseremail]; } else if (xxuser['mail']) { email = xxuser['mail']; } // Use given field name or default
564 if (Array.isArray(email)) { email = email[0]; } // Mail may be multivalued in LDAP in which case, answer is an array. Use the 1st value.
565 if (email) { email = email.toLowerCase(); } // it seems some code elsewhere also lowercase the emailaddress, so let's be consistent.
566
567 // Get the real name for this LDAP user
568 var realname = null;
569 if (typeof domain.ldapuserrealname == 'string') {
570 if (domain.ldapuserrealname.indexOf('{{{') >= 0) { realname = assembleStringFromObject(domain.ldapuserrealname, xxuser); } else { realname = xxuser[domain.ldapuserrealname]; }
571 }
572 else { if (typeof xxuser['name'] == 'string') { realname = xxuser['name']; } }
573
574 // Get the phone number for this LDAP user
575 var phonenumber = null;
576 if (domain.ldapuserphonenumber) { phonenumber = xxuser[domain.ldapuserphonenumber]; }
577 else { if (typeof xxuser['telephoneNumber'] == 'string') { phonenumber = xxuser['telephoneNumber']; } }
578
579 // Work on getting the image of this LDAP user
580 var userimage = null, userImageBuffer = null;
581 if (xxuser._raw) { // Using _raw allows us to get data directly as buffer.
582 if (domain.ldapuserimage && xxuser[domain.ldapuserimage]) { userImageBuffer = xxuser._raw[domain.ldapuserimage]; }
583 else if (xxuser['thumbnailPhoto']) { userImageBuffer = xxuser._raw['thumbnailPhoto']; }
584 else if (xxuser['jpegPhoto']) { userImageBuffer = xxuser._raw['jpegPhoto']; }
585 if (userImageBuffer != null) {
586 if ((userImageBuffer[0] == 0xFF) && (userImageBuffer[1] == 0xD8) && (userImageBuffer[2] == 0xFF) && (userImageBuffer[3] == 0xE0)) { userimage = 'data:image/jpeg;base64,' + userImageBuffer.toString('base64'); }
587 if ((userImageBuffer[0] == 0x89) && (userImageBuffer[1] == 0x50) && (userImageBuffer[2] == 0x4E) && (userImageBuffer[3] == 0x47)) { userimage = 'data:image/png;base64,' + userImageBuffer.toString('base64'); }
588 }
589 }
590
591 // Display user information extracted from LDAP data
592 parent.authLog('ldapHandler', 'LDAP user login, id: ' + shortname + ', username: ' + username + ', email: ' + email + ', realname: ' + realname + ', phone: ' + phonenumber + ', image: ' + (userimage != null));
593
594 // If there is a testing userid, use that
595 if (ldapHandlerFunc.ldapShortName) {
596 shortname = ldapHandlerFunc.ldapShortName;
597 userid = 'user/' + domain.id + '/' + shortname;
598 }
599
600 // Save the user image
601 if (userimage != null) { parent.db.Set({ _id: 'im' + userid, image: userimage }); } else { db.Remove('im' + userid); }
602
603 // Close the LDAP object
604 if (ldapHandlerFunc.ldapobj) { try { ldapHandlerFunc.ldapobj.close(); } catch (ex) { console.log(ex); } }
605
606 // Check if the user already exists
607 var user = obj.users[userid];
608 if (user == null) {
609 // This user does not exist, create a new account.
610 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 };
611 if (email) { user['email'] = email; user['emailVerified'] = true; }
612 if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; }
613 if (obj.common.validateStrArray(domain.newaccountrealms)) { user.groups = domain.newaccountrealms; }
614 var usercount = 0;
615 for (var i in obj.users) { if (obj.users[i].domain == domain.id) { usercount++; } }
616 if (usercount == 0) { user.siteadmin = 4294967295; /*if (domain.newaccounts === 2) { delete domain.newaccounts; }*/ } // If this is the first user, give the account site admin.
617
618 // Auto-join any user groups
619 if (typeof domain.newaccountsusergroups == 'object') {
620 for (var i in domain.newaccountsusergroups) {
621 var ugrpid = domain.newaccountsusergroups[i];
622 if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
623 var ugroup = obj.userGroups[ugrpid];
624 if (ugroup != null) {
625 // Add group to the user
626 if (user.links == null) { user.links = {}; }
627 user.links[ugroup._id] = { rights: 1 };
628
629 // Add user to the group
630 ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
631 db.Set(ugroup);
632
633 // Notify user group change
634 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 };
635 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.
636 parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
637 }
638 }
639 }
640
641 // Check the user real name
642 if (realname) { user.realname = realname; }
643
644 // Check the user phone number
645 if (phonenumber) { user.phone = phonenumber; }
646
647 // Indicate that this user has a image
648 if (userimage != null) { user.flags = 1; }
649
650 // See if the user is a member of the site admin group.
651 if (typeof siteAdminGroup === 'string') {
652 parent.authLog('ldapHandler', `LDAP: Granting site admin privilages to new user "${user.name}" found in admin group: ${siteAdminGroup}`);
653 user.siteadmin = 0xFFFFFFFF;
654 }
655
656 // Sync the user with LDAP matching user groups
657 if (syncExternalUserGroups(domain, user, userMemberships, 'ldap') == true) { userChanged = true; }
658
659 obj.users[user._id] = user;
660 obj.db.SetUser(user);
661 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 };
662 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.
663 obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
664 return fn(null, user._id);
665 } else {
666 var userChanged = false;
667
668 // This is an existing user
669 // If the display username has changes, update it.
670 if (user.name != username) { user.name = username; userChanged = true; }
671
672 // Check if user email has changed
673 if (user.email && !email) { // email unset in ldap => unset
674 delete user.email;
675 delete user.emailVerified;
676 userChanged = true;
677 } else if (user.email != email) { // update email
678 user['email'] = email;
679 user['emailVerified'] = true;
680 userChanged = true;
681 }
682
683 // Check the user real name
684 if (realname != user.realname) { user.realname = realname; userChanged = true; }
685
686 // Check the user phone number
687 if (phonenumber != user.phone) { user.phone = phonenumber; userChanged = true; }
688
689 // Check the user image flag
690 if ((userimage != null) && ((user.flags == null) || ((user.flags & 1) == 0))) { if (user.flags == null) { user.flags = 1; } else { user.flags += 1; } userChanged = true; }
691 if ((userimage == null) && (user.flags != null) && ((user.flags & 1) != 0)) { if (user.flags == 1) { delete user.flags; } else { user.flags -= 1; } userChanged = true; }
692
693 // See if the user is a member of the site admin group.
694 if ((typeof siteAdminGroup === 'string') && (user.siteadmin !== 0xFFFFFFFF)) {
695 parent.authLog('ldapHandler', `LDAP: Granting site admin privilages to user "${user.name}" found in administrator group: ${siteAdminGroup}`);
696 user.siteadmin = 0xFFFFFFFF;
697 userChanged = true;
698 } else if ((siteAdminGroup === false) && (user.siteadmin === 0xFFFFFFFF)) {
699 parent.authLog('ldapHandler', `LDAP: Revoking site admin privilages from user "${user.name}" since they are not found in any administrator groups.`);
700 delete user.siteadmin;
701 userChanged = true;
702 }
703
704 // Synd the user with LDAP matching user groups
705 if (syncExternalUserGroups(domain, user, userMemberships, 'ldap') == true) { userChanged = true; }
706
707 // If the user changed, save the changes to the database here
708 if (userChanged) {
709 obj.db.SetUser(user);
710 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 };
711 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.
712 parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
713 }
714
715 // If user is locker out, block here.
716 if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
717 return fn(null, user._id);
718 }
719 }
720
721 if (domain.ldapoptions.url == 'test') {
722 // Test LDAP login
723 var xxuser = domain.ldapoptions[name.toLowerCase()];
724 if (xxuser == null) { fn(new Error('invalid password')); return; } else {
725 ldapHandler.ldapShortName = name.toLowerCase();
726 if (typeof xxuser == 'string') {
727 // The test LDAP user points to a JSON file where the user information is, load it.
728 ldapHandler(null, require(xxuser));
729 } else {
730 // The test user information is in the config.json, use it.
731 ldapHandler(null, xxuser);
732 }
733 }
734 } else {
735 // LDAP login
736 var LdapAuth = require('ldapauth-fork');
737 if (domain.ldapoptions == null) { domain.ldapoptions = {}; }
738 domain.ldapoptions.includeRaw = true; // This allows us to get data as buffers which is useful for images.
739 var ldap = new LdapAuth(domain.ldapoptions);
740 ldapHandler.ldapobj = ldap;
741 ldap.on('error', function (err) { parent.debug('ldap', 'LDAP OnError: ' + err); try { ldap.close(); } catch (ex) { console.log(ex); } }); // Close the LDAP object
742 ldap.authenticate(name, pass, ldapHandler);
743 }
744 } else {
745 // Regular login
746
747 // If JWT auth module is enabled, try PostgreSQL authentication first
748 if (obj.parent.jwtAuth && typeof obj.parent.jwtAuth.authenticatePassword === 'function') {
749 obj.parent.jwtAuth.authenticatePassword(name, pass, function (meshUser) {
750 if (meshUser) {
751 // PostgreSQL authentication successful
752 console.log('[WebServer] PostgreSQL authentication successful for:', meshUser._id);
753
754 // Store or update user in MeshCentral's user cache
755 obj.users[meshUser._id] = meshUser;
756
757 // Check if user is locked
758 if ((meshUser.siteadmin) && (meshUser.siteadmin != 0xFFFFFFFF) && (meshUser.siteadmin & 32) != 0) {
759 fn('locked');
760 return;
761 }
762
763 // Return successful authentication
764 return fn(null, meshUser._id);
765 } else {
766 // PostgreSQL authentication failed, try MeshCentral local users
767 tryLocalAuthentication();
768 }
769 });
770 return;
771 }
772
773 // No JWT auth or it's not available, try local authentication
774 tryLocalAuthentication();
775
776 function tryLocalAuthentication() {
777 var user = obj.users['user/' + domain.id + '/' + name.toLowerCase()];
778 // Query the db for the given username
779 if (!user) { fn(new Error('cannot find user')); return; }
780 // Apply the same algorithm to the POSTed password, applying the hash against the pass / salt, if there is a match we found the user
781 if (user.salt == null) {
782 fn(new Error('invalid password'));
783 } else {
784 if (user.passtype != null) {
785 // IIS default clear or weak password hashing (SHA-1)
786 require('./pass').iishash(user.passtype, pass, user.salt, function (err, hash) {
787 if (err) return fn(err);
788 if (hash == user.hash) {
789 // Update the password to the stronger format.
790 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);
791 if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
792 return fn(null, user._id);
793 }
794 fn(new Error('invalid password'), null, user.passhint);
795 });
796 } else {
797 // Default strong password hashing (pbkdf2 SHA384)
798 require('./pass').hash(pass, user.salt, function (err, hash, tag) {
799 if (err) return fn(err);
800 if (hash == user.hash) {
801 if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
802 return fn(null, user._id);
803 }
804 fn(new Error('invalid password'), null, user.passhint);
805 }, 0);
806 }
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 handleLogoutRequest(req, res) {
878 const domain = checkUserIpAddress(req, res);
879 if (domain == null) { return; }
880 if (domain.auth == 'sspi') { parent.debug('web', 'handleLogoutRequest: failed checks.'); res.sendStatus(404); return; }
881 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
882
883 // If a HTTP header is required, check new UserRequiredHttpHeader
884 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; } }
885
886 res.set({ 'Cache-Control': 'no-store' });
887 // Destroy the user's session to log them out will be re-created next request
888 var userid = req.session.userid;
889 if (req.session.userid) {
890 var user = obj.users[req.session.userid];
891 if (user != null) {
892 obj.parent.authLog('https', 'User ' + user.name + ' logout from ' + req.clientIp + ' port ' + req.connection.remotePort, { sessionid: req.session.x, useragent: req.headers['user-agent'] });
893 obj.parent.DispatchEvent(['*'], obj, { etype: 'user', userid: user._id, username: user.name, action: 'logout', msgid: 2, msg: 'Account logout', domain: domain.id });
894 }
895 if (req.session.x) { clearDestroyedSessions(); obj.destroyedSessions[req.session.userid + '/' + req.session.x] = Date.now(); } // Destroy this session
896 }
897 req.session = null;
898 parent.debug('web', 'handleLogoutRequest: success.');
899
900 // If this user was logged in using an authentication strategy and there is a logout URL, use it.
901 if ((userid != null) && (domain.authstrategies?.authStrategyFlags != null)) {
902 let logouturl = null;
903 let userStrategy = ((userid.split('/')[2]).split(':')[0]).substring(1);
904 // Setup logout url for oidc
905 if (userStrategy == 'oidc' && domain.authstrategies.oidc != null) {
906 if (typeof domain.authstrategies.oidc.logouturl == 'string') {
907 logouturl = domain.authstrategies.oidc.logouturl;
908 } else if (typeof domain.authstrategies.oidc.issuer.end_session_endpoint == 'string' && typeof domain.authstrategies.oidc.client.post_logout_redirect_uri == 'string') {
909 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;
910 } else if (typeof domain.authstrategies.oidc.issuer.end_session_endpoint == 'string') {
911 logouturl = domain.authstrategies.oidc.issuer.end_session_endpoint;
912 }
913 // Log out all other strategies
914 } else if ((domain.authstrategies[userStrategy] != null) && (typeof domain.authstrategies[userStrategy].logouturl == 'string')) { logouturl = domain.authstrategies[userStrategy].logouturl; }
915 // If custom logout was setup, use it
916 if (logouturl != null) {
917 parent.authLog('handleLogoutRequest', userStrategy.toUpperCase() + ': LOGOUT: ' + logouturl);
918 res.redirect(logouturl);
919 return;
920 }
921 }
922
923 // This is the default logout redirect to the login page
924 if (req.query.key != null) { res.redirect(domain.url + 'login?key=' + encodeURIComponent(req.query.key)); } else { res.redirect(domain.url + 'login'); }
925 }
926
927 // Return an object with 2FA type if 2-step auth can be skipped
928 function checkUserOneTimePasswordSkip(domain, user, req, loginOptions) {
929 if (parent.config.settings.no2factorauth == true) return null;
930
931 // If this login occurred using a login token, no 2FA needed.
932 if ((loginOptions != null) && (typeof loginOptions.tokenName === 'string')) { return { twoFactorType: 'tokenlogin' }; }
933
934 // Check if we can skip 2nd factor auth because of the source IP address
935 if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
936 for (var i in domain.passwordrequirements.skip2factor) { if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) { return { twoFactorType: 'ipaddr' }; } }
937 }
938
939 // Check if a 2nd factor cookie is present
940 if (typeof req.headers.cookie == 'string') {
941 const cookies = req.headers.cookie.split('; ');
942 for (var i in cookies) {
943 if (cookies[i].startsWith('twofactor=')) {
944 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.
945 if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { return { twoFactorType: 'cookie' }; }
946 }
947 }
948 }
949
950 return null;
951 }
952
953 // Return true if this user has 2-step auth active
954 function checkUserOneTimePasswordRequired(domain, user, req, loginOptions) {
955 // If this login occurred using a login token, no 2FA needed.
956 if ((loginOptions != null) && (typeof loginOptions.tokenName === 'string')) { return false; }
957
958 // Check if we can skip 2nd factor auth because of the source IP address
959 if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
960 for (var i in domain.passwordrequirements.skip2factor) { if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) return false; }
961 }
962
963 // Check if a 2nd factor cookie is present
964 if (typeof req.headers.cookie == 'string') {
965 const cookies = req.headers.cookie.split('; ');
966 for (var i in cookies) {
967 if (cookies[i].startsWith('twofactor=')) {
968 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.
969 if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { return false; }
970 }
971 }
972 }
973
974 // See if SMS 2FA is available
975 var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
976
977 // See if Messenger 2FA is available
978 var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
979
980 // Check if a 2nd factor is present
981 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))));
982 }
983
984 // Check the 2-step auth token
985 function checkUserOneTimePassword(req, domain, user, token, hwtoken, func) {
986 parent.debug('web', 'checkUserOneTimePassword()');
987 const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.nousers !== true) && (parent.config.settings.no2factorauth !== true));
988 if (twoStepLoginSupported == false) { parent.debug('web', 'checkUserOneTimePassword: not supported.'); func(true); return; };
989
990 // Check if we can use OTP tokens with email
991 var otpemail = (domain.mailserver != null);
992 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; }
993 var otpsms = (parent.smsserver != null);
994 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; }
995 var otpmsg = ((parent.msgserver != null) && (parent.msgserver.providers != 0));
996 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.msg2factor == false)) { otpmsg = false; }
997
998 // Check 2FA login cookie
999 if ((token != null) && (token.startsWith('cookie='))) {
1000 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.
1001 if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { func(true, { twoFactorType: 'cookie' }); return; }
1002 }
1003
1004 // Check email key
1005 if ((otpemail) && (user.otpekey != null) && (user.otpekey.d != null) && (user.otpekey.k === token)) {
1006 var deltaTime = (Date.now() - user.otpekey.d);
1007 if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the email token (10000 * 60 * 5).
1008 user.otpekey = {};
1009 obj.db.SetUser(user);
1010 parent.debug('web', 'checkUserOneTimePassword: success (email).');
1011 func(true, { twoFactorType: 'email' });
1012 return;
1013 }
1014 }
1015
1016 // Check SMS key
1017 if ((otpsms) && (user.phone != null) && (user.otpsms != null) && (user.otpsms.d != null) && (user.otpsms.k === token)) {
1018 var deltaTime = (Date.now() - user.otpsms.d);
1019 if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the SMS token (10000 * 60 * 5).
1020 delete user.otpsms;
1021 obj.db.SetUser(user);
1022 parent.debug('web', 'checkUserOneTimePassword: success (SMS).');
1023 func(true, { twoFactorType: 'sms' });
1024 return;
1025 }
1026 }
1027
1028 // Check messenger key
1029 if ((otpmsg) && (user.msghandle != null) && (user.otpmsg != null) && (user.otpmsg.d != null) && (user.otpmsg.k === token)) {
1030 var deltaTime = (Date.now() - user.otpmsg.d);
1031 if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the Messenger token (10000 * 60 * 5).
1032 delete user.otpmsg;
1033 obj.db.SetUser(user);
1034 parent.debug('web', 'checkUserOneTimePassword: success (Messenger).');
1035 func(true, { twoFactorType: 'messenger' });
1036 return;
1037 }
1038 }
1039
1040 // Check hardware key
1041 if (user.otphkeys && (user.otphkeys.length > 0) && (typeof (hwtoken) == 'string') && (hwtoken.length > 0)) {
1042 var authResponse = null;
1043 try { authResponse = JSON.parse(hwtoken); } catch (ex) { }
1044 if ((authResponse != null) && (authResponse.clientDataJSON)) {
1045 // Get all WebAuthn keys
1046 var webAuthnKeys = [];
1047 for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 3) { webAuthnKeys.push(user.otphkeys[i]); } }
1048 if (webAuthnKeys.length > 0) {
1049 // Decode authentication response
1050 var clientAssertionResponse = { response: {} };
1051 clientAssertionResponse.id = authResponse.id;
1052 clientAssertionResponse.rawId = Buffer.from(authResponse.id, 'base64');
1053 clientAssertionResponse.response.authenticatorData = Buffer.from(authResponse.authenticatorData, 'base64');
1054 clientAssertionResponse.response.clientDataJSON = Buffer.from(authResponse.clientDataJSON, 'base64');
1055 clientAssertionResponse.response.signature = Buffer.from(authResponse.signature, 'base64');
1056 clientAssertionResponse.response.userHandle = Buffer.from(authResponse.userHandle, 'base64');
1057
1058 // Look for the key with clientAssertionResponse.id
1059 var webAuthnKey = null;
1060 for (var i = 0; i < webAuthnKeys.length; i++) { if (webAuthnKeys[i].keyId == clientAssertionResponse.id) { webAuthnKey = webAuthnKeys[i]; } }
1061
1062 // If we found a valid key to use, let's validate the response
1063 if (webAuthnKey != null) {
1064 // Figure out the origin
1065 var httpport = ((args.aliasport != null) ? args.aliasport : args.port);
1066 var origin = 'https://' + (domain.dns ? domain.dns : parent.certificates.CommonName);
1067 if (httpport != 443) { origin += ':' + httpport; }
1068
1069 var u2fchallenge = null;
1070 if ((req.session != null) && (req.session.e != null)) { const sec = parent.decryptSessionData(req.session.e); if (sec != null) { u2fchallenge = sec.u2f; } }
1071 var assertionExpectations = {
1072 challenge: u2fchallenge,
1073 origin: origin,
1074 factor: 'either',
1075 fmt: 'fido-u2f',
1076 publicKey: webAuthnKey.publicKey,
1077 prevCounter: webAuthnKey.counter,
1078 userHandle: Buffer.from(user._id, 'binary').toString('base64')
1079 };
1080
1081 var webauthnResponse = null;
1082 try { webauthnResponse = obj.webauthn.verifyAuthenticatorAssertionResponse(clientAssertionResponse.response, assertionExpectations); } catch (ex) { parent.debug('web', 'checkUserOneTimePassword: exception ' + ex); console.log(ex); }
1083 if ((webauthnResponse != null) && (webauthnResponse.verified === true)) {
1084 // Update the hardware key counter and accept the 2nd factor
1085 webAuthnKey.counter = webauthnResponse.counter;
1086 obj.db.SetUser(user);
1087 parent.debug('web', 'checkUserOneTimePassword: success (hardware).');
1088 func(true, { twoFactorType: 'fido' });
1089 } else {
1090 parent.debug('web', 'checkUserOneTimePassword: fail (hardware).');
1091 func(false);
1092 }
1093 return;
1094 }
1095 }
1096 }
1097 }
1098
1099 // Check Google Authenticator
1100 const otplib = require('otplib')
1101 otplib.authenticator.options = { window: 2 }; // Set +/- 1 minute window
1102 if (user.otpsecret && (typeof (token) == 'string') && (token.length == 6) && (otplib.authenticator.check(token, user.otpsecret) == true)) {
1103 parent.debug('web', 'checkUserOneTimePassword: success (authenticator).');
1104 func(true, { twoFactorType: 'otp' });
1105 return;
1106 };
1107
1108 // Check written down keys
1109 if ((user.otpkeys != null) && (user.otpkeys.keys != null) && (typeof (token) == 'string') && (token.length == 8)) {
1110 var tokenNumber = parseInt(token);
1111 for (var i = 0; i < user.otpkeys.keys.length; i++) {
1112 if ((tokenNumber === user.otpkeys.keys[i].p) && (user.otpkeys.keys[i].u === true)) {
1113 parent.debug('web', 'checkUserOneTimePassword: success (one-time).');
1114 user.otpkeys.keys[i].u = false; func(true, { twoFactorType: 'backup' }); return;
1115 }
1116 }
1117 }
1118
1119 // Check OTP hardware key (Yubikey OTP)
1120 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)) {
1121 var keyId = token.substring(0, 12);
1122
1123 // Find a matching OTP key
1124 var match = false;
1125 for (var i = 0; i < user.otphkeys.length; i++) { if ((user.otphkeys[i].type === 2) && (user.otphkeys[i].keyid === keyId)) { match = true; } }
1126
1127 // If we have a match, check the OTP
1128 if (match === true) {
1129 var yub = require('yub');
1130 yub.init(domain.yubikey.id, domain.yubikey.secret);
1131 yub.verify(token, function (err, results) {
1132 if ((results != null) && (results.status == 'OK')) {
1133 parent.debug('web', 'checkUserOneTimePassword: success (Yubikey).');
1134 func(true, { twoFactorType: 'hwotp' });
1135 } else {
1136 parent.debug('web', 'checkUserOneTimePassword: fail (Yubikey).');
1137 func(false);
1138 }
1139 });
1140 return;
1141 }
1142 }
1143
1144 parent.debug('web', 'checkUserOneTimePassword: fail (2).');
1145 func(false);
1146 }
1147
1148 // Return a U2F hardware key challenge
1149 function getHardwareKeyChallenge(req, domain, user, func) {
1150 var sec = {};
1151 if (req.session == null) { req.session = {}; } else { try { sec = parent.decryptSessionData(req.session.e); } catch (ex) { } }
1152
1153 if (user.otphkeys && (user.otphkeys.length > 0)) {
1154 // Get all WebAuthn keys
1155 var webAuthnKeys = [];
1156 for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 3) { webAuthnKeys.push(user.otphkeys[i]); } }
1157 if (webAuthnKeys.length > 0) {
1158 // Generate a Webauthn challenge, this is really easy, no need to call any modules to do this.
1159 var authnOptions = { type: 'webAuthn', keyIds: [], timeout: 60000, challenge: obj.crypto.randomBytes(64).toString('base64') };
1160 // userVerification: 'preferred' use security pin if possible (default), 'required' always use security pin, 'discouraged' do not use security pin.
1161 authnOptions.userVerification = (domain.passwordrequirements && domain.passwordrequirements.fidopininput) ? domain.passwordrequirements.fidopininput : 'preferred'; // Use the domain setting if it exists, otherwise use 'preferred'.{
1162 for (var i = 0; i < webAuthnKeys.length; i++) { authnOptions.keyIds.push(webAuthnKeys[i].keyId); }
1163 sec.u2f = authnOptions.challenge;
1164 req.session.e = parent.encryptSessionData(sec);
1165 parent.debug('web', 'getHardwareKeyChallenge: success');
1166 func(JSON.stringify(authnOptions));
1167 return;
1168 }
1169 }
1170
1171 // Remove the challenge if present
1172 if (sec.u2f != null) { delete sec.u2f; req.session.e = parent.encryptSessionData(sec); }
1173
1174 parent.debug('web', 'getHardwareKeyChallenge: fail');
1175 func('');
1176 }
1177
1178 // Redirect a root request to a different page
1179 function handleRootRedirect(req, res, direct) {
1180 const domain = checkUserIpAddress(req, res);
1181 if (domain == null) { return; }
1182 res.redirect(domain.rootredirect + getQueryPortion(req));
1183 }
1184
1185 function handleLoginRequest(req, res, direct) {
1186 const domain = checkUserIpAddress(req, res);
1187 if (domain == null) { return; }
1188 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1189 if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
1190 if (req.session == null) { req.session = {}; }
1191
1192 // Check if this is a banned ip address
1193 if (obj.checkAllowLogin(req) == false) {
1194 // Wait and redirect the user
1195 setTimeout(function () {
1196 req.session.messageid = 114; // IP address blocked, try again later.
1197 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1198 }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
1199 return;
1200 }
1201
1202 // Normally, use the body username/password. If this is a token, use the username/password in the session.
1203 var xusername = req.body.username, xpassword = req.body.password;
1204 if ((xusername == null) && (xpassword == null) && (req.body.token != null)) {
1205 const sec = parent.decryptSessionData(req.session.e);
1206 xusername = sec.tuser; xpassword = sec.tpass;
1207 }
1208
1209 // Authenticate the user
1210 obj.authenticate(xusername, xpassword, domain, function (err, userid, passhint, loginOptions) {
1211 if (userid) {
1212 var user = obj.users[userid];
1213
1214 // Check if we are in maintenance mode
1215 if ((parent.config.settings.maintenancemode != null) && (user.siteadmin != 4294967295)) {
1216 req.session.messageid = 115; // Server under maintenance
1217 req.session.loginmode = 1;
1218 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1219 return;
1220 }
1221
1222 var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.email != null) && (user.emailVerified == true) && (user.otpekey != null));
1223 var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
1224 var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
1225 var push2fa = ((parent.firebase != null) && (user.otpdev != null));
1226 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));
1227
1228 // Check if two factor can be skipped
1229 const twoFactorSkip = checkUserOneTimePasswordSkip(domain, user, req, loginOptions);
1230
1231 // Check if this user has 2-step login active
1232 if ((twoFactorSkip == null) && (req.session.loginmode != 6) && checkUserOneTimePasswordRequired(domain, user, req, loginOptions)) {
1233 if ((req.body.hwtoken == '**timeout**')) {
1234 delete req.session; // Clear the session
1235 res.redirect(domain.url + getQueryPortion(req));
1236 return;
1237 }
1238
1239 if ((req.body.hwtoken == '**email**') && email2fa) {
1240 user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
1241 obj.db.SetUser(user);
1242 parent.debug('web', 'Sending 2FA email to: ' + user.email);
1243 domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key);
1244 req.session.messageid = 2; // "Email sent" message
1245 req.session.loginmode = 4;
1246 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1247 return;
1248 }
1249
1250 if ((req.body.hwtoken == '**sms**') && sms2fa) {
1251 // Cause a token to be sent to the user's phone number
1252 user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
1253 obj.db.SetUser(user);
1254 parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
1255 parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
1256 // Ask for a login token & confirm sms was sent
1257 req.session.messageid = 4; // "SMS sent" message
1258 req.session.loginmode = 4;
1259 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1260 return;
1261 }
1262
1263 if ((req.body.hwtoken == '**msg**') && msg2fa) {
1264 // Cause a token to be sent to the user's messenger account
1265 user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
1266 obj.db.SetUser(user);
1267 parent.debug('web', 'Sending 2FA message to: ' + user.msghandle);
1268 parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req));
1269 // Ask for a login token & confirm message was sent
1270 req.session.messageid = 6; // "Message sent" message
1271 req.session.loginmode = 4;
1272 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1273 return;
1274 }
1275
1276 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')) {
1277 // Redirect to duo here
1278 const duo = require('@duosecurity/duo_universal');
1279 const client = new duo.Client({
1280 clientId: domain.duo2factor.integrationkey,
1281 clientSecret: domain.duo2factor.secretkey,
1282 apiHost: domain.duo2factor.apihostname,
1283 redirectUrl: obj.generateBaseURL(domain, req) + 'auth-duo' + (domain.loginkey != null ? ('?key=' + domain.loginkey) : '')
1284 });
1285 // Decrypt any session data
1286 const sec = parent.decryptSessionData(req.session.e);
1287 sec.duostate = client.generateState();
1288 req.session.e = parent.encryptSessionData(sec);
1289 parent.debug('web', 'Redirecting user ' + user._id + ' to Duo');
1290 res.redirect(client.createAuthUrl(user._id.split('/')[2], sec.duostate));
1291 return;
1292 }
1293
1294 // Handle device push notification 2FA request
1295 // We create a browser cookie, send it back and when the browser connects it's web socket, it will trigger the push notification.
1296 if ((req.body.hwtoken == '**push**') && push2fa && ((domain.passwordrequirements == null) || (domain.passwordrequirements.push2factor != false))) {
1297 const logincodeb64 = Buffer.from(obj.common.zeroPad(getRandomSixDigitInteger(), 6)).toString('base64');
1298 const sessioncode = obj.crypto.randomBytes(24).toString('base64');
1299
1300 // Create a browser cookie so the browser can connect using websocket and wait for device accept/reject.
1301 const browserCookie = parent.encodeCookie({ a: 'waitAuth', c: logincodeb64, u: user._id, n: user.otpdev, s: sessioncode, d: domain.id });
1302
1303 // Get the HTTPS port
1304 var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port if specified
1305
1306 // Get the agent connection server name
1307 var serverName = obj.getWebServerName(domain, req);
1308 if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
1309
1310 // Build the connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
1311 var xdomain = (domain.dns == null) ? domain.id : '';
1312 if (xdomain != '') xdomain += '/';
1313 var url = 'wss://' + serverName + ':' + httpsPort + '/' + xdomain + '2fahold.ashx?c=' + browserCookie;
1314
1315 // Request that the login page wait for device auth
1316 req.session.messageid = 5; // "Sending notification..." message
1317 req.session.passhint = url;
1318 req.session.loginmode = 8;
1319 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1320 return;
1321 }
1322
1323 checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result, authData) {
1324 if (result == false) {
1325 var randomWaitTime = 0;
1326
1327 // Check if 2FA is allowed for this IP address
1328 if (obj.checkAllow2Fa(req) == false) {
1329 // Wait and redirect the user
1330 setTimeout(function () {
1331 req.session.messageid = 114; // IP address blocked, try again later.
1332 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1333 }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
1334 return;
1335 }
1336
1337 // 2-step auth is required, but the token is not present or not valid.
1338 if ((req.body.token != null) || (req.body.hwtoken != null)) {
1339 randomWaitTime = 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095); // This is a fail, wait a random time. 2 to 6 seconds.
1340 req.session.messageid = 108; // Invalid token, try again.
1341 obj.parent.authLog('https', 'Failed 2FA for ' + xusername + ' from ' + cleanRemoteAddr(req.clientIp) + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'] });
1342 parent.debug('web', 'handleLoginRequest: invalid 2FA token');
1343 const ua = obj.getUserAgentInfo(req);
1344 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] });
1345 obj.setbad2Fa(req);
1346 } else {
1347 parent.debug('web', 'handleLoginRequest: 2FA token required');
1348 }
1349
1350 // Wait and redirect the user
1351 setTimeout(function () {
1352 req.session.loginmode = 4;
1353 if ((user.email != null) && (user.emailVerified == true) && (domain.mailserver != null) && (user.otpekey != null)) { req.session.temail = 1; }
1354 if ((user.phone != null) && (parent.smsserver != null)) { req.session.tsms = 1; }
1355 if ((user.msghandle != null) && (parent.msgserver != null) && (parent.msgserver.providers != 0)) { req.session.tmsg = 1; }
1356 if ((user.otpdev != null) && (parent.firebase != null)) { req.session.tpush = 1; }
1357 if ((user.otpduo != null)) { req.session.tduo = 1; }
1358 req.session.e = parent.encryptSessionData({ tuserid: userid, tuser: xusername, tpass: xpassword });
1359 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1360 }, randomWaitTime);
1361 } else {
1362 // Check if we need to remember this device
1363 if ((req.body.remembertoken === 'on') && ((domain.twofactorcookiedurationdays == null) || (domain.twofactorcookiedurationdays > 0))) {
1364 var maxCookieAge = domain.twofactorcookiedurationdays;
1365 if (typeof maxCookieAge != 'number') { maxCookieAge = 30; }
1366 const twoFactorCookie = obj.parent.encodeCookie({ userid: user._id, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey);
1367 res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: parent.config.settings.sessionsamesite, secure: true });
1368 }
1369
1370 // Check if email address needs to be confirmed
1371 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'))
1372 if (emailcheck && (user.emailVerified !== true)) {
1373 parent.debug('web', 'Redirecting using ' + user.name + ' to email check login page');
1374 req.session.messageid = 3; // "Email verification required" message
1375 req.session.loginmode = 7;
1376 req.session.passhint = user.email;
1377 req.session.cuserid = userid;
1378 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1379 return;
1380 }
1381
1382 // Login successful
1383 parent.debug('web', 'handleLoginRequest: successful 2FA login');
1384 if (authData != null) { if (loginOptions == null) { loginOptions = {}; } loginOptions.twoFactorType = authData.twoFactorType; }
1385 completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions);
1386 }
1387 });
1388 return;
1389 }
1390
1391 // Check if email address needs to be confirmed
1392 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'))
1393 if (emailcheck && (user.emailVerified !== true)) {
1394 parent.debug('web', 'Redirecting using ' + user.name + ' to email check login page');
1395 req.session.messageid = 3; // "Email verification required" message
1396 req.session.loginmode = 7;
1397 req.session.passhint = user.email;
1398 req.session.cuserid = userid;
1399 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1400 return;
1401 }
1402
1403 // Login successful
1404 parent.debug('web', 'handleLoginRequest: successful login');
1405 if (twoFactorSkip != null) { if (loginOptions == null) { loginOptions = {}; } loginOptions.twoFactorType = twoFactorSkip.twoFactorType; }
1406 completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions);
1407 } else {
1408 // Login failed, log the error
1409 obj.parent.authLog('https', 'Failed password for ' + xusername + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'] });
1410
1411 // Wait a random delay
1412 setTimeout(function () {
1413 // If the account is locked, display that.
1414 if (typeof xusername == 'string') {
1415 var xuserid = 'user/' + domain.id + '/' + xusername.toLowerCase();
1416 if (err == 'locked') {
1417 parent.debug('web', 'handleLoginRequest: login failed, locked account');
1418 req.session.messageid = 110; // Account locked.
1419 const ua = obj.getUserAgentInfo(req);
1420 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] });
1421 obj.setbadLogin(req);
1422 } else if (err == 'denied') {
1423 parent.debug('web', 'handleLoginRequest: login failed, access denied');
1424 req.session.messageid = 111; // Access denied.
1425 const ua = obj.getUserAgentInfo(req);
1426 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] });
1427 obj.setbadLogin(req);
1428 } else {
1429 parent.debug('web', 'handleLoginRequest: login failed, bad username and password');
1430 req.session.messageid = 112; // Login failed, check username and password.
1431 const ua = obj.getUserAgentInfo(req);
1432 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] });
1433 obj.setbadLogin(req);
1434 }
1435 }
1436
1437 // Clean up login mode and display password hint if present.
1438 delete req.session.loginmode;
1439 if ((passhint != null) && (passhint.length > 0)) {
1440 req.session.passhint = passhint;
1441 } else {
1442 delete req.session.passhint;
1443 }
1444
1445 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1446 }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095)); // Wait for 2 to ~6 seconds.
1447 }
1448 });
1449 }
1450
1451 function completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions) {
1452 // Check if we need to change the password
1453 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))))) {
1454 // Request a password change
1455 parent.debug('web', 'handleLoginRequest: login ok, password change requested');
1456 req.session.loginmode = 6;
1457 req.session.messageid = 113; // Password change requested.
1458
1459 // Decrypt any session data
1460 const sec = parent.decryptSessionData(req.session.e);
1461 sec.rtuser = xusername;
1462 sec.rtpass = xpassword;
1463 sec.rtreset = true;
1464 req.session.e = parent.encryptSessionData(sec);
1465
1466 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1467 return;
1468 }
1469
1470 // Save login time
1471 user.pastlogin = user.login;
1472 user.login = user.access = Math.floor(Date.now() / 1000);
1473 obj.db.SetUser(user);
1474
1475 // Notify account login
1476 const targets = ['*', 'server-users', user._id];
1477 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
1478 const ua = obj.getUserAgentInfo(req);
1479 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 };
1480 if (loginOptions != null) {
1481 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.
1482 if (loginOptions.twoFactorType != null) { loginEvent.twoFactorType = loginOptions.twoFactorType; }
1483 }
1484 obj.parent.DispatchEvent(targets, obj, loginEvent);
1485
1486 // Regenerate session when signing in to prevent fixation
1487 //req.session.regenerate(function () {
1488 // Store the user's primary key in the session store to be retrieved, or in this case the entire user object
1489 delete req.session.e;
1490 delete req.session.u2f;
1491 delete req.session.loginmode;
1492 delete req.session.tuserid;
1493 delete req.session.tuser;
1494 delete req.session.tpass;
1495 delete req.session.temail;
1496 delete req.session.tsms;
1497 delete req.session.tmsg;
1498 delete req.session.tduo;
1499 delete req.session.tpush;
1500 delete req.session.messageid;
1501 delete req.session.passhint;
1502 delete req.session.cuserid;
1503 delete req.session.expire;
1504 delete req.session.currentNode;
1505 req.session.userid = userid;
1506 req.session.ip = req.clientIp;
1507 setSessionRandom(req);
1508 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 });
1509
1510 // If a login token was used, add this information and expire time to the session.
1511 if ((loginOptions != null) && (loginOptions.tokenName != null) && (loginOptions.tokenUser != null)) {
1512 req.session.loginToken = loginOptions.tokenUser;
1513 if (loginOptions.expire != null) { req.session.expire = loginOptions.expire; }
1514 }
1515
1516 if (req.body.viewmode) { req.session.viewmode = req.body.viewmode; }
1517 if (req.body.host) {
1518 // TODO: This is a terrible search!!! FIX THIS.
1519 /*
1520 obj.db.GetAllType('node', function (err, docs) {
1521 for (var i = 0; i < docs.length; i++) {
1522 if (docs[i].name == req.body.host) {
1523 req.session.currentNode = docs[i]._id;
1524 break;
1525 }
1526 }
1527 console.log("CurrentNode: " + req.session.currentNode);
1528 // This redirect happens after finding node is completed
1529 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1530 });
1531 */
1532 parent.debug('web', 'handleLoginRequest: login ok (1)');
1533 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } // Temporary
1534 } else {
1535 parent.debug('web', 'handleLoginRequest: login ok (2)');
1536 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1537 }
1538 //});
1539 }
1540
1541 function handleCreateAccountRequest(req, res, direct) {
1542 const domain = checkUserIpAddress(req, res);
1543 if (domain == null) { return; }
1544
1545 // Allow account creation only if no NATIVE users exist (first user = site admin)
1546 // Count native MeshCentral users only (exclude _external PostgreSQL users)
1547 let nativeUserCount = 0;
1548 for (const userId in obj.users) {
1549 if (!obj.users[userId]._external && obj.users[userId].domain === domain.id) {
1550 nativeUserCount++;
1551 }
1552 }
1553
1554 if (nativeUserCount > 0) {
1555 // Users already exist - redirect to dashboard for account creation
1556 req.session.loginmode = 2;
1557 req.session.messageid = 100; // Unable to create account
1558 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1559 return;
1560 }
1561
1562 if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handleCreateAccountRequest: failed checks.'); res.sendStatus(404); return; }
1563 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1564 if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
1565 if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
1566
1567 // Check if we are in maintenance mode
1568 if (parent.config.settings.maintenancemode != null) {
1569 req.session.messageid = 115; // Server under maintenance
1570 req.session.loginmode = 1;
1571 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1572 return;
1573 }
1574
1575 // Always lowercase the email address
1576 if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
1577
1578 // If the email is the username, set this here.
1579 if (domain.usernameisemail) { req.body.username = req.body.email; }
1580
1581 // Check if there is domain.newAccountToken, check if supplied token is valid
1582 if ((domain.newaccountspass != null) && (domain.newaccountspass != '') && (req.body.newaccountspass != domain.newaccountspass)) {
1583 parent.debug('web', 'handleCreateAccountRequest: Invalid account creation token');
1584 req.session.loginmode = 2;
1585 req.session.messageid = 103; // Invalid account creation token.
1586 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1587 return;
1588 }
1589
1590 // If needed, check the new account creation CAPTCHA
1591 if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
1592 const c = parent.decodeCookie(req.body.captchaargs, parent.loginCookieEncryptionKey, 10); // 10 minute timeout
1593 if ((c == null) || (c.type != 'newAccount') || (typeof c.captcha != 'string') || (c.captcha.length < 5) || (c.captcha != req.body.anewaccountcaptcha)) {
1594 req.session.loginmode = 2;
1595 req.session.messageid = 117; // Invalid security check
1596 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1597 return;
1598 }
1599 }
1600
1601 // Accounts that start with ~ are not allowed
1602 if ((typeof req.body.username != 'string') || (req.body.username.length < 1) || (req.body.username[0] == '~')) {
1603 parent.debug('web', 'handleCreateAccountRequest: unable to create account (0)');
1604 req.session.loginmode = 2;
1605 req.session.messageid = 100; // Unable to create account.
1606 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1607 return;
1608 }
1609
1610 // Count the number of users in this domain
1611 var domainUserCount = 0;
1612 for (var i in obj.users) { if (obj.users[i].domain == domain.id) { domainUserCount++; } }
1613
1614 // Check if we are allowed to create new users using the login screen
1615 if ((domain.newaccounts !== 1) && (domain.newaccounts !== true) && (domainUserCount > 0)) {
1616 parent.debug('web', 'handleCreateAccountRequest: domainUserCount > 1.');
1617 res.sendStatus(401);
1618 return;
1619 }
1620
1621 // Check if this request is for an allows email domain
1622 if ((domain.newaccountemaildomains != null) && Array.isArray(domain.newaccountemaildomains)) {
1623 var i = -1;
1624 if (typeof req.body.email == 'string') { i = req.body.email.indexOf('@'); }
1625 if (i == -1) {
1626 parent.debug('web', 'handleCreateAccountRequest: unable to create account (1)');
1627 req.session.loginmode = 2;
1628 req.session.messageid = 100; // Unable to create account.
1629 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1630 return;
1631 }
1632 var emailok = false, emaildomain = req.body.email.substring(i + 1).toLowerCase();
1633 for (var i in domain.newaccountemaildomains) { if (emaildomain == domain.newaccountemaildomains[i].toLowerCase()) { emailok = true; } }
1634 if (emailok == false) {
1635 parent.debug('web', 'handleCreateAccountRequest: unable to create account (2)');
1636 req.session.loginmode = 2;
1637 req.session.messageid = 100; // Unable to create account.
1638 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1639 return;
1640 }
1641 }
1642
1643 // Check if we exceed the maximum number of user accounts
1644 obj.db.isMaxType(domain.limits.maxuseraccounts, 'user', domain.id, function (maxExceed) {
1645 if (maxExceed) {
1646 parent.debug('web', 'handleCreateAccountRequest: account limit reached');
1647 req.session.loginmode = 2;
1648 req.session.messageid = 101; // Account limit reached.
1649 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1650 } else {
1651 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)) {
1652 parent.debug('web', 'handleCreateAccountRequest: unable to create account (3)');
1653 req.session.loginmode = 2;
1654 req.session.messageid = 100; // Unable to create account.
1655 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1656 } else {
1657 // Check if this email was already verified
1658 obj.db.GetUserWithVerifiedEmail(domain.id, req.body.email, function (err, docs) {
1659 if ((docs != null) && (docs.length > 0)) {
1660 parent.debug('web', 'handleCreateAccountRequest: Existing account with this email address');
1661 req.session.loginmode = 2;
1662 req.session.messageid = 102; // Existing account with this email address.
1663 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1664 } else {
1665 // Check if user exists
1666 if (obj.users['user/' + domain.id + '/' + req.body.username.toLowerCase()]) {
1667 parent.debug('web', 'handleCreateAccountRequest: Username already exists');
1668 req.session.loginmode = 2;
1669 req.session.messageid = 104; // Username already exists.
1670 } else {
1671 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 };
1672 if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; }
1673 if (obj.common.validateStrArray(domain.newaccountrealms)) { user.groups = domain.newaccountrealms; }
1674 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; }
1675 if (domainUserCount == 0) { user.siteadmin = 4294967295; /*if (domain.newaccounts === 2) { delete domain.newaccounts; }*/ } // If this is the first user, give the account site admin.
1676
1677 // Auto-join any user groups
1678 if (typeof domain.newaccountsusergroups == 'object') {
1679 for (var i in domain.newaccountsusergroups) {
1680 var ugrpid = domain.newaccountsusergroups[i];
1681 if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
1682 var ugroup = obj.userGroups[ugrpid];
1683 if (ugroup != null) {
1684 // Add group to the user
1685 if (user.links == null) { user.links = {}; }
1686 user.links[ugroup._id] = { rights: 1 };
1687
1688 // Add user to the group
1689 ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
1690 db.Set(ugroup);
1691
1692 // Notify user group change
1693 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 };
1694 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.
1695 parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
1696 }
1697 }
1698 }
1699
1700 obj.users[user._id] = user;
1701 req.session.userid = user._id;
1702 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
1703 setSessionRandom(req);
1704 // Create a user, generate a salt and hash the password
1705 require('./pass').hash(req.body.password1, function (err, salt, hash, tag) {
1706 if (err) throw err;
1707 user.salt = salt;
1708 user.hash = hash;
1709 delete user.passtype;
1710 obj.db.SetUser(user);
1711
1712 // Send the verification email
1713 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); }
1714 }, 0);
1715 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 };
1716 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.
1717 obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
1718 }
1719 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1720 }
1721 });
1722 }
1723 }
1724 });
1725 }
1726
1727 // Called to process an account password reset
1728 function handleResetPasswordRequest(req, res, direct) {
1729 const domain = checkUserIpAddress(req, res);
1730 if (domain == null) { return; }
1731 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1732 if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
1733 if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
1734
1735 // Decrypt any session data
1736 const sec = parent.decryptSessionData(req.session.e);
1737
1738 // Check everything is ok
1739 const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false) || (sec.rtreset === true));
1740 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')) {
1741 parent.debug('web', 'handleResetPasswordRequest: checks failed');
1742 delete req.session.e;
1743 delete req.session.u2f;
1744 delete req.session.loginmode;
1745 delete req.session.tuserid;
1746 delete req.session.tuser;
1747 delete req.session.tpass;
1748 delete req.session.temail;
1749 delete req.session.tsms;
1750 delete req.session.tmsg;
1751 delete req.session.tpush;
1752 delete req.session.messageid;
1753 delete req.session.passhint;
1754 delete req.session.cuserid;
1755 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1756 return;
1757 }
1758
1759 // Authenticate the user
1760 obj.authenticate(sec.rtuser, sec.rtpass, domain, function (err, userid, passhint, loginOptions) {
1761 if (userid) {
1762 // Login
1763 var user = obj.users[userid];
1764
1765 // If we have password requirements, check this here.
1766 if (!obj.common.checkPasswordRequirements(req.body.rpassword1, domain.passwordrequirements)) {
1767 parent.debug('web', 'handleResetPasswordRequest: password rejected, use a different one (1)');
1768 req.session.loginmode = 6;
1769 req.session.messageid = 105; // Password rejected, use a different one.
1770 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1771 return;
1772 }
1773
1774 // Check if the password is the same as a previous one
1775 obj.checkOldUserPasswords(domain, user, req.body.rpassword1, function (result) {
1776 if (result != 0) {
1777 // This is the same password as an older one, request a password change again
1778 parent.debug('web', 'handleResetPasswordRequest: password rejected, use a different one (2)');
1779 req.session.loginmode = 6;
1780 req.session.messageid = 105; // Password rejected, use a different one.
1781 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1782 } else {
1783 // Update the password, use a different salt.
1784 require('./pass').hash(req.body.rpassword1, function (err, salt, hash, tag) {
1785 const nowSeconds = Math.floor(Date.now() / 1000);
1786 if (err) { parent.debug('web', 'handleResetPasswordRequest: hash error.'); throw err; }
1787
1788 if (domain.passwordrequirements != null) {
1789 // Save password hint if this feature is enabled
1790 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; }
1791
1792 // Save previous password if this feature is enabled
1793 if ((typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
1794 if (user.oldpasswords == null) { user.oldpasswords = []; }
1795 user.oldpasswords.push({ salt: user.salt, hash: user.hash, start: user.passchange, end: nowSeconds });
1796 const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
1797 if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
1798 }
1799 }
1800
1801 user.salt = salt;
1802 user.hash = hash;
1803 user.passchange = user.access = nowSeconds;
1804 delete user.passtype;
1805 obj.db.SetUser(user);
1806
1807 // Event the account change
1808 var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'User password reset', domain: domain.id };
1809 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.
1810 obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
1811
1812 // Login successful
1813 parent.debug('web', 'handleResetPasswordRequest: success');
1814 req.session.userid = userid;
1815 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
1816 setSessionRandom(req);
1817 const sec = parent.decryptSessionData(req.session.e);
1818 completeLoginRequest(req, res, domain, obj.users[userid], userid, sec.tuser, sec.tpass, direct, loginOptions);
1819 }, 0);
1820 }
1821 }, 0);
1822 } else {
1823 // Failed, error out.
1824 parent.debug('web', 'handleResetPasswordRequest: failed authenticate()');
1825 delete req.session.e;
1826 delete req.session.u2f;
1827 delete req.session.loginmode;
1828 delete req.session.tuserid;
1829 delete req.session.tuser;
1830 delete req.session.tpass;
1831 delete req.session.temail;
1832 delete req.session.tsms;
1833 delete req.session.tmsg;
1834 delete req.session.tpush;
1835 delete req.session.messageid;
1836 delete req.session.passhint;
1837 delete req.session.cuserid;
1838 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1839 return;
1840 }
1841 });
1842 }
1843
1844 // Called to process an account reset request
1845 function handleResetAccountRequest(req, res, direct) {
1846 const domain = checkUserIpAddress(req, res);
1847 if (domain == null) { return; }
1848 const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false));
1849 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; }
1850 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1851 if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
1852 if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
1853
1854 // Always lowercase the email address
1855 if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
1856
1857 // Get the email from the body or session.
1858 var email = req.body.email;
1859 if ((email == null) || (email == '')) { email = req.session.temail; }
1860
1861 // Check the email string format
1862 if (!email || checkEmail(email) == false) {
1863 parent.debug('web', 'handleResetAccountRequest: Invalid email');
1864 req.session.loginmode = 3;
1865 req.session.messageid = 106; // Invalid email.
1866 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1867 } else {
1868 obj.db.GetUserWithVerifiedEmail(domain.id, email, function (err, docs) {
1869 // Remove all accounts that start with ~ since they are special accounts.
1870 var cleanDocs = [];
1871 if ((err == null) && (docs.length > 0)) {
1872 for (var i in docs) {
1873 const user = docs[i];
1874 const locked = ((user.siteadmin != null) && (user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)); // No password recovery for locked accounts
1875 const specialAccount = (user._id.split('/')[2].startsWith('~')); // No password recovery for special accounts
1876 if ((specialAccount == false) && (locked == false)) { cleanDocs.push(user); }
1877 }
1878 }
1879 docs = cleanDocs;
1880
1881 // Check if we have any account that match this email address
1882 if ((err != null) || (docs.length == 0)) {
1883 parent.debug('web', 'handleResetAccountRequest: Account not found');
1884 req.session.loginmode = 3;
1885 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.
1886 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1887 } else {
1888 // 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.
1889 var responseSent = false;
1890 for (var i in docs) {
1891 var user = docs[i];
1892 if (checkUserOneTimePasswordRequired(domain, user, req) == true) {
1893 // Second factor setup, request it now.
1894 checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result, authData) {
1895 if (result == false) {
1896 if (i == 0) {
1897
1898 // Check if 2FA is allowed for this IP address
1899 if (obj.checkAllow2Fa(req) == false) {
1900 // Wait and redirect the user
1901 setTimeout(function () {
1902 req.session.messageid = 114; // IP address blocked, try again later.
1903 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1904 }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
1905 return;
1906 }
1907
1908 // 2-step auth is required, but the token is not present or not valid.
1909 parent.debug('web', 'handleResetAccountRequest: Invalid 2FA token, try again');
1910 if ((req.body.token != null) || (req.body.hwtoken != null)) {
1911 var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
1912 var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
1913 if ((req.body.hwtoken == '**sms**') && sms2fa) {
1914 // Cause a token to be sent to the user's phone number
1915 user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
1916 obj.db.SetUser(user);
1917 parent.debug('web', 'Sending 2FA SMS for password recovery to: ' + user.phone);
1918 parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
1919 req.session.messageid = 4; // SMS sent.
1920 } else if ((req.body.hwtoken == '**msg**') && msg2fa) {
1921 // Cause a token to be sent to the user's messager account
1922 user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
1923 obj.db.SetUser(user);
1924 parent.debug('web', 'Sending 2FA message for password recovery to: ' + user.msghandle);
1925 parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req));
1926 req.session.messageid = 6; // Message sent.
1927 } else {
1928 req.session.messageid = 108; // Invalid token, try again.
1929 const ua = obj.getUserAgentInfo(req);
1930 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] });
1931 obj.setbad2Fa(req);
1932 }
1933 }
1934 req.session.loginmode = 5;
1935 req.session.temail = email;
1936 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1937 }
1938 } else {
1939 // Send email to perform recovery.
1940 delete req.session.temail;
1941 if (domain.mailserver != null) {
1942 domain.mailserver.sendAccountResetMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
1943 if (i == 0) {
1944 parent.debug('web', 'handleResetAccountRequest: Hold on, reset mail sent.');
1945 req.session.loginmode = 1;
1946 req.session.messageid = 1; // If valid, reset mail sent.
1947 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1948 }
1949 } else {
1950 if (i == 0) {
1951 parent.debug('web', 'handleResetAccountRequest: Unable to sent email.');
1952 req.session.loginmode = 3;
1953 req.session.messageid = 109; // Unable to sent email.
1954 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1955 }
1956 }
1957 }
1958 });
1959 } else {
1960 // No second factor, send email to perform recovery.
1961 if (domain.mailserver != null) {
1962 domain.mailserver.sendAccountResetMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
1963 if (i == 0) {
1964 parent.debug('web', 'handleResetAccountRequest: Hold on, reset mail sent.');
1965 req.session.loginmode = 1;
1966 req.session.messageid = 1; // If valid, reset mail sent.
1967 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1968 }
1969 } else {
1970 if (i == 0) {
1971 parent.debug('web', 'handleResetAccountRequest: Unable to sent email.');
1972 req.session.loginmode = 3;
1973 req.session.messageid = 109; // Unable to sent email.
1974 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1975 }
1976 }
1977 }
1978 }
1979 }
1980 });
1981 }
1982 }
1983
1984 // Handle account email change and email verification request
1985 function handleCheckAccountEmailRequest(req, res, direct) {
1986 const domain = checkUserIpAddress(req, res);
1987 if (domain == null) { return; }
1988 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; }
1989 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
1990 if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
1991 if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
1992
1993 // Always lowercase the email address
1994 if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
1995
1996 // Get the email from the body or session.
1997 var email = req.body.email;
1998 if ((email == null) || (email == '')) { email = req.session.temail; }
1999
2000 // Check if this request is for an allows email domain
2001 if ((domain.newaccountemaildomains != null) && Array.isArray(domain.newaccountemaildomains)) {
2002 var i = -1;
2003 if (typeof req.body.email == 'string') { i = req.body.email.indexOf('@'); }
2004 if (i == -1) {
2005 parent.debug('web', 'handleCreateAccountRequest: unable to create account (1)');
2006 req.session.loginmode = 7;
2007 req.session.messageid = 106; // Invalid email.
2008 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2009 return;
2010 }
2011 var emailok = false, emaildomain = req.body.email.substring(i + 1).toLowerCase();
2012 for (var i in domain.newaccountemaildomains) { if (emaildomain == domain.newaccountemaildomains[i].toLowerCase()) { emailok = true; } }
2013 if (emailok == false) {
2014 parent.debug('web', 'handleCreateAccountRequest: unable to create account (2)');
2015 req.session.loginmode = 7;
2016 req.session.messageid = 106; // Invalid email.
2017 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2018 return;
2019 }
2020 }
2021
2022 // Check the email string format
2023 if (!email || checkEmail(email) == false) {
2024 parent.debug('web', 'handleCheckAccountEmailRequest: Invalid email');
2025 req.session.loginmode = 7;
2026 req.session.messageid = 106; // Invalid email.
2027 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2028 } else {
2029 // Check is email already exists
2030 obj.db.GetUserWithVerifiedEmail(domain.id, email, function (err, docs) {
2031 if ((err != null) || ((docs.length > 0) && (docs.find(function (u) { return (u._id === req.session.cuserid); }) < 0))) {
2032 // Email already exists
2033 req.session.messageid = 102; // Existing account with this email address.
2034 } else {
2035 // Update the user and notify of user email address change
2036 var user = obj.users[req.session.cuserid];
2037 if (user.email != email) {
2038 user.email = email;
2039 db.SetUser(user);
2040 var targets = ['*', 'server-users', user._id];
2041 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
2042 var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Account changed: ' + user.name, domain: domain.id };
2043 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.
2044 parent.DispatchEvent(targets, obj, event);
2045 }
2046
2047 // Send the verification email
2048 domain.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
2049
2050 // Send the response
2051 req.session.messageid = 2; // Email sent.
2052 }
2053 req.session.loginmode = 7;
2054 delete req.session.cuserid;
2055 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2056 });
2057 }
2058 }
2059
2060 // Called to process a web based email verification request
2061 function handleCheckMailRequest(req, res) {
2062 const domain = checkUserIpAddress(req, res);
2063 if (domain == null) { return; }
2064 if ((domain.auth == 'sspi') || (domain.auth == 'ldap') || (domain.mailserver == null)) { parent.debug('web', 'handleCheckMailRequest: failed checks.'); res.sendStatus(404); return; }
2065 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2066
2067 if (req.query.c != null) {
2068 var cookie = obj.parent.decodeCookie(req.query.c, domain.mailserver.mailCookieEncryptionKey, 30);
2069 if ((cookie != null) && (cookie.u != null) && (cookie.u.startsWith('user/')) && (cookie.e != null)) {
2070 var idsplit = cookie.u.split('/');
2071 if ((idsplit.length != 3) || (idsplit[1] != domain.id)) {
2072 parent.debug('web', 'handleCheckMailRequest: Invalid domain.');
2073 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));
2074 } else {
2075 obj.db.Get(cookie.u, function (err, docs) {
2076 if (docs.length == 0) {
2077 parent.debug('web', 'handleCheckMailRequest: Invalid username.');
2078 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));
2079 } else {
2080 var user = docs[0];
2081 if (user.email != cookie.e) {
2082 parent.debug('web', 'handleCheckMailRequest: Invalid e-mail.');
2083 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));
2084 } else {
2085 if (cookie.a == 1) {
2086 // Account email verification
2087 if (user.emailVerified == true) {
2088 parent.debug('web', 'handleCheckMailRequest: email already verified.');
2089 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));
2090 } else {
2091 obj.db.GetUserWithVerifiedEmail(domain.id, user.email, function (err, docs) {
2092 if ((docs.length > 0) && (docs.find(function (u) { return (u._id === user._id); }) < 0)) {
2093 parent.debug('web', 'handleCheckMailRequest: email already in use.');
2094 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));
2095 } else {
2096 parent.debug('web', 'handleCheckMailRequest: email verification success.');
2097
2098 // Set the verified flag
2099 obj.users[user._id].emailVerified = true;
2100 user.emailVerified = true;
2101 obj.db.SetUser(user);
2102
2103 // Event the change
2104 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 };
2105 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.
2106 obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
2107
2108 // Send the confirmation page
2109 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));
2110
2111 // Send a notification
2112 obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: 'Email verified', value: user.email, nolog: 1, id: Math.random() });
2113
2114 // Send to authLog
2115 obj.parent.authLog('https', 'Verified email address ' + user.email + ' for user ' + user.name, { useragent: req.headers['user-agent'] });
2116 }
2117 });
2118 }
2119 } else if (cookie.a == 2) {
2120 // Account reset
2121 if (user.emailVerified != true) {
2122 parent.debug('web', 'handleCheckMailRequest: email not verified.');
2123 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));
2124 } else {
2125 if (req.query.confirm == 1) {
2126 // Set a temporary password
2127 obj.crypto.randomBytes(16, function (err, buf) {
2128 var newpass = buf.toString('base64').split('=').join('').split('/').join('').split('+').join('');
2129 require('./pass').hash(newpass, function (err, salt, hash, tag) {
2130 if (err) throw err;
2131
2132 // Change the password
2133 var userinfo = obj.users[user._id];
2134 userinfo.salt = salt;
2135 userinfo.hash = hash;
2136 delete userinfo.passtype;
2137 userinfo.passchange = userinfo.access = Math.floor(Date.now() / 1000);
2138 delete userinfo.passhint;
2139 obj.db.SetUser(userinfo);
2140
2141 // Event the change
2142 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 };
2143 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.
2144 obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
2145
2146 // Send the new password
2147 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));
2148 parent.debug('web', 'handleCheckMailRequest: send temporary password.');
2149
2150 // Send to authLog
2151 obj.parent.authLog('https', 'Performed account reset for user ' + user.name);
2152 }, 0);
2153 });
2154 } else {
2155 // Display a link for the user to confirm password reset
2156 // 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.
2157 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));
2158 }
2159 }
2160 } else {
2161 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));
2162 }
2163 }
2164 }
2165 });
2166 }
2167 } else {
2168 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));
2169 }
2170 }
2171 }
2172
2173 // Called to process an agent invite GET/POST request
2174 function handleInviteRequest(req, res) {
2175 const domain = getDomain(req);
2176 if (domain == null) { parent.debug('web', 'handleInviteRequest: failed checks.'); res.sendStatus(404); return; }
2177 if (domain.agentinvitecodes != true) { nice404(req, res); return; }
2178 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2179 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
2180
2181 // Each for a device group that has this invite code.
2182 for (var i in obj.meshes) {
2183 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)) {
2184 // Send invitation link, valid for 1 minute.
2185 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)) : ''));
2186 return;
2187 }
2188 }
2189
2190 render(req, res, getRenderPage('invite', req, domain), getRenderArgs({ messageid: 100 }, req, domain)); // Bad invitation code
2191 }
2192
2193 // Called to render the MSTSC (RDP) or SSH web page
2194 function handleMSTSCRequest(req, res, page) {
2195 const domain = getDomain(req);
2196 if (domain == null) { parent.debug('web', 'handleMSTSCRequest: failed checks.'); res.sendStatus(404); return; }
2197 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2198
2199 // Check if we are in maintenance mode
2200 if ((parent.config.settings.maintenancemode != null) && (req.query.loginscreen !== '1')) {
2201 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));
2202 return;
2203 }
2204
2205 // Set features we want to send to this page
2206 var features = 0;
2207 if (domain.allowsavingdevicecredentials === false) { features |= 1; }
2208
2209 // Get the logged in user if present
2210 var user = null;
2211
2212 // If there is a login token, use that
2213 if (req.query.login != null) {
2214 var ucookie = parent.decodeCookie(req.query.login, parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
2215 if ((ucookie != null) && (ucookie.a === 3) && (typeof ucookie.u == 'string')) { user = obj.users[ucookie.u]; }
2216 }
2217
2218 // If no token, see if we have an active session
2219 if ((user == null) && (req.session.userid != null)) { user = obj.users[req.session.userid]; }
2220
2221 // If still no user, see if we have a default user
2222 if ((user == null) && (obj.args.user)) { user = obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]; }
2223
2224 // No user login, exit now
2225 if (user == null) { res.sendStatus(401); return; }
2226
2227 if (req.query.ws != null) {
2228 // This is a query with a websocket relay cookie, check that the cookie is valid and use it.
2229 var rcookie = parent.decodeCookie(req.query.ws, parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
2230 if ((rcookie != null) && (rcookie.domainid == domain.id) && (rcookie.nodeid != null) && (rcookie.tcpport != null)) {
2231
2232 // Fetch the node from the database
2233 obj.db.Get(rcookie.nodeid, function (err, nodes) {
2234 if ((err != null) || (nodes.length != 1)) { res.sendStatus(404); return; }
2235 const node = nodes[0];
2236
2237 // Check if we have SSH/RDP credentials for this device
2238 var serverCredentials = 0;
2239 if (domain.allowsavingdevicecredentials !== false) {
2240 if (page == 'ssh') {
2241 if ((typeof node.ssh == 'object') && (typeof node.ssh.u == 'string') && (typeof node.ssh.p == 'string')) { serverCredentials = 1; } // Username and password
2242 else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string') && (typeof node.ssh.kp == 'string')) { serverCredentials = 2; } // Username, key and password
2243 else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string')) { serverCredentials = 3; } // Username and key. No password.
2244 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
2245 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
2246 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
2247 } else {
2248 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
2249 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
2250 }
2251 }
2252
2253 // Render the page
2254 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));
2255 });
2256 return;
2257 }
2258 }
2259
2260 // Check the nodeid
2261 if (req.query.node != null) {
2262 var nodeidsplit = req.query.node.split('/');
2263 if (nodeidsplit.length == 1) {
2264 req.query.node = 'node/' + domain.id + '/' + nodeidsplit[0]; // Format the nodeid correctly
2265 } else if (nodeidsplit.length == 3) {
2266 if ((nodeidsplit[0] != 'node') || (nodeidsplit[1] != domain.id)) { req.query.node = null; } // Check the nodeid format
2267 } else {
2268 req.query.node = null; // Bad nodeid
2269 }
2270 }
2271
2272 // If there is no nodeid, exit now
2273 if (req.query.node == null) { render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: '', name: '', features: features }, req, domain)); return; }
2274
2275 // Fetch the node from the database
2276 obj.db.Get(req.query.node, function (err, nodes) {
2277 if ((err != null) || (nodes.length != 1)) { res.sendStatus(404); return; }
2278 const node = nodes[0];
2279
2280 // Check access rights, must have remote control rights
2281 if ((obj.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0) { res.sendStatus(401); return; }
2282
2283 // Figure out the target port
2284 var port = 0, serverCredentials = false;
2285 if (page == 'ssh') {
2286 // SSH port
2287 port = 22;
2288 if (typeof node.sshport == 'number') { port = node.sshport; }
2289
2290 // Check if we have SSH credentials for this device
2291 if (domain.allowsavingdevicecredentials !== false) {
2292 if ((typeof node.ssh == 'object') && (typeof node.ssh.u == 'string') && (typeof node.ssh.p == 'string')) { serverCredentials = 1; } // Username and password
2293 else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string') && (typeof node.ssh.kp == 'string')) { serverCredentials = 2; } // Username, key and password
2294 else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string')) { serverCredentials = 3; } // Username and key. No password.
2295 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
2296 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
2297 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
2298 }
2299 } else {
2300 // RDP port
2301 port = 3389;
2302 if (typeof node.rdpport == 'number') { port = node.rdpport; }
2303
2304 // Check if we have RDP credentials for this device
2305 if (domain.allowsavingdevicecredentials !== false) {
2306 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
2307 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
2308 }
2309 }
2310 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; } }
2311
2312 // Generate a cookie and respond
2313 var cookie = parent.encodeCookie({ userid: user._id, domainid: user.domain, nodeid: node._id, tcpport: port }, parent.loginCookieEncryptionKey);
2314 render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: cookie, name: encodeURIComponent(node.name).replace(/'/g, '%27'), serverCredentials: serverCredentials, features: features }, req, domain));
2315 });
2316 }
2317
2318 // Called to handle push-only requests
2319 function handleFirebasePushOnlyRelayRequest(req, res) {
2320 parent.debug('email', 'handleFirebasePushOnlyRelayRequest');
2321 if ((req.body == null) || (req.body.msg == null) || (obj.parent.firebase == null)) { res.sendStatus(404); return; }
2322 if (obj.parent.config.firebase.pushrelayserver == null) { res.sendStatus(404); return; }
2323 if ((typeof obj.parent.config.firebase.pushrelayserver == 'string') && (req.query.key != obj.parent.config.firebase.pushrelayserver)) { res.sendStatus(404); return; }
2324 var data = null;
2325 try { data = JSON.parse(req.body.msg) } catch (ex) { res.sendStatus(404); return; }
2326 if (typeof data != 'object') { res.sendStatus(404); return; }
2327 if (typeof data.pmt != 'string') { res.sendStatus(404); return; }
2328 if (typeof data.payload != 'object') { res.sendStatus(404); return; }
2329 if (typeof data.payload.notification != 'object') { res.sendStatus(404); return; }
2330 if (typeof data.payload.notification.title != 'string') { res.sendStatus(404); return; }
2331 if (typeof data.payload.notification.body != 'string') { res.sendStatus(404); return; }
2332 if (typeof data.options != 'object') { res.sendStatus(404); return; }
2333 if ((data.options.priority != 'Normal') && (data.options.priority != 'High')) { res.sendStatus(404); return; }
2334 if ((typeof data.options.timeToLive != 'number') || (data.options.timeToLive < 1)) { res.sendStatus(404); return; }
2335 parent.debug('email', 'handleFirebasePushOnlyRelayRequest - ok');
2336 obj.parent.firebase.sendToDevice({ pmt: data.pmt }, data.payload, data.options, function (id, err, errdesc) {
2337 if (err == null) { res.sendStatus(200); } else { res.sendStatus(500); }
2338 });
2339 }
2340
2341 // Called to handle two-way push notification relay request
2342 function handleFirebaseRelayRequest(ws, req) {
2343 parent.debug('email', 'handleFirebaseRelayRequest');
2344 if (obj.parent.firebase == null) { try { ws.close(); } catch (e) { } return; }
2345 if (obj.parent.firebase.setupRelay == null) { try { ws.close(); } catch (e) { } return; }
2346 if (obj.parent.config.firebase.relayserver == null) { try { ws.close(); } catch (e) { } return; }
2347 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; }
2348 obj.parent.firebase.setupRelay(ws);
2349 }
2350
2351 // Called to process an agent invite request
2352 function handleAgentInviteRequest(req, res) {
2353 const domain = getDomain(req);
2354 if ((domain == null) || ((req.query.m == null) && (req.query.c == null))) { parent.debug('web', 'handleAgentInviteRequest: failed checks.'); res.sendStatus(404); return; }
2355 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2356
2357 if (req.query.c != null) {
2358 // A cookie is specified in the query string, use that
2359 var cookie = obj.parent.decodeCookie(req.query.c, obj.parent.invitationLinkEncryptionKey);
2360 if (cookie == null) { res.sendStatus(404); return; }
2361 var mesh = obj.meshes[cookie.mid];
2362 if (mesh == null) { res.sendStatus(404); return; }
2363 var installflags = cookie.f;
2364 if (typeof installflags != 'number') { installflags = 0; }
2365 var showagents = cookie.ag;
2366 if (typeof showagents != 'number') { showagents = 0; }
2367 parent.debug('web', 'handleAgentInviteRequest using cookie.');
2368
2369 // Build the mobile agent URL, this is used to connect mobile devices
2370 var agentServerName = obj.getWebServerName(domain, req);
2371 if (typeof obj.args.agentaliasdns == 'string') { agentServerName = obj.args.agentaliasdns; }
2372 var xdomain = (domain.dns == null) ? domain.id : '';
2373 var agentHttpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
2374 if (obj.args.agentport != null) { agentHttpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
2375 if (obj.args.agentaliasport != null) { agentHttpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
2376 var magenturl = 'mc://' + agentServerName + ((agentHttpsPort != 443) ? (':' + agentHttpsPort) : '') + ((xdomain != '') ? ('/' + xdomain) : '') + ',' + obj.agentCertificateHashBase64 + ',' + mesh._id.split('/')[2];
2377
2378 var meshcookie = parent.encodeCookie({ m: mesh._id.split('/')[2] }, parent.invitationLinkEncryptionKey);
2379 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));
2380 } else if (req.query.m != null) {
2381 // The MeshId is specified in the query string, use that
2382 var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.m.toLowerCase()];
2383 if (mesh == null) { res.sendStatus(404); return; }
2384 var installflags = 0;
2385 if (req.query.f) { installflags = parseInt(req.query.f); }
2386 if (typeof installflags != 'number') { installflags = 0; }
2387 var showagents = 0;
2388 if (req.query.f) { showagents = parseInt(req.query.ag); }
2389 if (typeof showagents != 'number') { showagents = 0; }
2390 parent.debug('web', 'handleAgentInviteRequest using meshid.');
2391
2392 // Build the mobile agent URL, this is used to connect mobile devices
2393 var agentServerName = obj.getWebServerName(domain, req);
2394 if (typeof obj.args.agentaliasdns == 'string') { agentServerName = obj.args.agentaliasdns; }
2395 var xdomain = (domain.dns == null) ? domain.id : '';
2396 var agentHttpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
2397 if (obj.args.agentport != null) { agentHttpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
2398 if (obj.args.agentaliasport != null) { agentHttpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
2399 var magenturl = 'mc://' + agentServerName + ((agentHttpsPort != 443) ? (':' + agentHttpsPort) : '') + ((xdomain != '') ? ('/' + xdomain) : '') + ',' + obj.agentCertificateHashBase64 + ',' + mesh._id.split('/')[2];
2400
2401 var meshcookie = parent.encodeCookie({ m: mesh._id.split('/')[2] }, parent.invitationLinkEncryptionKey);
2402 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));
2403 }
2404 }
2405
2406 // Called to process an agent invite request
2407 function handleUserImageRequest(req, res) {
2408 const domain = getDomain(req);
2409 if (domain == null) { parent.debug('web', 'handleUserImageRequest: failed checks.'); res.sendStatus(404); return; }
2410 if ((req.session == null) || (req.session.userid == null)) { parent.debug('web', 'handleUserImageRequest: failed checks 2.'); res.sendStatus(404); return; }
2411 var imageUserId = req.session.userid;
2412 if ((req.query.id != null)) {
2413 var user = obj.users[req.session.userid];
2414 if ((user == null) || (user.siteadmin == null) && ((user.siteadmin & 2) == 0)) { res.sendStatus(404); return; }
2415 imageUserId = 'user/' + domain.id + '/' + req.query.id;
2416 }
2417 obj.db.Get('im' + imageUserId, function (err, docs) {
2418 if ((err != null) || (docs == null) || (docs.length != 1) || (typeof docs[0].image != 'string')) { res.sendStatus(404); return; }
2419 var imagebase64 = docs[0].image;
2420 if (imagebase64.startsWith('data:image/png;base64,')) {
2421 res.set('Content-Type', 'image/png');
2422 res.set({ 'Cache-Control': 'no-store' });
2423 res.send(Buffer.from(imagebase64.substring(22), 'base64'));
2424 } else if (imagebase64.startsWith('data:image/jpeg;base64,')) {
2425 res.set('Content-Type', 'image/jpeg');
2426 res.set({ 'Cache-Control': 'no-store' });
2427 res.send(Buffer.from(imagebase64.substring(23), 'base64'));
2428 } else {
2429 res.sendStatus(404);
2430 }
2431 });
2432 }
2433
2434 function handleDeleteAccountRequest(req, res, direct) {
2435 parent.debug('web', 'handleDeleteAccountRequest()');
2436 const domain = checkUserIpAddress(req, res);
2437 if (domain == null) { return; }
2438 if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handleDeleteAccountRequest: failed checks.'); res.sendStatus(404); return; }
2439 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2440 if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
2441 if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
2442
2443 var user = null;
2444 if (req.body.authcookie) {
2445 // If a authentication cookie is provided, decode it here
2446 var loginCookie = obj.parent.decodeCookie(req.body.authcookie, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
2447 if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { user = obj.users[loginCookie.userid]; }
2448 } else {
2449 // Check if the user is logged and we have all required parameters
2450 if (!req.session || !req.session.userid || !req.body.apassword1 || (req.body.apassword1 != req.body.apassword2) || (req.session.userid.split('/')[1] != domain.id)) {
2451 parent.debug('web', 'handleDeleteAccountRequest: required parameters not present.');
2452 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2453 return;
2454 } else {
2455 user = obj.users[req.session.userid];
2456 }
2457 }
2458 if (!user) { parent.debug('web', 'handleDeleteAccountRequest: user not found.'); res.sendStatus(404); return; }
2459 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) { parent.debug('web', 'handleDeleteAccountRequest: account settings locked.'); res.sendStatus(404); return; }
2460
2461 // Check if the password is correct
2462 obj.authenticate(user._id.split('/')[2], req.body.apassword1, domain, function (err, userid, passhint, loginOptions) {
2463 var deluser = obj.users[userid];
2464 if ((userid != null) && (deluser != null)) {
2465 // Remove all links to this user
2466 if (deluser.links != null) {
2467 for (var i in deluser.links) {
2468 if (i.startsWith('mesh/')) {
2469 // Get the device group
2470 var mesh = obj.meshes[i];
2471 if (mesh) {
2472 // Remove user from the mesh
2473 if (mesh.links[deluser._id] != null) { delete mesh.links[deluser._id]; parent.db.Set(mesh); }
2474
2475 // Notify mesh change
2476 var change = 'Removed user ' + deluser.name + ' from group ' + mesh.name;
2477 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 };
2478 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.
2479 parent.DispatchEvent(['*', mesh._id, deluser._id, user._id], obj, event);
2480 }
2481 } else if (i.startsWith('node/')) {
2482 // Get the node and the rights for this node
2483 obj.GetNodeWithRights(domain, deluser, i, function (node, rights, visible) {
2484 if ((node == null) || (node.links == null) || (node.links[deluser._id] == null)) return;
2485
2486 // Remove the link and save the node to the database
2487 delete node.links[deluser._id];
2488 if (Object.keys(node.links).length == 0) { delete node.links; }
2489 db.Set(obj.cleanDevice(node));
2490
2491 // Event the node change
2492 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) }
2493 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.
2494 parent.DispatchEvent(['*', node.meshid, node._id], obj, event);
2495 });
2496 } else if (i.startsWith('ugrp/')) {
2497 // Get the device group
2498 var ugroup = obj.userGroups[i];
2499 if (ugroup) {
2500 // Remove user from the user group
2501 if (ugroup.links[deluser._id] != null) { delete ugroup.links[deluser._id]; parent.db.Set(ugroup); }
2502
2503 // Notify user group change
2504 var change = 'Removed user ' + deluser.name + ' from user group ' + ugroup.name;
2505 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 };
2506 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.
2507 parent.DispatchEvent(['*', ugroup._id, user._id, deluser._id], obj, event);
2508 }
2509 }
2510 }
2511 }
2512
2513 obj.db.Remove('ws' + deluser._id); // Remove user web state
2514 obj.db.Remove('nt' + deluser._id); // Remove notes for this user
2515 obj.db.Remove('ntp' + deluser._id); // Remove personal notes for this user
2516 obj.db.Remove('im' + deluser._id); // Remove image for this user
2517
2518 // Delete any login tokens
2519 parent.db.GetAllTypeNodeFiltered(['logintoken-' + deluser._id], domain.id, 'logintoken', null, function (err, docs) {
2520 if ((err == null) && (docs != null)) { for (var i = 0; i < docs.length; i++) { parent.db.Remove(docs[i]._id, function () { }); } }
2521 });
2522
2523 // Delete all files on the server for this account
2524 try {
2525 var deluserpath = obj.getServerRootFilePath(deluser);
2526 if (deluserpath != null) { obj.deleteFolderRec(deluserpath); }
2527 } catch (e) { }
2528
2529 // Remove the user
2530 obj.db.Remove(deluser._id);
2531 delete obj.users[deluser._id];
2532 req.session = null;
2533 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2534 obj.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', userid: deluser._id, username: deluser.name, action: 'accountremove', msg: 'Account removed', domain: domain.id });
2535 parent.debug('web', 'handleDeleteAccountRequest: removed user.');
2536 } else {
2537 parent.debug('web', 'handleDeleteAccountRequest: auth failed.');
2538 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2539 }
2540 });
2541 }
2542
2543 // Check a user's password
2544 obj.checkUserPassword = function (domain, user, password, func) {
2545 // Check the old password
2546 if (user.passtype != null) {
2547 // IIS default clear or weak password hashing (SHA-1)
2548 require('./pass').iishash(user.passtype, password, user.salt, function (err, hash) {
2549 if (err) { parent.debug('web', 'checkUserPassword: SHA-1 fail.'); return func(false); }
2550 if (hash == user.hash) {
2551 if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { parent.debug('web', 'checkUserPassword: SHA-1 locked.'); return func(false); } // Account is locked
2552 parent.debug('web', 'checkUserPassword: SHA-1 ok.');
2553 return func(true); // Allow password change
2554 }
2555 func(false);
2556 });
2557 } else {
2558 // Default strong password hashing (pbkdf2 SHA384)
2559 require('./pass').hash(password, user.salt, function (err, hash, tag) {
2560 if (err) { parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 fail.'); return func(false); }
2561 if (hash == user.hash) {
2562 if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 locked.'); return func(false); } // Account is locked
2563 parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 ok.');
2564 return func(true); // Allow password change
2565 }
2566 func(false);
2567 }, 0);
2568 }
2569 }
2570
2571 // Check a user's old passwords
2572 // Callback: 0=OK, 1=OldPass, 2=CommonPass
2573 obj.checkOldUserPasswords = function (domain, user, password, func) {
2574 // Check how many old passwords we need to check
2575 if ((domain.passwordrequirements != null) && (typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
2576 if (user.oldpasswords != null) {
2577 const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
2578 if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
2579 }
2580 } else {
2581 delete user.oldpasswords;
2582 }
2583
2584 // If there is no old passwords, exit now.
2585 var oldPassCount = 1;
2586 if (user.oldpasswords != null) { oldPassCount += user.oldpasswords.length; }
2587 var oldPassCheckState = { response: 0, count: oldPassCount, user: user, func: func };
2588
2589 // Test against common passwords if this feature is enabled
2590 // Example of common passwords: 123456789, password123
2591 if ((domain.passwordrequirements != null) && (domain.passwordrequirements.bancommonpasswords == true)) {
2592 oldPassCheckState.count++;
2593 require('wildleek')(password).then(function (wild) {
2594 if (wild == true) { oldPassCheckState.response = 2; }
2595 if (--oldPassCheckState.count == 0) { oldPassCheckState.func(oldPassCheckState.response); }
2596 });
2597 }
2598
2599 // Try current password
2600 require('./pass').hash(password, user.salt, function oldPassCheck(err, hash, tag) {
2601 if ((err == null) && (hash == tag.user.hash)) { tag.response = 1; }
2602 if (--tag.count == 0) { tag.func(tag.response); }
2603 }, oldPassCheckState);
2604
2605 // Try each old password
2606 if (user.oldpasswords != null) {
2607 for (var i in user.oldpasswords) {
2608 const oldpassword = user.oldpasswords[i];
2609 // Default strong password hashing (pbkdf2 SHA384)
2610 require('./pass').hash(password, oldpassword.salt, function oldPassCheck(err, hash, tag) {
2611 if ((err == null) && (hash == tag.oldPassword.hash)) { tag.state.response = 1; }
2612 if (--tag.state.count == 0) { tag.state.func(tag.state.response); }
2613 }, { oldPassword: oldpassword, state: oldPassCheckState });
2614 }
2615 }
2616 }
2617
2618 // Handle password changes
2619 function handlePasswordChangeRequest(req, res, direct) {
2620 const domain = checkUserIpAddress(req, res);
2621 if (domain == null) { return; }
2622 if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handlePasswordChangeRequest: failed checks (1).'); res.sendStatus(404); return; }
2623 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2624 if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
2625 if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
2626
2627 // Check if the user is logged and we have all required parameters
2628 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)) {
2629 parent.debug('web', 'handlePasswordChangeRequest: failed checks (2).');
2630 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2631 return;
2632 }
2633
2634 // Get the current user
2635 var user = obj.users[req.session.userid];
2636 if (!user) {
2637 parent.debug('web', 'handlePasswordChangeRequest: user not found.');
2638 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2639 return;
2640 }
2641
2642 // Check account settings locked
2643 if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) {
2644 parent.debug('web', 'handlePasswordChangeRequest: account settings locked.');
2645 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2646 return;
2647 }
2648
2649 // Check old password
2650 obj.checkUserPassword(domain, user, req.body.apassword1, function (result) {
2651 if (result == true) {
2652 // Check if the new password is allowed, only do this if this feature is enabled.
2653 parent.checkOldUserPasswords(domain, user, command.newpass, function (result) {
2654 if (result == 1) {
2655 parent.debug('web', 'handlePasswordChangeRequest: old password reuse attempt.');
2656 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2657 } else if (result == 2) {
2658 parent.debug('web', 'handlePasswordChangeRequest: commonly used password use attempt.');
2659 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2660 } else {
2661 // Update the password
2662 require('./pass').hash(req.body.apassword1, function (err, salt, hash, tag) {
2663 const nowSeconds = Math.floor(Date.now() / 1000);
2664 if (err) { parent.debug('web', 'handlePasswordChangeRequest: hash error.'); throw err; }
2665 if (domain.passwordrequirements != null) {
2666 // Save password hint if this feature is enabled
2667 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; }
2668
2669 // Save previous password if this feature is enabled
2670 if ((typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
2671 if (user.oldpasswords == null) { user.oldpasswords = []; }
2672 user.oldpasswords.push({ salt: user.salt, hash: user.hash, start: user.passchange, end: nowSeconds });
2673 const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
2674 if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
2675 }
2676 }
2677 user.salt = salt;
2678 user.hash = hash;
2679 user.passchange = user.access = nowSeconds;
2680 delete user.passtype;
2681
2682 obj.db.SetUser(user);
2683 req.session.viewmode = 2;
2684 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
2685 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 });
2686 }, 0);
2687 }
2688 });
2689 }
2690 });
2691 }
2692
2693 // Called when a strategy login occurred
2694 // This is called after a successful Oauth to Twitter, Google, GitHub...
2695 function handleStrategyLogin(req, res) {
2696 const domain = checkUserIpAddress(req, res);
2697 if (domain == null) { return; }
2698 if ((req.user != null) && (req.user.sid != null) && (req.user.strategy != null)) {
2699 const strategy = domain.authstrategies[req.user.strategy];
2700 const groups = { 'enabled': typeof strategy.groups == 'object' }
2701 parent.authLog(req.user.strategy.toUpperCase(), `User Authorized: ${JSON.stringify(req.user)}`);
2702 if (groups.enabled) { // Groups only available for OIDC strategy currently
2703 groups.userMemberships = obj.common.convertStrArray(req.user.groups);
2704 groups.syncEnabled = (strategy.groups.sync === true || strategy.groups.sync?.filter) ? true : false;
2705 groups.syncMemberships = [];
2706 groups.siteAdminEnabled = strategy.groups.siteadmin ? true : false;
2707 groups.grantAdmin = false;
2708 groups.revokeAdmin = strategy.groups.revokeAdmin ? strategy.groups.revokeAdmin : true;
2709 groups.requiredGroups = obj.common.convertStrArray(strategy.groups.required);
2710 groups.siteAdmin = obj.common.convertStrArray(strategy.groups.siteadmin);
2711 groups.syncFilter = obj.common.convertStrArray(strategy.groups.sync?.filter);
2712
2713 // Fancy Logs
2714 let groupMessage = '';
2715 if (groups.userMemberships.length == 1) { groupMessage = ` Found membership: "${groups.userMemberships[0]}"` }
2716 else { groupMessage = ` Found ${groups.userMemberships.length} memberships: ["${groups.userMemberships.join('", "')}"]` }
2717 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}"` + groupMessage);
2718
2719 // Check user membership in required groups
2720 if (groups.requiredGroups.length > 0) {
2721 let match = false
2722 for (var i in groups.requiredGroups) {
2723 if (groups.userMemberships.indexOf(groups.requiredGroups[i]) != -1) {
2724 match = true;
2725 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Membership to required group found: "${groups.requiredGroups[i]}"`);
2726 }
2727 }
2728 if (match === false) {
2729 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Login denied. No membership to required group.`);
2730 req.session.loginmode = 1;
2731 req.session.messageid = 111; // Access Denied.
2732 res.redirect(domain.url + getQueryPortion(req));
2733 return;
2734 }
2735 }
2736
2737 // Check user membership in admin groups
2738 if (groups.siteAdminEnabled === true) {
2739 groups.grantAdmin = false;
2740 for (var i in strategy.groups.siteadmin) {
2741 if (groups.userMemberships.indexOf(strategy.groups.siteadmin[i]) >= 0) {
2742 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" User membership found in site admin group: "${strategy.groups.siteadmin[i]}"`);
2743 groups.siteAdmin = strategy.groups.siteadmin[i];
2744 groups.grantAdmin = true;
2745 break;
2746 }
2747 }
2748 }
2749
2750 // Check if we need to sync user-memberships (IdP) with user-groups (meshcentral)
2751 if (groups.syncEnabled === true) {
2752 if (groups.syncFilter.length > 0){ // config.json has specified sync.filter so loop and use it
2753 for (var i in groups.syncFilter) {
2754 if (groups.userMemberships.indexOf(groups.syncFilter[i]) >= 0) { groups.syncMemberships.push(groups.syncFilter[i]); }
2755 }
2756 } else { // config.json doesnt have sync.filter specified so we are going to sync all the users groups from oidc instead
2757 for (var i in groups.userMemberships) {
2758 groups.syncMemberships.push(groups.userMemberships[i]);
2759 }
2760 }
2761 if (groups.syncMemberships.length > 0) {
2762 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" User memberships to sync: ${groups.syncMemberships.join(', ')}`);
2763 } else {
2764 groups.syncMemberships = null;
2765 groups.syncEnabled = false;
2766 if (groups.syncFilter.length > 0){
2767 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" No sync memberships found using filters: ${groups.syncFilter.join(', ')}`);
2768 } else {
2769 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" No sync memberships found`);
2770 }
2771 }
2772 }
2773 }
2774
2775 // Check if the user already exists
2776 const userid = 'user/' + domain.id + '/' + req.user.sid;
2777 var user = obj.users[userid];
2778 if (user == null) {
2779 var newAccountAllowed = false;
2780 var newAccountRealms = null;
2781
2782 if (domain.newaccounts === true) { newAccountAllowed = true; }
2783 if (obj.common.validateStrArray(domain.newaccountrealms)) { newAccountRealms = domain.newaccountrealms; }
2784
2785 if (domain.authstrategies[req.user.strategy]) {
2786 if (domain.authstrategies[req.user.strategy].newaccounts === true) { newAccountAllowed = true; }
2787 if (obj.common.validateStrArray(domain.authstrategies[req.user.strategy].newaccountrealms)) { newAccountRealms = domain.authstrategies[req.user.strategy].newaccountrealms; }
2788 }
2789
2790 if (newAccountAllowed === true) {
2791 // Create the user
2792 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: USER: "${req.user.sid}" Creating new login user: "${userid}"`);
2793 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 };
2794 if (req.user.email != null) { user.email = req.user.email; user.emailVerified = req.user.email_verified ? req.user.email_verified : true; }
2795 if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; } // New accounts automatically assigned server rights.
2796 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.
2797 if (newAccountRealms) { user.groups = newAccountRealms; } // New accounts automatically part of some groups (Realms).
2798 obj.users[userid] = user;
2799
2800 // Auto-join any user groups
2801 var newaccountsusergroups = null;
2802 if (typeof domain.newaccountsusergroups == 'object') { newaccountsusergroups = domain.newaccountsusergroups; }
2803 if (typeof domain.authstrategies[req.user.strategy].newaccountsusergroups == 'object') { newaccountsusergroups = domain.authstrategies[req.user.strategy].newaccountsusergroups; }
2804 if (newaccountsusergroups) {
2805 for (var i in newaccountsusergroups) {
2806 var ugrpid = newaccountsusergroups[i];
2807 if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
2808 var ugroup = obj.userGroups[ugrpid];
2809 if (ugroup != null) {
2810 // Add group to the user
2811 if (user.links == null) { user.links = {}; }
2812 user.links[ugroup._id] = { rights: 1 };
2813
2814 // Add user to the group
2815 ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
2816 db.Set(ugroup);
2817
2818 // Notify user group change
2819 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 };
2820 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.
2821 parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
2822 }
2823 }
2824 }
2825
2826 if (groups.enabled === true) {
2827 // Sync the user groups if enabled
2828 if (groups.syncEnabled === true) {
2829 // Set groupType to the preset name if it exists, otherwise use the strategy name
2830 const groupType = domain.authstrategies[req.user.strategy].custom?.preset ? domain.authstrategies[req.user.strategy].custom.preset : req.user.strategy;
2831 syncExternalUserGroups(domain, user, groups.syncMemberships, groupType);
2832 }
2833 // See if the user is a member of the site admin group.
2834 if (groups.grantAdmin === true) {
2835 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Granting site admin privilages`);
2836 user.siteadmin = 0xFFFFFFFF;
2837 }
2838 }
2839
2840 // Save the user
2841 obj.db.SetUser(user);
2842
2843 // Event user creation
2844 var targets = ['*', 'server-users'];
2845 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 };
2846 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.
2847 parent.DispatchEvent(targets, obj, event);
2848
2849 req.session.userid = userid;
2850 setSessionRandom(req);
2851
2852 // Notify account login using SSO
2853 var targets = ['*', 'server-users', user._id];
2854 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
2855 const ua = obj.getUserAgentInfo(req);
2856 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' };
2857 obj.parent.DispatchEvent(targets, obj, loginEvent);
2858 } else {
2859 // New users not allowed
2860 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: LOGIN FAILED: USER: "${req.user.sid}" New accounts are not allowed`);
2861 req.session.loginmode = 1;
2862 req.session.messageid = 100; // Unable to create account.
2863 res.redirect(domain.url + getQueryPortion(req));
2864 return;
2865 }
2866 } else { // Login success
2867 // Check for basic changes
2868 var userChanged = false;
2869 if ((req.user.name != null) && (req.user.name != user.name)) { user.name = req.user.name; userChanged = true; }
2870 if ((req.user.email != null) && (req.user.email != user.email)) { user.email = req.user.email; user.emailVerified = true; userChanged = true; }
2871
2872 if (groups.enabled === true) {
2873 // Sync the user groups if enabled
2874 if (groups.syncEnabled === true) {
2875 syncExternalUserGroups(domain, user, groups.syncMemberships, req.user.strategy)
2876 }
2877 // See if the user is a member of the site admin group.
2878 if (groups.siteAdminEnabled === true) {
2879 if (groups.grantAdmin === true) {
2880 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Granting site admin privilages`);
2881 if (user.siteadmin !== 0xFFFFFFFF) { user.siteadmin = 0xFFFFFFFF; userChanged = true; }
2882 } else if ((groups.revokeAdmin === true) && (user.siteadmin === 0xFFFFFFFF)) {
2883 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Revoking site admin privilages.`);
2884 delete user.siteadmin;
2885 userChanged = true;
2886 }
2887 }
2888 }
2889
2890 // Update db record for user if there are changes detected
2891 if (userChanged) {
2892 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: CHANGED: USER: "${req.user.sid}" Updating user database entry`);
2893 obj.db.SetUser(user);
2894
2895 // Event user change
2896 var targets = ['*', 'server-users'];
2897 var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Account changed', domain: domain.id };
2898 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.
2899 parent.DispatchEvent(targets, obj, event);
2900 }
2901 req.session.userid = userid;
2902 setSessionRandom(req);
2903
2904 // Notify account login using SSO
2905 var targets = ['*', 'server-users', user._id];
2906 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
2907 const ua = obj.getUserAgentInfo(req);
2908 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' };
2909 obj.parent.DispatchEvent(targets, obj, loginEvent);
2910 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: LOGIN SUCCESS: USER: "${req.user.sid}"`);
2911 }
2912 } else if (req.session && req.session.userid && obj.users[req.session.userid]) {
2913 parent.authLog('handleStrategyLogin', `User Already Authorised "${(req.session.passport && req.session.passport.user) ? req.session.passport.user : req.session.userid }"`);
2914 } else {
2915 parent.authLog('handleStrategyLogin', `LOGIN FAILED: REQUEST CONTAINS NO USER OR SID`);
2916 }
2917 //res.redirect(domain.url); // This does not handle cookie correctly.
2918 res.set('Content-Type', 'text/html');
2919 let url = domain.url;
2920 if (Object.keys(req.query).length > 0) { url += "?" + Object.keys(req.query).map(function(key) { return encodeURIComponent(key) + "=" + encodeURIComponent(req.query[key]); }).join("&"); }
2921
2922 // check for relaystate is set, test against configured server name and accepted query params
2923 if(req.body && req.body.RelayState !== undefined){
2924 var relayState = decodeURIComponent(req.body.RelayState);
2925 var serverName = (obj.getWebServerName(domain, req)).replaceAll('.','\\.');
2926
2927 var regexstr = `(?<=https:\\/\\/(?:.+?\\.)?${serverName}\\/?)` +
2928 `.*((?<=([\\?&])gotodevicename=(.{64})|` +
2929 `gotonode=(.{64})|` +
2930 `gotodeviceip=(((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4})|` +
2931 `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}:)` +
2932 `lang=(.{5})|` +
2933 `sitestyle=(\\d+)|` +
2934 `user=(.{64})|` +
2935 `pass=(.{256})|` +
2936 `key=|` +
2937 `locale=|` +
2938 `gotomesh=(.{64})|` +
2939 `gotouser=(.{0,64})|` +
2940 `gotougrp=(.{64})|` +
2941 `debug=|` +
2942 `filter=|` +
2943 `webrtc=|` +
2944 `hide=|` +
2945 `viewmode=(\\d+)(?=[\\&]|\\b)))`;
2946
2947 var regex = new RegExp(regexstr);
2948 if(regex.test(relayState)){
2949 url = relayState;
2950 }
2951 }
2952
2953 res.end('<html><head><meta http-equiv="refresh" content=0;url="' + url + '"></head><body></body></html>');
2954 }
2955
2956 // Indicates that any request to "/" should render "default" or "login" depending on login state
2957 function handleRootRequest(req, res, direct) {
2958 const domain = checkUserIpAddress(req, res);
2959 if (domain == null) { return; }
2960 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
2961 if (!obj.args) { parent.debug('web', 'handleRootRequest: no obj.args.'); res.sendStatus(500); return; }
2962
2963 // If a HTTP header is required, check new UserRequiredHttpHeader
2964 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; } }
2965
2966 // If the session is expired, clear it.
2967 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]; } }
2968
2969 // Check if we are in maintenance mode
2970 if ((parent.config.settings.maintenancemode != null) && (req.query.loginscreen !== '1')) {
2971 parent.debug('web', 'handleLoginRequest: Server under maintenance.');
2972 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));
2973 return;
2974 }
2975
2976 // If set and there is no user logged in, redirect the root page. Make sure not to redirect if /login is used
2977 if ((typeof domain.unknownuserrootredirect == 'string') && ((req.session == null) || (req.session.userid == null))) {
2978 var q = require('url').parse(req.url, true);
2979 if (!q.pathname.endsWith('/login')) { res.redirect(domain.unknownuserrootredirect + getQueryPortion(req)); return; }
2980 }
2981
2982 if ((domain.sspi != null) && ((req.query.login == null) || (obj.parent.loginCookieEncryptionKey == null))) {
2983 // Login using SSPI
2984 domain.sspi.authenticate(req, res, function (err) {
2985 if ((err != null) || (req.connection.user == null)) {
2986 obj.parent.authLog('https', 'Failed SSPI-auth for ' + req.connection.user + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'] });
2987 parent.debug('web', 'handleRootRequest: SSPI auth required.');
2988 try { res.sendStatus(401); } catch (ex) { } // sspi.authenticate() should already have responded to this request.
2989 } else {
2990 parent.debug('web', 'handleRootRequest: SSPI auth ok.');
2991 handleRootRequestEx(req, res, domain, direct);
2992 }
2993 });
2994 } else if (req.query.user && req.query.pass) {
2995 // User credentials are being passed in the URL. WARNING: Putting credentials in a URL is bad security... but people are requesting this option.
2996 obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid, passhint, loginOptions) {
2997 // 2FA is not supported in URL authentication method. If user has 2FA enabled, this login method fails.
2998 var user = obj.users[userid];
2999 if ((err == null) && checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true) {
3000 handleRootRequestEx(req, res, domain, direct);
3001 } else if ((userid != null) && (err == null)) {
3002 // Login success
3003 parent.debug('web', 'handleRootRequest: user/pass in URL auth ok.');
3004 req.session.userid = userid;
3005 delete req.session.currentNode;
3006 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
3007 setSessionRandom(req);
3008 obj.parent.authLog('https', 'Accepted password for ' + userid + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'], sessionid: req.session.x });
3009 handleRootRequestEx(req, res, domain, direct);
3010 } else {
3011 // Login failed
3012 handleRootRequestEx(req, res, domain, direct);
3013 }
3014 });
3015 } else if ((req.session != null) && (typeof req.session.loginToken == 'string')) {
3016 // Check if the loginToken is still valid
3017 obj.db.Get('logintoken-' + req.session.loginToken, function (err, docs) {
3018 if ((err != null) || (docs == null) || (docs.length != 1) || (docs[0].tokenUser != req.session.loginToken)) { for (var i in req.session) { delete req.session[i]; } }
3019 handleRootRequestEx(req, res, domain, direct); // Login using a different system
3020 });
3021 } else {
3022 // Login using a different system
3023 handleRootRequestEx(req, res, domain, direct);
3024 }
3025 }
3026
3027 function handleRootRequestEx(req, res, domain, direct) {
3028 var nologout = false, user = null;
3029 res.set({ 'Cache-Control': 'no-store' });
3030
3031 // Check if we have an incomplete domain name in the path
3032 if ((domain.id != '') && (domain.dns == null) && (req.url.split('/').length == 2)) {
3033 parent.debug('web', 'handleRootRequestEx: incomplete domain name in the path.');
3034 res.redirect(domain.url + getQueryPortion(req)); // BAD***
3035 return;
3036 }
3037
3038 if (obj.args.nousers == true) {
3039 // If in single user mode, setup things here.
3040 delete req.session.loginmode;
3041 req.session.userid = 'user/' + domain.id + '/~';
3042 delete req.session.currentNode;
3043 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
3044 setSessionRandom(req);
3045 if (obj.users[req.session.userid] == null) {
3046 // Create the dummy user ~ with impossible password
3047 parent.debug('web', 'handleRootRequestEx: created dummy user in nouser mode.');
3048 obj.users[req.session.userid] = { type: 'user', _id: req.session.userid, name: '~', email: '~', domain: domain.id, siteadmin: 4294967295 };
3049 obj.db.SetUser(obj.users[req.session.userid]);
3050 }
3051 } else if (obj.args.user && obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]) {
3052 // If a default user is active, setup the session here.
3053 parent.debug('web', 'handleRootRequestEx: auth using default user.');
3054 delete req.session.loginmode;
3055 req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase();
3056 delete req.session.currentNode;
3057 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
3058 setSessionRandom(req);
3059 } else if (req.query.token && obj.parent.jwtAuth) {
3060 // JWT token authentication (RMM+PSA Integration)
3061 var jwtToken = req.query.token;
3062 obj.parent.jwtAuth.validateToken(jwtToken, function (jwtUser) {
3063 if (jwtUser) {
3064 // JWT authenticated successfully - create session
3065 parent.debug('web', 'handleRootRequestEx: JWT auth ok for ' + jwtUser._id);
3066 delete req.session.loginmode;
3067 req.session.userid = jwtUser._id;
3068 delete req.session.currentNode;
3069 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
3070 setSessionRandom(req);
3071
3072 // Store user in memory if not already present
3073 if (!obj.users[jwtUser._id]) {
3074 obj.users[jwtUser._id] = jwtUser;
3075 }
3076
3077 // Continue with authenticated request
3078 handleRootRequestExAuthenticated(req, res, domain, direct);
3079 } else {
3080 // JWT validation failed
3081 parent.debug('web', 'handleRootRequestEx: JWT auth failed');
3082 handleRootRequestExAuthenticated(req, res, domain, direct);
3083 }
3084 });
3085 return; // Exit here since validateToken is async
3086 } else if (req.query.login && (obj.parent.loginCookieEncryptionKey != null)) {
3087 var loginCookie = obj.parent.decodeCookie(req.query.login, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
3088 //if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // If the cookie is bound to an IP address, check here.
3089 if ((loginCookie != null) && (loginCookie.a == 3) && (loginCookie.u != null) && (loginCookie.u.split('/')[1] == domain.id)) {
3090 // If a login cookie was provided, setup the session here.
3091 parent.debug('web', 'handleRootRequestEx: cookie auth ok.');
3092 delete req.session.loginmode;
3093 req.session.userid = loginCookie.u;
3094 delete req.session.currentNode;
3095 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
3096 setSessionRandom(req);
3097 } else {
3098 parent.debug('web', 'handleRootRequestEx: cookie auth failed.');
3099 }
3100 } else if (domain.sspi != null) {
3101 // SSPI login (Windows only)
3102 //console.log(req.connection.user, req.connection.userSid);
3103 if ((req.connection.user == null) || (req.connection.userSid == null)) {
3104 parent.debug('web', 'handleRootRequestEx: SSPI no user auth.');
3105 res.sendStatus(404); return;
3106 } else {
3107 nologout = true;
3108 req.session.userid = 'user/' + domain.id + '/' + req.connection.user.toLowerCase();
3109 req.session.usersid = req.connection.userSid;
3110 req.session.usersGroups = req.connection.userGroups;
3111 delete req.session.currentNode;
3112 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
3113 setSessionRandom(req);
3114 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 });
3115
3116 // Check if this user exists, create it if not.
3117 user = obj.users[req.session.userid];
3118 if ((user == null) || (user.sid != req.session.usersid)) {
3119 // Create the domain user
3120 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) };
3121 if (domain.newaccountsrights) { user2.siteadmin = domain.newaccountsrights; }
3122 if (obj.common.validateStrArray(domain.newaccountrealms)) { user2.groups = domain.newaccountrealms; }
3123 for (var i in obj.users) { if (obj.users[i].domain == domain.id) { usercount++; } }
3124 if (usercount == 0) { user2.siteadmin = 4294967295; } // If this is the first user, give the account site admin.
3125
3126 // Auto-join any user groups
3127 if (typeof domain.newaccountsusergroups == 'object') {
3128 for (var i in domain.newaccountsusergroups) {
3129 var ugrpid = domain.newaccountsusergroups[i];
3130 if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
3131 var ugroup = obj.userGroups[ugrpid];
3132 if (ugroup != null) {
3133 // Add group to the user
3134 if (user2.links == null) { user2.links = {}; }
3135 user2.links[ugroup._id] = { rights: 1 };
3136
3137 // Add user to the group
3138 ugroup.links[user2._id] = { userid: user2._id, name: user2.name, rights: 1 };
3139 db.Set(ugroup);
3140
3141 // Notify user group change
3142 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 };
3143 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.
3144 parent.DispatchEvent(['*', ugroup._id, user2._id], obj, event);
3145 }
3146 }
3147 }
3148
3149 obj.users[req.session.userid] = user2;
3150 obj.db.SetUser(user2);
3151 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 };
3152 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.
3153 obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
3154 parent.debug('web', 'handleRootRequestEx: SSPI new domain user.');
3155 }
3156 }
3157 }
3158
3159 // Figure out the minimal password requirement
3160 var passRequirements = null;
3161 if (domain.passwordrequirements != null) {
3162 if (domain.passrequirementstr == null) {
3163 var passRequirements = {};
3164 if (typeof domain.passwordrequirements.min == 'number') { passRequirements.min = domain.passwordrequirements.min; }
3165 if (typeof domain.passwordrequirements.max == 'number') { passRequirements.max = domain.passwordrequirements.max; }
3166 if (typeof domain.passwordrequirements.upper == 'number') { passRequirements.upper = domain.passwordrequirements.upper; }
3167 if (typeof domain.passwordrequirements.lower == 'number') { passRequirements.lower = domain.passwordrequirements.lower; }
3168 if (typeof domain.passwordrequirements.numeric == 'number') { passRequirements.numeric = domain.passwordrequirements.numeric; }
3169 if (typeof domain.passwordrequirements.nonalpha == 'number') { passRequirements.nonalpha = domain.passwordrequirements.nonalpha; }
3170 domain.passwordrequirementsstr = encodeURIComponent(JSON.stringify(passRequirements));
3171 }
3172 passRequirements = domain.passwordrequirementsstr;
3173 }
3174
3175 // If a user exists and is logged in, serve the default app, otherwise server the login app.
3176 if (req.session && req.session.userid && obj.users[req.session.userid]) {
3177 const user = obj.users[req.session.userid];
3178
3179 // Check if we are in maintenance mode
3180 if ((parent.config.settings.maintenancemode != null) && (user.siteadmin != 4294967295)) {
3181 req.session.messageid = 115; // Server under maintenance
3182 req.session.loginmode = 1;
3183 res.redirect(domain.url);
3184 return;
3185 }
3186
3187 // If the request has a "meshmessengerid", redirect to MeshMessenger
3188 // This situation happens when you get a push notification for a chat session, but are not logged in.
3189 if (req.query.meshmessengerid != null) {
3190 res.redirect(domain.url + 'messenger?id=' + encodeURIComponent(req.query.meshmessengerid) + ((req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : ''));
3191 return;
3192 }
3193
3194 const xdbGetFunc = function dbGetFunc(err, states) {
3195 if (dbGetFunc.req.session.userid.split('/')[1] != domain.id) { // Check if the session is for the correct domain
3196 parent.debug('web', 'handleRootRequestEx: incorrect domain.');
3197 dbGetFunc.req.session = null;
3198 dbGetFunc.res.redirect(domain.url + getQueryPortion(dbGetFunc.req)); // BAD***
3199 return;
3200 }
3201
3202 // Check if this is a locked account
3203 if ((dbGetFunc.user.siteadmin != null) && ((dbGetFunc.user.siteadmin & 32) != 0) && (dbGetFunc.user.siteadmin != 0xFFFFFFFF)) {
3204 // Locked account
3205 parent.debug('web', 'handleRootRequestEx: locked account.');
3206 delete dbGetFunc.req.session.userid;
3207 delete dbGetFunc.req.session.currentNode;
3208 delete dbGetFunc.req.session.passhint;
3209 delete dbGetFunc.req.session.cuserid;
3210 dbGetFunc.req.session.messageid = 110; // Account locked.
3211 dbGetFunc.res.redirect(domain.url + getQueryPortion(dbGetFunc.req)); // BAD***
3212 return;
3213 }
3214
3215 var viewmode = 1;
3216 if (dbGetFunc.req.session.viewmode) {
3217 viewmode = dbGetFunc.req.session.viewmode;
3218 delete dbGetFunc.req.session.viewmode;
3219 } else if (dbGetFunc.req.query.viewmode) {
3220 viewmode = dbGetFunc.req.query.viewmode;
3221 }
3222 var currentNode = '';
3223 if (dbGetFunc.req.session.currentNode) {
3224 currentNode = dbGetFunc.req.session.currentNode;
3225 delete dbGetFunc.req.session.currentNode;
3226 } else if (dbGetFunc.req.query.node) {
3227 currentNode = 'node/' + domain.id + '/' + dbGetFunc.req.query.node;
3228 }
3229 var logoutcontrols = {};
3230 if (obj.args.nousers != true) { logoutcontrols.name = user.name; }
3231
3232 // Give the web page a list of supported server features for this domain and user
3233 const allFeatures = obj.getDomainUserFeatures(domain, dbGetFunc.user, dbGetFunc.req);
3234
3235 // Create a authentication cookie
3236 const authCookie = obj.parent.encodeCookie({ userid: dbGetFunc.user._id, domainid: domain.id, ip: req.clientIp }, obj.parent.loginCookieEncryptionKey);
3237 const authRelayCookie = obj.parent.encodeCookie({ ruserid: dbGetFunc.user._id, x: req.session.x }, obj.parent.loginCookieEncryptionKey);
3238
3239 // Send the main web application
3240 var extras = (dbGetFunc.req.query.key != null) ? ('&key=' + dbGetFunc.req.query.key) : '';
3241 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
3242 var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
3243
3244 // Clean up the U2F challenge if needed
3245 if (dbGetFunc.req.session.u2f) { delete dbGetFunc.req.session.u2f; };
3246 if (dbGetFunc.req.session.e) {
3247 const sec = parent.decryptSessionData(dbGetFunc.req.session.e);
3248 if (sec.u2f != null) { delete sec.u2f; dbGetFunc.req.session.e = parent.encryptSessionData(sec); }
3249 }
3250
3251 // Intel AMT Scanning options
3252 var amtscanoptions = '';
3253 if (typeof domain.amtscanoptions == 'string') { amtscanoptions = encodeURIComponent(domain.amtscanoptions); }
3254 else if (obj.common.validateStrArray(domain.amtscanoptions)) { domain.amtscanoptions = domain.amtscanoptions.join(','); amtscanoptions = encodeURIComponent(domain.amtscanoptions); }
3255
3256 // Fetch the web state
3257 parent.debug('web', 'handleRootRequestEx: success.');
3258
3259 var webstate = '{}';
3260 if ((err == null) && (states != null) && (Array.isArray(states)) && (states.length == 1) && (states[0].state != null)) { webstate = obj.filterUserWebState(states[0].state); }
3261 if ((webstate == '{}') && (typeof domain.defaultuserwebstate == 'object')) { webstate = JSON.stringify(domain.defaultuserwebstate); } // User has no web state, use defaults.
3262 if (typeof domain.forceduserwebstate == 'object') { // Forces initial user web state if present, use it.
3263 var webstate2 = {};
3264 try { if (webstate != '{}') { webstate2 = JSON.parse(webstate); } } catch (ex) { }
3265 for (var i in domain.forceduserwebstate) { webstate2[i] = domain.forceduserwebstate[i]; }
3266 webstate = JSON.stringify(webstate2);
3267 }
3268
3269 // Custom user interface
3270 var customui = '';
3271 if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); }
3272
3273 // Custom files (CSS and JS)
3274 var customFiles = '';
3275 if (domain.customFiles != null) {
3276 customFiles = encodeURIComponent(JSON.stringify(domain.customFiles));
3277 } else if (domain.customfiles != null) {
3278 customFiles = encodeURIComponent(JSON.stringify(domain.customfiles));
3279 }
3280
3281 // Server features
3282 var serverFeatures = 255;
3283 if (domain.myserver === false) { serverFeatures = 0; } // 64 = Show "My Server" tab
3284 else if (typeof domain.myserver == 'object') {
3285 if (domain.myserver.backup !== true) { serverFeatures -= 1; } // Disallow simple server backups
3286 if (domain.myserver.restore !== true) { serverFeatures -= 2; } // Disallow simple server restore
3287 if (domain.myserver.upgrade !== true) { serverFeatures -= 4; } // Disallow server upgrade
3288 if (domain.myserver.errorlog !== true) { serverFeatures -= 8; } // Disallow show server crash log
3289 if (domain.myserver.console !== true) { serverFeatures -= 16; } // Disallow server console
3290 if (domain.myserver.trace !== true) { serverFeatures -= 32; } // Disallow server tracing
3291 if (domain.myserver.config !== true) { serverFeatures -= 128; } // Disallow server configuration
3292 }
3293 if (obj.db.databaseType != 1) { // If not using NeDB, we can't backup using the simple system.
3294 if ((serverFeatures & 1) != 0) { serverFeatures -= 1; } // Disallow server backups
3295 if ((serverFeatures & 2) != 0) { serverFeatures -= 2; } // Disallow simple server restore
3296 }
3297
3298 // Get WebRTC configuration
3299 var webRtcConfig = null;
3300 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'); }
3301 else if (args.webrtcconfig && (typeof args.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(args.webrtcconfig)).replace(/'/g, '%27'); }
3302
3303 // Load default page style or new modern ui
3304 var uiViewMode = 'default';
3305 var webstateJSON = JSON.parse(webstate);
3306 if (req.query.sitestyle != null) {
3307 if (req.query.sitestyle == 3) { uiViewMode = 'default3'; }
3308 } else if (webstateJSON && webstateJSON.uiViewMode == 3) {
3309 uiViewMode = 'default3';
3310 } else if (domain.sitestyle == 3) {
3311 uiViewMode = 'default3';
3312 }
3313 // Refresh the session
3314 render(dbGetFunc.req, dbGetFunc.res, getRenderPage(uiViewMode, dbGetFunc.req, domain), getRenderArgs({
3315 authCookie: authCookie,
3316 authRelayCookie: authRelayCookie,
3317 viewmode: viewmode,
3318 currentNode: currentNode,
3319 logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27'),
3320 domain: domain.id,
3321 debuglevel: parent.debugLevel,
3322 serverDnsName: obj.getWebServerName(domain, req),
3323 serverRedirPort: args.redirport,
3324 serverPublicPort: httpsPort,
3325 serverfeatures: serverFeatures,
3326 features: allFeatures.features,
3327 features2: allFeatures.features2,
3328 sessiontime: (args.sessiontime) ? args.sessiontime : 60,
3329 mpspass: args.mpspass,
3330 passRequirements: passRequirements,
3331 customui: customui,
3332 customFiles: customFiles,
3333 webcerthash: Buffer.from(obj.webCertificateFullHashs[domain.id], 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'),
3334 footer: (domain.footer == null) ? '' : obj.common.replacePlaceholders(domain.footer, {
3335 'serverversion': obj.parent.currentVer,
3336 'servername': obj.getWebServerName(domain, req),
3337 'agentsessions': Object.keys(parent.webserver.wsagents).length,
3338 'connectedusers': Object.keys(parent.webserver.wssessions).length,
3339 'userssessions': Object.keys(parent.webserver.wssessions2).length,
3340 'relaysessions': parent.webserver.relaySessionCount,
3341 'relaycount': Object.keys(parent.webserver.wsrelays).length
3342 }),
3343 webstate: encodeURIComponent(webstate).replace(/'/g, '%27'),
3344 amtscanoptions: amtscanoptions,
3345 pluginHandler: (parent.pluginHandler == null) ? 'null' : parent.pluginHandler.prepExports(),
3346 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)),
3347 webRelayDns: ((args.relaydns != null) ? args.relaydns[0] : ''),
3348 hidePowerTimeline: (domain.hidepowertimeline ? 'true' : 'false'),
3349 showNotesPanel: (domain.shownotespanel ? 'true' : 'false'),
3350 userSessionsSort: (domain.usersessionssort ? domain.usersessionssort : 'SessionId'),
3351 webrtcconfig: webRtcConfig,
3352 collapseGroups: (domain.collapsegroups ? 'true' : 'false')
3353 }, dbGetFunc.req, domain, uiViewMode), user);
3354 }
3355 xdbGetFunc.req = req;
3356 xdbGetFunc.res = res;
3357 xdbGetFunc.user = user;
3358 obj.db.Get('ws' + user._id, xdbGetFunc);
3359 } else {
3360 // Send back the login application
3361 // If this is a 2 factor auth request, look for a hardware key challenge.
3362 // Normal login 2 factor request
3363 if (req.session && (req.session.loginmode == 4)) {
3364 const sec = parent.decryptSessionData(req.session.e);
3365 if ((sec != null) && (typeof sec.tuserid == 'string')) {
3366 const user = obj.users[sec.tuserid];
3367 if (user != null) {
3368 parent.debug('web', 'handleRootRequestEx: sending 2FA challenge.');
3369 getHardwareKeyChallenge(req, domain, user, function (hwchallenge) { handleRootRequestLogin(req, res, domain, hwchallenge, passRequirements); });
3370 return;
3371 }
3372 }
3373 }
3374 // Password recovery 2 factor request
3375 if (req.session && (req.session.loginmode == 5) && (req.session.temail)) {
3376 obj.db.GetUserWithVerifiedEmail(domain.id, req.session.temail, function (err, docs) {
3377 if ((err != null) || (docs.length == 0)) {
3378 parent.debug('web', 'handleRootRequestEx: password recover 2FA fail.');
3379 req.session = null;
3380 res.redirect(domain.url + getQueryPortion(req)); // BAD***
3381 } else {
3382 var user = obj.users[docs[0]._id];
3383 if (user != null) {
3384 parent.debug('web', 'handleRootRequestEx: password recover 2FA challenge.');
3385 getHardwareKeyChallenge(req, domain, user, function (hwchallenge) { handleRootRequestLogin(req, res, domain, hwchallenge, passRequirements); });
3386 } else {
3387 parent.debug('web', 'handleRootRequestEx: password recover 2FA no user.');
3388 req.session = null;
3389 res.redirect(domain.url + getQueryPortion(req)); // BAD***
3390 }
3391 }
3392 });
3393 return;
3394 }
3395 handleRootRequestLogin(req, res, domain, '', passRequirements);
3396 }
3397 }
3398
3399 // Return a list of server supported features for a given domain and user
3400 obj.getDomainUserFeatures = function (domain, user, req) {
3401 var features = 0;
3402 var features2 = 0;
3403 if (obj.args.wanonly == true) { features += 0x00000001; } // WAN-only mode
3404 if (obj.args.lanonly == true) { features += 0x00000002; } // LAN-only mode
3405 if (obj.args.nousers == true) { features += 0x00000004; } // Single user mode
3406 if (domain.userQuota == -1) { features += 0x00000008; } // No server files mode
3407 if (obj.args.mpstlsoffload) { features += 0x00000010; } // No mutual-auth CIRA
3408 if ((parent.config.settings.allowframing != null) || (domain.allowframing != null)) { features += 0x00000020; } // Allow site within iframe
3409 if ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true)) { features += 0x00000040; } // Email invites
3410 if (obj.args.webrtc == true) { features += 0x00000080; } // Enable WebRTC (Default false for now)
3411 // 0x00000100 --> This feature flag is free for future use.
3412 if (obj.args.allowhighqualitydesktop !== false) { features += 0x00000200; } // Enable AllowHighQualityDesktop (Default true)
3413 if ((obj.args.lanonly == true) || (obj.args.mpsport == 0)) { features += 0x00000400; } // No CIRA
3414 if ((obj.parent.serverSelfWriteAllowed == true) && (user != null) && ((user.siteadmin & 0x00000010) != 0)) { features += 0x00000800; } // Server can self-write (Allows self-update)
3415 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
3416 if (domain.agentnoproxy === true) { features += 0x00002000; } // Indicates that agents should be installed without using a HTTP proxy
3417 if ((parent.config.settings.no2factorauth !== true) && domain.yubikey && domain.yubikey.id && domain.yubikey.secret && (user._id.split('/')[2][0] != '~')) { features += 0x00004000; } // Indicates Yubikey support
3418 if (domain.geolocation == true) { features += 0x00008000; } // Enable geo-location features
3419 if ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true)) { features += 0x00010000; } // Enable password hints
3420 if (parent.config.settings.no2factorauth !== true) { features += 0x00020000; } // Enable WebAuthn/FIDO2 support
3421 if ((obj.args.nousers != true) && (domain.passwordrequirements != null) && (domain.passwordrequirements.force2factor === true) && (user._id.split('/')[2][0] != '~')) {
3422 // Check if we can skip 2nd factor auth because of the source IP address
3423 var skip2factor = false;
3424 if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
3425 for (var i in domain.passwordrequirements.skip2factor) {
3426 if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) { skip2factor = true; }
3427 }
3428 }
3429 if (skip2factor == false) { features += 0x00040000; } // Force 2-factor auth
3430 }
3431 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.
3432 if (domain.amtacmactivation) { features += 0x00100000; } // Intel AMT ACM activation/upgrade is possible
3433 if (domain.usernameisemail) { features += 0x00200000; } // Username is email address
3434 if (parent.mqttbroker != null) { features += 0x00400000; } // This server supports MQTT channels
3435 if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null)) { features += 0x00800000; } // using email for 2FA is allowed
3436 if (domain.agentinvitecodes == true) { features += 0x01000000; } // Support for agent invite codes
3437 if (parent.smsserver != null) { features += 0x02000000; } // SMS messaging is supported
3438 if ((parent.smsserver != null) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false))) { features += 0x04000000; } // SMS 2FA is allowed
3439 if (domain.sessionrecording != null) { features += 0x08000000; } // Server recordings enabled
3440 if (domain.urlswitching === false) { features += 0x10000000; } // Disables the URL switching feature
3441 if (domain.novnc === false) { features += 0x20000000; } // Disables noVNC
3442 if (domain.mstsc === false) { features += 0x40000000; } // Disables MSTSC.js
3443 if (obj.isTrustedCert(domain) == false) { features += 0x80000000; } // Indicate we are not using a trusted certificate
3444 if (obj.parent.amtManager != null) { features2 += 0x00000001; } // Indicates that the Intel AMT manager is active
3445 if (obj.parent.firebase != null) { features2 += 0x00000002; } // Indicates the server supports Firebase push messaging
3446 if ((obj.parent.firebase != null) && (obj.parent.firebase.pushOnly != true)) { features2 += 0x00000004; } // Indicates the server supports Firebase two-way push messaging
3447 if (obj.parent.webpush != null) { features2 += 0x00000008; } // Indicates web push is enabled
3448 if (((obj.args.noagentupdate == 1) || (obj.args.noagentupdate == true))) { features2 += 0x00000010; } // No agent update
3449 if (parent.amtProvisioningServer != null) { features2 += 0x00000020; } // Intel AMT LAN provisioning server
3450 if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.push2factor != false)) && (obj.parent.firebase != null)) { features2 += 0x00000040; } // Indicates device push notification 2FA is enabled
3451 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
3452 if (req.session.loginToken != null) { features2 += 0x00000100; } // LoginToken mode, no account changes.
3453 if (domain.ssh == true) { features2 += 0x00000200; } // SSH is enabled
3454 if (domain.localsessionrecording === false) { features2 += 0x00000400; } // Disable local recording feature
3455 if (domain.clipboardget == false) { features2 += 0x00000800; } // Disable clipboard get
3456 if (domain.clipboardset == false) { features2 += 0x00001000; } // Disable clipboard set
3457 if ((typeof domain.desktop == 'object') && (domain.desktop.viewonly == true)) { features2 += 0x00002000; } // Indicates remote desktop is viewonly
3458 if (domain.mailserver != null) { features2 += 0x00004000; } // Indicates email server is active
3459 if (domain.devicesearchbarserverandclientname) { features2 += 0x00008000; } // Search bar will find both server name and client name
3460 if (domain.ipkvm) { features2 += 0x00010000; } // Indicates support for IP KVM device groups
3461 if ((domain.passwordrequirements) && (domain.passwordrequirements.otp2factor == false)) { features2 += 0x00020000; } // Indicates support for OTP 2FA is disabled
3462 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.backupcode2factor === false)) { features2 += 0x00040000; } // Indicates 2FA backup codes are disabled
3463 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.single2factorwarning === false)) { features2 += 0x00080000; } // Indicates no warning if a single 2FA is in use
3464 if (domain.nightmode === 1) { features2 += 0x00100000; } // Always night mode
3465 if (domain.nightmode === 2) { features2 += 0x00200000; } // Always day mode
3466 if (domain.allowsavingdevicecredentials == false) { features2 += 0x00400000; } // Do not allow device credentials to be saved on the server
3467 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
3468 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
3469 if ((parent.msgserver != null) && (parent.msgserver.providers != 0)) { features2 += 0x02000000; } // User messaging server is enabled
3470 if ((parent.msgserver != null) && (parent.msgserver.providers != 0) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false))) { features2 += 0x04000000; } // User messaging 2FA is allowed
3471 if (domain.scrolltotop == true) { features2 += 0x08000000; } // Show the "Scroll to top" button
3472 if (domain.devicesearchbargroupname === true) { features2 += 0x10000000; } // Search bar will find by group name too
3473 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
3474 if (domain.showmodernuitoggle == true) { features2 += 0x40000000; } // Indicates that the new UI should be shown
3475 if (domain.sitestyle === 3) { features2 |= 0x80000000; } // Indicates that Modern UI is forced (siteStyle = 3)
3476 return { features: features, features2: features2 };
3477 }
3478
3479 function handleRootRequestLogin(req, res, domain, hardwareKeyChallenge, passRequirements) {
3480 parent.debug('web', 'handleRootRequestLogin()');
3481
3482 // JWT authentication is handled by middleware - allow login page to render
3483 // The page will be accessible but actual authentication requires JWT
3484
3485 var features = 0;
3486 if ((parent.config != null) && (parent.config.settings != null) && ((parent.config.settings.allowframing == true) || (typeof parent.config.settings.allowframing == 'string'))) { features += 32; } // Allow site within iframe
3487 if (domain.usernameisemail) { features += 0x00200000; } // Username is email address
3488 var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
3489 var loginmode = 0;
3490 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.
3491
3492 // Format an error message if needed
3493 var passhint = null, msgid = 0;
3494 if (req.session != null) {
3495 msgid = req.session.messageid;
3496 if ((msgid == 5) || (loginmode == 7) || ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true))) { passhint = EscapeHtml(req.session.passhint); }
3497 delete req.session.messageid;
3498 delete req.session.passhint;
3499 }
3500 const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false));
3501 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'))
3502
3503 // Check if we are allowed to create new users using the login screen
3504 var newAccountsAllowed = true;
3505 if ((domain.newaccounts !== 1) && (domain.newaccounts !== true)) { for (var i in obj.users) { if (obj.users[i].domain == domain.id) { newAccountsAllowed = false; break; } } }
3506 if (parent.config.settings.maintenancemode != null) { newAccountsAllowed = false; }
3507
3508 // Encrypt the hardware key challenge state if needed
3509 var hwstate = null;
3510 if (hardwareKeyChallenge && req.session) {
3511 const sec = parent.decryptSessionData(req.session.e);
3512 hwstate = obj.parent.encodeCookie({ u: sec.tuser, p: sec.tpass, c: sec.u2f }, obj.parent.loginCookieEncryptionKey)
3513 }
3514
3515 // Check if we can use OTP tokens with email. We can't use email for 2FA password recovery (loginmode 5).
3516 var otpemail = (loginmode != 5) && (domain.mailserver != null) && (req.session != null) && ((req.session.temail === 1) || (typeof req.session.temail == 'string'));
3517 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; }
3518 var otpduo = (req.session != null) && (req.session.tduo === 1);
3519 if (((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.duo2factor == false)) || (typeof domain.duo2factor != 'object')) { otpduo = false; }
3520 var otpsms = (parent.smsserver != null) && (req.session != null) && (req.session.tsms === 1);
3521 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; }
3522 var otpmsg = (parent.msgserver != null) && (req.session != null) && (req.session.tmsg === 1);
3523 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.msg2factor == false)) { otpmsg = false; }
3524 var otppush = (parent.firebase != null) && (req.session != null) && (req.session.tpush === 1);
3525 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.push2factor == false)) { otppush = false; }
3526 const autofido = ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.autofido2fa == true)); // See if FIDO should be automatically prompted if user account has it.
3527
3528 // See if we support two-factor trusted cookies
3529 var twoFactorCookieDays = 30;
3530 if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
3531
3532 // See what authentication strategies we have
3533 var authStrategies = [];
3534 if (typeof domain.authstrategies == 'object') {
3535 if (typeof domain.authstrategies.twitter == 'object') { authStrategies.push('twitter'); }
3536 if (typeof domain.authstrategies.google == 'object') { authStrategies.push('google'); }
3537 if (typeof domain.authstrategies.github == 'object') { authStrategies.push('github'); }
3538 if (typeof domain.authstrategies.azure == 'object') { authStrategies.push('azure'); }
3539 if (typeof domain.authstrategies.oidc == 'object') {
3540 if (obj.common.validateObject(domain.authstrategies.oidc.custom) && obj.common.validateString(domain.authstrategies.oidc.custom.preset)) {
3541 authStrategies.push('oidc-' + domain.authstrategies.oidc.custom.preset);
3542 } else {
3543 authStrategies.push('oidc');
3544 }
3545 }
3546 if (typeof domain.authstrategies.intel == 'object') { authStrategies.push('intel'); }
3547 if (typeof domain.authstrategies.jumpcloud == 'object') { authStrategies.push('jumpcloud'); }
3548 if (typeof domain.authstrategies.saml == 'object') { authStrategies.push('saml'); }
3549 }
3550
3551 // Custom user interface
3552 var customui = '';
3553 if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); }
3554
3555 // Custom files (CSS and JS)
3556 var customFiles = '';
3557 if (domain.customFiles != null) {
3558 customFiles = encodeURIComponent(JSON.stringify(domain.customFiles));
3559 } else if (domain.customfiles != null) {
3560 customFiles = encodeURIComponent(JSON.stringify(domain.customfiles));
3561 }
3562
3563 // Get two-factor screen timeout
3564 var twoFactorTimeout = 300000; // Default is 5 minutes, 0 for no timeout.
3565 if ((typeof domain.passwordrequirements == 'object') && (typeof domain.passwordrequirements.twofactortimeout == 'number')) {
3566 twoFactorTimeout = domain.passwordrequirements.twofactortimeout * 1000;
3567 }
3568
3569 // Setup CAPTCHA if needed
3570 var newAccountCaptcha = '', newAccountCaptchaImage = '';
3571 if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
3572 newAccountCaptcha = obj.parent.encodeCookie({ type: 'newAccount', captcha: require('svg-captcha').randomText(5) }, obj.parent.loginCookieEncryptionKey);
3573 newAccountCaptchaImage = 'newAccountCaptcha.ashx?x=' + newAccountCaptcha;
3574 }
3575
3576 // Check for flash errors from passport.js and make the array unique
3577 var flashErrors = [];
3578 if (req.session.flash && req.session.flash.error) {
3579 flashErrors = obj.common.uniqueArray(req.session.flash.error);
3580 req.session.flash = null;
3581 }
3582
3583 // Render the login page
3584 render(req, res,
3585 getRenderPage((domain.sitestyle >= 2) ? 'login2' : 'login', req, domain),
3586 getRenderArgs({
3587 loginmode: loginmode,
3588 rootCertLink: getRootCertLink(domain),
3589 newAccount: newAccountsAllowed, // True if new accounts are allowed from the login page
3590 newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1), // 1 if new account creation requires password
3591 newAccountCaptcha: newAccountCaptcha, // If new account creation requires a CAPTCHA, this string will not be empty
3592 newAccountCaptchaImage: newAccountCaptchaImage, // Set to the URL of the CAPTCHA image
3593 serverDnsName: obj.getWebServerName(domain, req),
3594 serverPublicPort: httpsPort,
3595 passlogin: (typeof domain.showpasswordlogin == 'boolean') ? domain.showpasswordlogin : true,
3596 emailcheck: emailcheck,
3597 features: features,
3598 sessiontime: (args.sessiontime) ? args.sessiontime : 60, // Session time in minutes, 60 minutes is the default
3599 passRequirements: passRequirements,
3600 customui: customui,
3601 customFiles: customFiles,
3602 footer: (domain.loginfooter == null) ? '' : obj.common.replacePlaceholders(domain.loginfooter, {
3603 'serverversion': obj.parent.currentVer,
3604 'servername': obj.getWebServerName(domain, req),
3605 'agentsessions': Object.keys(parent.webserver.wsagents).length,
3606 'connectedusers': Object.keys(parent.webserver.wssessions).length,
3607 'userssessions': Object.keys(parent.webserver.wssessions2).length,
3608 'relaysessions': parent.webserver.relaySessionCount,
3609 'relaycount': Object.keys(parent.webserver.wsrelays).length
3610 }),
3611 hkey: encodeURIComponent(hardwareKeyChallenge).replace(/'/g, '%27'),
3612 messageid: msgid,
3613 flashErrors: JSON.stringify(flashErrors).replace(/"/g, '\\"'),
3614 passhint: passhint,
3615
3616 welcometext: domain.welcometext ? encodeURIComponent(obj.common.replacePlaceholders(domain.welcometext, {
3617 'serverversion': obj.parent.currentVer,
3618 'servername': obj.getWebServerName(domain, req),
3619 'agentsessions': Object.keys(parent.webserver.wsagents).length,
3620 'connectedusers': Object.keys(parent.webserver.wssessions).length,
3621 'userssessions': Object.keys(parent.webserver.wssessions2).length,
3622 'relaysessions': parent.webserver.relaySessionCount,
3623 'relaycount': Object.keys(parent.webserver.wsrelays).length
3624 })).split('\'').join('\\\'') : null,
3625 welcomePictureFullScreen: ((typeof domain.welcomepicturefullscreen == 'boolean') ? domain.welcomepicturefullscreen : false),
3626 hwstate: hwstate,
3627 otpemail: otpemail,
3628 otpduo: otpduo,
3629 otpsms: otpsms,
3630 otpmsg: otpmsg,
3631 otppush: otppush,
3632 autofido: autofido,
3633 twoFactorCookieDays: twoFactorCookieDays,
3634 authStrategies: authStrategies.join(','),
3635 loginpicture: (typeof domain.loginpicture == 'string'),
3636 tokenTimeout: twoFactorTimeout, // Two-factor authentication screen timeout in milliseconds,
3637 renderLanguages: obj.renderLanguages,
3638 showLanguageSelect: domain.showlanguageselect ? domain.showlanguageselect : false,
3639 }, req, domain, (domain.sitestyle >= 2) ? 'login2' : 'login'));
3640 }
3641
3642 // Handle a post request on the root
3643 function handleRootPostRequest(req, res) {
3644 const domain = checkUserIpAddress(req, res);
3645 if (domain == null) { return; }
3646 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.end("Not Found"); return; } // Check 3FA URL key
3647 if (req.body == null) { req.body = {}; }
3648 parent.debug('web', 'handleRootPostRequest, action: ' + req.body.action);
3649
3650 // If a HTTP header is required, check new UserRequiredHttpHeader
3651 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; } }
3652
3653 switch (req.body.action) {
3654 case 'login': { handleLoginRequest(req, res, true); break; }
3655 case 'tokenlogin': {
3656 if (req.body.hwstate) {
3657 var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 10);
3658 if (cookie != null) { req.session.e = parent.encryptSessionData({ tuser: cookie.u, tpass: cookie.p, u2f: cookie.c }); }
3659 }
3660 handleLoginRequest(req, res, true); break;
3661 }
3662 case 'pushlogin': {
3663 if (req.body.hwstate) {
3664 var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 1);
3665 if ((cookie != null) && (typeof cookie.u == 'string') && (cookie.d == domain.id) && (cookie.a == 'pushAuth')) {
3666 // Push authentication is a success, login the user
3667 req.session = { userid: cookie.u };
3668
3669 // Check if we need to remember this device
3670 if ((req.body.remembertoken === 'on') && ((domain.twofactorcookiedurationdays == null) || (domain.twofactorcookiedurationdays > 0))) {
3671 var maxCookieAge = domain.twofactorcookiedurationdays;
3672 if (typeof maxCookieAge != 'number') { maxCookieAge = 30; }
3673 const twoFactorCookie = obj.parent.encodeCookie({ userid: cookie.u, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey);
3674 res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: parent.config.settings.sessionsamesite, secure: true });
3675 }
3676 var user = obj.users[cookie.u];
3677 // Notify account login
3678 var targets = ['*', 'server-users', user._id];
3679 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
3680 const ua = obj.getUserAgentInfo(req);
3681 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' };
3682 obj.parent.DispatchEvent(targets, obj, loginEvent);
3683 handleRootRequestEx(req, res, domain);
3684 return;
3685 }
3686 }
3687 handleLoginRequest(req, res, true); break;
3688 }
3689 case 'changepassword': { handlePasswordChangeRequest(req, res, true); break; }
3690 case 'deleteaccount': { handleDeleteAccountRequest(req, res, true); break; }
3691 case 'createaccount': { handleCreateAccountRequest(req, res, true); break; }
3692 case 'resetpassword': { handleResetPasswordRequest(req, res, true); break; }
3693 case 'resetaccount': { handleResetAccountRequest(req, res, true); break; }
3694 case 'checkemail': { handleCheckAccountEmailRequest(req, res, true); break; }
3695 default: { handleLoginRequest(req, res, true); break; }
3696 }
3697 }
3698
3699 // Return true if it looks like we are using a real TLS certificate.
3700 obj.isTrustedCert = function (domain) {
3701 if ((domain != null) && (typeof domain.trustedcert == 'boolean')) return domain.trustedcert; // If the status of the cert specified, use that.
3702 if (typeof obj.args.trustedcert == 'boolean') return obj.args.trustedcert; // If the status of the cert specified, use that.
3703 if (obj.args.tlsoffload != null) return true; // We are using TLS offload, a real cert is likely used.
3704 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.
3705 if ((typeof obj.certificates.WebIssuer == 'string') && (obj.certificates.WebIssuer.indexOf('MeshCentralRoot-') == 0)) return false; // Our cert is issued by self-signed cert.
3706 if (obj.certificates.CommonName.indexOf('.') == -1) return false; // Our cert is named with a fake name
3707 return true; // This is a guess
3708 }
3709
3710 // Get the link to the root certificate if needed
3711 function getRootCertLink(domain) {
3712 // Check if the HTTPS certificate is issued from MeshCentralRoot, if so, add download link to root certificate.
3713 if (obj.isTrustedCert(domain) == false) {
3714 // Get the domain suffix
3715 var xdomain = (domain.dns == null) ? domain.id : '';
3716 if (xdomain != '') xdomain += '/';
3717 return '<a href=/' + xdomain + 'MeshServerRootCert.cer title="Download the root certificate for this server">Root Certificate</a>';
3718 }
3719 return '';
3720 }
3721
3722 // Serve the xterm page
3723 function handleXTermRequest(req, res) {
3724 const domain = checkUserIpAddress(req, res);
3725 if (domain == null) { return; }
3726 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
3727
3728 parent.debug('web', 'handleXTermRequest: sending xterm');
3729 res.set({ 'Cache-Control': 'no-store' });
3730 if (req.session && req.session.userid) {
3731 if (req.session.userid.split('/')[1] != domain.id) { res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain
3732 var user = obj.users[req.session.userid];
3733 if ((user == null) || (req.query.nodeid == null)) { res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the user exists
3734
3735 // Check permissions
3736 obj.GetNodeWithRights(domain, user, req.query.nodeid, function (node, rights, visible) {
3737 if ((node == null) || ((rights & 8) == 0) || ((rights != 0xFFFFFFFF) && ((rights & 512) != 0))) { res.redirect(domain.url + getQueryPortion(req)); return; }
3738
3739 var logoutcontrols = { name: user.name };
3740 var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : '';
3741 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
3742
3743 // Create a authentication cookie
3744 const authCookie = obj.parent.encodeCookie({ userid: user._id, domainid: domain.id, ip: req.clientIp }, obj.parent.loginCookieEncryptionKey);
3745 const authRelayCookie = obj.parent.encodeCookie({ ruserid: user._id, domainid: domain.id }, obj.parent.loginCookieEncryptionKey);
3746 var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
3747 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));
3748 });
3749 } else {
3750 res.redirect(domain.url + getQueryPortion(req));
3751 return;
3752 }
3753 }
3754
3755 // Handle new account Captcha GET
3756 function handleNewAccountCaptchaRequest(req, res) {
3757 const domain = checkUserIpAddress(req, res);
3758 if (domain == null) { return; }
3759 if ((domain.newaccountscaptcha == null) || (domain.newaccountscaptcha === false) || (req.query.x == null)) { res.sendStatus(404); return; }
3760 const c = obj.parent.decodeCookie(req.query.x, obj.parent.loginCookieEncryptionKey);
3761 if ((c == null) || (c.type !== 'newAccount') || (typeof c.captcha != 'string')) { res.sendStatus(404); return; }
3762 res.type('svg');
3763 res.status(200).end(require('svg-captcha')(c.captcha, {}));
3764 }
3765
3766 // Handle Captcha GET
3767 function handleCaptchaGetRequest(req, res) {
3768 const domain = checkUserIpAddress(req, res);
3769 if (domain == null) { return; }
3770 if (parent.crowdSecBounser == null) { res.sendStatus(404); return; }
3771 parent.crowdSecBounser.applyCaptcha(req, res, function () { res.redirect((((domain.id == '') && (domain.dns == null)) ? '/' : ('/' + domain.id))); });
3772 }
3773
3774 // Handle Captcha POST
3775 function handleCaptchaPostRequest(req, res) {
3776 if (parent.crowdSecBounser == null) { res.sendStatus(404); return; }
3777 const domain = checkUserIpAddress(req, res);
3778 if (domain == null) { return; }
3779 req.originalUrl = (((domain.id == '') && (domain.dns == null)) ? '/' : ('/' + domain.id));
3780 parent.crowdSecBounser.applyCaptcha(req, res, function () { res.redirect(req.originalUrl); });
3781 }
3782
3783 // Render the terms of service.
3784 function handleTermsRequest(req, res) {
3785 const domain = checkUserIpAddress(req, res);
3786 if (domain == null) { return; }
3787 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
3788
3789 // See if term.txt was loaded from the database
3790 if ((parent.configurationFiles != null) && (parent.configurationFiles['terms.txt'] != null)) {
3791 // Send the terms from the database
3792 res.set({ 'Cache-Control': 'no-store' });
3793 if (req.session && req.session.userid) {
3794 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
3795 var user = obj.users[req.session.userid];
3796 var logoutcontrols = { name: user.name };
3797 var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : '';
3798 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
3799 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));
3800 } else {
3801 render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(parent.configurationFiles['terms.txt'].toString()).split('\'').join('\\\''), logoutControls: encodeURIComponent('{}') }, req, domain));
3802 }
3803 } else {
3804 // See if there is a terms.txt file in meshcentral-data
3805 var p = obj.path.join(obj.parent.datapath, 'terms.txt');
3806 if (obj.fs.existsSync(p)) {
3807 obj.fs.readFile(p, 'utf8', function (err, data) {
3808 if (err != null) { parent.debug('web', 'handleTermsRequest: no terms.txt'); res.sendStatus(404); return; }
3809
3810 // Send the terms from terms.txt
3811 res.set({ 'Cache-Control': 'no-store' });
3812 if (req.session && req.session.userid) {
3813 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
3814 var user = obj.users[req.session.userid];
3815 var logoutcontrols = { name: user.name };
3816 var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : '';
3817 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
3818 render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(data).split('\'').join('\\\''), logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain));
3819 } else {
3820 render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(data).split('\'').join('\\\''), logoutControls: encodeURIComponent('{}') }, req, domain));
3821 }
3822 });
3823 } else {
3824 // Send the default terms
3825 parent.debug('web', 'handleTermsRequest: sending default terms');
3826 res.set({ 'Cache-Control': 'no-store' });
3827 if (req.session && req.session.userid) {
3828 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
3829 var user = obj.users[req.session.userid];
3830 var logoutcontrols = { name: user.name };
3831 var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : '';
3832 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
3833 render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain));
3834 } else {
3835 render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ logoutControls: encodeURIComponent('{}') }, req, domain));
3836 }
3837 }
3838 }
3839 }
3840
3841 // Render the messenger application.
3842 function handleMessengerRequest(req, res) {
3843 const domain = getDomain(req);
3844 if (domain == null) { parent.debug('web', 'handleMessengerRequest: no domain'); res.sendStatus(404); return; }
3845 parent.debug('web', 'handleMessengerRequest()');
3846
3847 // Check if we are in maintenance mode
3848 if (parent.config.settings.maintenancemode != null) {
3849 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));
3850 return;
3851 }
3852
3853 // Check if this session is for a user
3854 if (req.query.id == null) { res.sendStatus(404); return; }
3855 var idSplit = decodeURIComponent(req.query.id).split('/');
3856 if ((idSplit.length != 7) || (idSplit[0] != 'meshmessenger')) { res.sendStatus(404); return; }
3857 if ((idSplit[1] == 'user') && (idSplit[4] == 'user')) {
3858 // This is a user to user conversation, both users must be logged in.
3859 var user1 = idSplit[1] + '/' + idSplit[2] + '/' + idSplit[3]
3860 var user2 = idSplit[4] + '/' + idSplit[5] + '/' + idSplit[6]
3861 if (!req.session || !req.session.userid) {
3862 // Redirect to login page
3863 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)); }
3864 return;
3865 }
3866 if ((req.session.userid != user1) && (req.session.userid != user2)) { res.sendStatus(404); return; }
3867 }
3868
3869 // Get WebRTC configuration
3870 var webRtcConfig = null;
3871 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'); }
3872 else if (args.webrtcconfig && (typeof args.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(args.webrtcconfig)).replace(/'/g, '%27'); }
3873
3874 // Setup other options
3875 var options = { webrtcconfig: webRtcConfig };
3876 if (typeof domain.meshmessengertitle == 'string') { options.meshMessengerTitle = domain.meshmessengertitle; } else { options.meshMessengerTitle = '!'; }
3877
3878 // Get the userid and name
3879 if ((domain.meshmessengertitle != null) && (req.query.id != null) && (req.query.id.startsWith('meshmessenger/node'))) {
3880 if (idSplit.length == 7) {
3881 const user = obj.users[idSplit[4] + '/' + idSplit[5] + '/' + idSplit[6]];
3882 if (user != null) {
3883 if (domain.meshmessengertitle.indexOf('{0}') >= 0) { options.username = encodeURIComponent(user.realname ? user.realname : user.name).replace(/'/g, '%27'); }
3884 if (domain.meshmessengertitle.indexOf('{1}') >= 0) { options.userid = encodeURIComponent(user.name).replace(/'/g, '%27'); }
3885 }
3886 }
3887 }
3888
3889 // Render the page
3890 res.set({ 'Cache-Control': 'no-store' });
3891 render(req, res, getRenderPage('messenger', req, domain), getRenderArgs(options, req, domain));
3892 }
3893
3894 // Handle messenger image request
3895 function handleMessengerImageRequest(req, res) {
3896 const domain = getDomain(req);
3897 if (domain == null) { parent.debug('web', 'handleMessengerImageRequest: no domain'); res.sendStatus(404); return; }
3898 parent.debug('web', 'handleMessengerImageRequest()');
3899
3900 // Check if we are in maintenance mode
3901 if (parent.config.settings.maintenancemode != null) { res.sendStatus(404); return; }
3902
3903 //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
3904 if (domain.meshmessengerpicture) {
3905 // Use the configured messenger logo picture
3906 try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.meshmessengerpicture)); return; } catch (ex) { }
3907 }
3908
3909 var imagefile = 'images/messenger.png';
3910 if (domain.webpublicpath != null) {
3911 obj.fs.exists(obj.path.join(domain.webpublicpath, imagefile), function (exists) {
3912 if (exists) {
3913 // Use the domain logo picture
3914 try { res.sendFile(obj.path.join(domain.webpublicpath, imagefile)); } catch (ex) { res.sendStatus(404); }
3915 } else {
3916 // Use the default logo picture
3917 try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
3918 }
3919 });
3920 } else if (parent.webPublicOverridePath) {
3921 obj.fs.exists(obj.path.join(obj.parent.webPublicOverridePath, imagefile), function (exists) {
3922 if (exists) {
3923 // Use the override logo picture
3924 try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, 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 {
3931 // Use the default logo picture
3932 try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
3933 }
3934 }
3935
3936 // Returns the server root certificate encoded in base64
3937 function getRootCertBase64() {
3938 var rootcert = obj.certificates.root.cert;
3939 var i = rootcert.indexOf('-----BEGIN CERTIFICATE-----\r\n');
3940 if (i >= 0) { rootcert = rootcert.substring(i + 29); }
3941 i = rootcert.indexOf('-----END CERTIFICATE-----');
3942 if (i >= 0) { rootcert = rootcert.substring(i, 0); }
3943 return Buffer.from(rootcert, 'base64').toString('base64');
3944 }
3945
3946 // Returns the mesh server root certificate
3947 function handleRootCertRequest(req, res) {
3948 const domain = getDomain(req);
3949 if (domain == null) { parent.debug('web', 'handleRootCertRequest: no domain'); res.sendStatus(404); return; }
3950 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
3951 if ((obj.userAllowedIp != null) && (checkIpAddressEx(req, res, obj.userAllowedIp, false) === false)) { parent.debug('web', 'handleRootCertRequest: invalid ip'); return; } // Check server-wide IP filter only.
3952 parent.debug('web', 'handleRootCertRequest()');
3953 setContentDispositionHeader(res, 'application/octet-stream', certificates.RootName + '.cer', null, 'rootcert.cer');
3954 res.send(Buffer.from(getRootCertBase64(), 'base64'));
3955 }
3956
3957 // Return a customised mainifest.json for PWA
3958 function handleManifestRequest(req, res){
3959 const domain = checkUserIpAddress(req);
3960 if (domain == null) { parent.debug('web', 'handleManifestRequest: no domain'); res.sendStatus(404); return; }
3961 parent.debug('web', 'handleManifestRequest()');
3962 var manifest = {
3963 "name": (domain.title != null) ? domain.title : 'MeshCentral',
3964 "short_name": (domain.title != null) ? domain.title : 'MeshCentral',
3965 "description": "Open source web based, remote computer management.",
3966 "scope": ".",
3967 "start_url": "/",
3968 "display": "fullscreen",
3969 "orientation": "any",
3970 "theme_color": "#ffffff",
3971 "background_color": "#ffffff",
3972 "icons": [{
3973 "src": "pwalogo.png",
3974 "sizes": "512x512",
3975 "type": "image/png"
3976 }]
3977 };
3978 res.json(manifest);
3979 }
3980
3981 // Handle user public file downloads
3982 function handleDownloadUserFiles(req, res) {
3983 const domain = checkUserIpAddress(req, res);
3984 if (domain == null) { return; }
3985 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
3986
3987 if (obj.common.validateString(req.path, 1, 4096) == false) { res.sendStatus(404); return; }
3988 var domainname = 'domain', spliturl = decodeURIComponent(req.path).split('/'), filename = '';
3989 if (spliturl[1] != 'userfiles') { spliturl.splice(1,1); } // remove domain.id from url for domains without dns
3990 if ((spliturl.length < 3) || (obj.common.IsFilenameValid(spliturl[2]) == false) || (domain.userQuota == -1)) { res.sendStatus(404); return; }
3991 if (domain.id != '') { domainname = 'domain-' + domain.id; }
3992 var path = obj.path.join(obj.filespath, domainname + '/user-' + spliturl[2] + '/Public');
3993 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; } }
3994
3995 var stat = null;
3996 try { stat = obj.fs.statSync(path); } catch (e) { }
3997 if ((stat != null) && ((stat.mode & 0x004000) == 0)) {
3998 if (req.query.download == 1) {
3999 setContentDispositionHeader(res, 'application/octet-stream', filename, null, 'file.bin');
4000 try { res.sendFile(obj.path.resolve(__dirname, path)); } catch (e) { res.sendStatus(404); }
4001 } else {
4002 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));
4003 }
4004 } else {
4005 render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'download2' : 'download', req, domain), getRenderArgs({ rootCertLink: getRootCertLink(domain), messageid: 2 }, req, domain));
4006 }
4007 }
4008
4009 // Handle device file request
4010 function handleDeviceFile(req, res) {
4011 const domain = getDomain(req, res);
4012 if (domain == null) { return; }
4013 if ((req.query.c == null) || (req.query.f == null)) { res.sendStatus(404); return; }
4014
4015 // Check the inbound desktop sharing cookie
4016 var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
4017 if ((c == null) || (c.domainid !== domain.id)) { res.sendStatus(404); return; }
4018
4019 // Check userid
4020 const user = obj.users[c.userid];
4021 if ((c == user)) { res.sendStatus(404); return; }
4022
4023 // If this cookie has restricted usages, check that it's allowed to perform downloads
4024 if (Array.isArray(c.usages) && (c.usages.indexOf(10) < 0)) { res.sendStatus(404); return; } // Check protocol #10
4025
4026 if (c.nid != null) { req.query.n = c.nid.split('/')[2]; } // This cookie is restricted to a specific nodeid.
4027 if (req.query.n == null) { res.sendStatus(404); return; }
4028
4029 // Check if this user has permission to manage this computer
4030 obj.GetNodeWithRights(domain, user, 'node/' + domain.id + '/' + req.query.n, function (node, rights, visible) {
4031 if ((node == null) || ((rights & MESHRIGHT_REMOTECONTROL) == 0) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device
4032
4033 // All good, start the file transfer
4034 req.query.id = getRandomLowerCase(12);
4035 obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, null, res, req, domain, user, node.meshid, node._id);
4036 });
4037 }
4038
4039 // Handle download of a server file by an agent
4040 function handleAgentDownloadFile(req, res) {
4041 const domain = checkAgentIpAddress(req, res);
4042 if (domain == null) { return; }
4043 if (req.query.c == null) { res.sendStatus(404); return; }
4044
4045 // Check the inbound desktop sharing cookie
4046 var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 5); // 5 minute timeout
4047 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; }
4048
4049 // Send the file back
4050 try { res.sendFile(obj.path.join(obj.filespath, 'tmp', c.f)); return; } catch (ex) { res.sendStatus(404); }
4051 }
4052
4053 // Handle logo request
4054 function handleLogoRequest(req, res) {
4055 const domain = checkUserIpAddress(req, res);
4056 if (domain == null) { return; }
4057
4058 //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
4059 if (domain.titlepicture) {
4060 if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.titlepicture] != null)) {
4061 // Use the logo in the database
4062 res.set({ 'Content-Type': domain.titlepicture.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
4063 res.send(parent.configurationFiles[domain.titlepicture]);
4064 return;
4065 } else {
4066 // Use the logo on file
4067 try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.titlepicture)); return; } catch (ex) { }
4068 }
4069 }
4070
4071 if ((domain.webpublicpath != null) && (obj.fs.existsSync(obj.path.join(domain.webpublicpath, 'images/logoback.png')))) {
4072 // Use the domain logo picture
4073 try { res.sendFile(obj.path.join(domain.webpublicpath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
4074 } else if (parent.webPublicOverridePath && obj.fs.existsSync(obj.path.join(obj.parent.webPublicOverridePath, 'images/logoback.png'))) {
4075 // Use the override logo picture
4076 try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
4077 } else {
4078 // Use the default logo picture
4079 try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
4080 }
4081 }
4082
4083 // Handle login logo request
4084 function handleLoginLogoRequest(req, res) {
4085 const domain = checkUserIpAddress(req, res);
4086 if (domain == null) { return; }
4087
4088 //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
4089 if (domain.loginpicture) {
4090 if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.loginpicture] != null)) {
4091 // Use the logo in the database
4092 res.set({ 'Content-Type': domain.loginpicture.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
4093 res.send(parent.configurationFiles[domain.loginpicture]);
4094 return;
4095 } else {
4096 // Use the logo on file
4097 try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.loginpicture)); return; } catch (ex) { res.sendStatus(404); }
4098 }
4099 } else {
4100 res.sendStatus(404);
4101 }
4102 }
4103
4104 // Handle PWA logo request
4105 function handlePWALogoRequest(req, res) {
4106 const domain = checkUserIpAddress(req, res);
4107 if (domain == null) { return; }
4108
4109 //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
4110 if (domain.pwalogo) {
4111 if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.pwalogo] != null)) {
4112 // Use the logo in the database
4113 res.set({ 'Content-Type': domain.pwalogo.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
4114 res.send(parent.configurationFiles[domain.pwalogo]);
4115 return;
4116 } else {
4117 // Use the logo on file
4118 try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.pwalogo)); return; } catch (ex) { }
4119 }
4120 }
4121
4122 if ((domain.webpublicpath != null) && (obj.fs.existsSync(obj.path.join(domain.webpublicpath, 'android-chrome-512x512.png')))) {
4123 // Use the domain logo picture
4124 try { res.sendFile(obj.path.join(domain.webpublicpath, 'android-chrome-512x512.png')); } catch (ex) { res.sendStatus(404); }
4125 } else if (parent.webPublicOverridePath && obj.fs.existsSync(obj.path.join(obj.parent.webPublicOverridePath, 'android-chrome-512x512.png'))) {
4126 // Use the override logo picture
4127 try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, 'android-chrome-512x512.png')); } catch (ex) { res.sendStatus(404); }
4128 } else {
4129 // Use the default logo picture
4130 try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'android-chrome-512x512.png')); } catch (ex) { res.sendStatus(404); }
4131 }
4132 }
4133
4134 // Handle translation request
4135 function handleTranslationsRequest(req, res) {
4136 const domain = checkUserIpAddress(req, res);
4137 if (domain == null) { return; }
4138 //if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
4139 if ((obj.userAllowedIp != null) && (checkIpAddressEx(req, res, obj.userAllowedIp, false) === false)) { return; } // Check server-wide IP filter only.
4140
4141 var user = null;
4142 if (obj.args.user != null) {
4143 // A default user is active
4144 user = obj.users['user/' + domain.id + '/' + obj.args.user];
4145 if (!user) { parent.debug('web', 'handleTranslationsRequest: user not found.'); res.sendStatus(401); return; }
4146 } else {
4147 // Check if the user is logged and we have all required parameters
4148 if (!req.session || !req.session.userid) { parent.debug('web', 'handleTranslationsRequest: failed checks (2).'); res.sendStatus(401); return; }
4149
4150 // Get the current user
4151 user = obj.users[req.session.userid];
4152 if (!user) { parent.debug('web', 'handleTranslationsRequest: user not found.'); res.sendStatus(401); return; }
4153 if (user.siteadmin != 0xFFFFFFFF) { parent.debug('web', 'handleTranslationsRequest: user not site administrator.'); res.sendStatus(401); return; }
4154 }
4155
4156 var data = '';
4157 req.setEncoding('utf8');
4158 req.on('data', function (chunk) { data += chunk; });
4159 req.on('end', function () {
4160 try { data = JSON.parse(data); } catch (ex) { data = null; }
4161 if (data == null) { res.sendStatus(404); return; }
4162 if (data.action == 'getTranslations') {
4163 if (obj.fs.existsSync(obj.path.join(obj.parent.datapath, 'translate.json'))) {
4164 // Return the translation file (JSON)
4165 try { res.sendFile(obj.path.join(obj.parent.datapath, 'translate.json')); } catch (ex) { res.sendStatus(404); }
4166 } else if (obj.fs.existsSync(obj.path.join(__dirname, 'translate', 'translate.json'))) {
4167 // Return the default translation file (JSON)
4168 try { res.sendFile(obj.path.join(__dirname, 'translate', 'translate.json')); } catch (ex) { res.sendStatus(404); }
4169 } else { res.sendStatus(404); }
4170 } else if (data.action == 'setTranslations') {
4171 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 })); } });
4172 } else if (data.action == 'translateServer') {
4173 if (obj.pendingTranslation === true) { res.send(JSON.stringify({ response: 'Server is already performing a translation.' })); return; }
4174 const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
4175 if (nodeVersion < 8) { res.send(JSON.stringify({ response: 'Server requires NodeJS 8.x or better.' })); return; }
4176 var translateFile = obj.path.join(obj.parent.datapath, 'translate.json');
4177 if (obj.fs.existsSync(translateFile) == false) { translateFile = obj.path.join(__dirname, 'translate', 'translate.json'); }
4178 if (obj.fs.existsSync(translateFile) == false) { res.send(JSON.stringify({ response: 'Unable to find translate.js file on the server.' })); return; }
4179 res.send(JSON.stringify({ response: 'ok' }));
4180 console.log('Started server translation...');
4181 obj.pendingTranslation = true;
4182 require('child_process').exec(process.argv[0] + ' translate.js translateall \"' + translateFile + '\"', { maxBuffer: 512000, timeout: 300000, cwd: obj.path.join(__dirname, 'translate') }, function (error, stdout, stderr) {
4183 delete obj.pendingTranslation;
4184 if (error) { console.log('Server translation error', error); }
4185 // console.log('stdout', stdout);
4186 if (stderr) { console.log('Server translation stderr', stderr); }
4187 //console.log('Server restart...'); // Perform a server restart
4188 //process.exit(0);
4189 console.log('Server translation completed.');
4190 });
4191 } else {
4192 // Unknown request
4193 res.sendStatus(404);
4194 }
4195 });
4196 }
4197
4198 // Handle welcome image request
4199 function handleWelcomeImageRequest(req, res) {
4200 const domain = checkUserIpAddress(req, res);
4201 if (domain == null) { return; }
4202
4203 //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
4204 if (domain.welcomepicture) {
4205 if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.welcomepicture] != null)) {
4206 // Use the welcome image in the database
4207 res.set({ 'Content-Type': domain.welcomepicture.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
4208 res.send(parent.configurationFiles[domain.welcomepicture]);
4209 return;
4210 }
4211
4212 // Use the configured logo picture
4213 try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.welcomepicture)); return; } catch (ex) { }
4214 }
4215
4216 var imagefile = 'images/mainwelcome.jpg';
4217 if (domain.sitestyle >= 2) { imagefile = 'images/login/back.png'; }
4218 if (domain.webpublicpath != null) {
4219 obj.fs.exists(obj.path.join(domain.webpublicpath, imagefile), function (exists) {
4220 if (exists) {
4221 // Use the domain logo picture
4222 try { res.sendFile(obj.path.join(domain.webpublicpath, imagefile)); } catch (ex) { res.sendStatus(404); }
4223 } else {
4224 // Use the default logo picture
4225 try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
4226 }
4227 });
4228 } else if (parent.webPublicOverridePath) {
4229 obj.fs.exists(obj.path.join(obj.parent.webPublicOverridePath, imagefile), function (exists) {
4230 if (exists) {
4231 // Use the override logo picture
4232 try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, imagefile)); } catch (ex) { res.sendStatus(404); }
4233 } else {
4234 // Use the default logo picture
4235 try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
4236 }
4237 });
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
4244 // Download a session recording
4245 function handleGetRecordings(req, res) {
4246 const domain = checkUserIpAddress(req, res);
4247 if (domain == null) return;
4248
4249 // Check the query
4250 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; }
4251
4252 // Get the recording path
4253 var recordingsPath = null;
4254 if (domain.sessionrecording.filepath) { recordingsPath = domain.sessionrecording.filepath; } else { recordingsPath = parent.recordpath; }
4255 if (recordingsPath == null) { res.sendStatus(401); return; }
4256
4257 // Get the user and check user rights
4258 var authUserid = null;
4259 if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
4260 if (authUserid == null) { res.sendStatus(401); return; }
4261 const user = obj.users[authUserid];
4262 if (user == null) { res.sendStatus(401); return; }
4263 if ((user.siteadmin & 512) == 0) { res.sendStatus(401); return; } // Check if we have right to get recordings
4264
4265 // Send the recorded file
4266 setContentDispositionHeader(res, 'application/octet-stream', req.query.file, null, 'recording.mcrec');
4267 try { res.sendFile(obj.path.join(recordingsPath, req.query.file)); } catch (ex) { res.sendStatus(404); }
4268 }
4269
4270 // Stream a session recording
4271 function handleGetRecordingsWebSocket(ws, req) {
4272 var domain = checkAgentIpAddress(ws, req);
4273 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; }
4274
4275 // Check the query
4276 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; }
4277
4278 // Get the recording path
4279 var recordingsPath = null;
4280 if (domain.sessionrecording.filepath) { recordingsPath = domain.sessionrecording.filepath; } else { recordingsPath = parent.recordpath; }
4281 if (recordingsPath == null) { try { ws.close(); } catch (ex) { } return; }
4282
4283 // Get the user and check user rights
4284 var authUserid = null;
4285 if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
4286 if (authUserid == null) { try { ws.close(); } catch (ex) { } return; }
4287 const user = obj.users[authUserid];
4288 if (user == null) { try { ws.close(); } catch (ex) { } return; }
4289 if ((user.siteadmin & 512) == 0) { try { ws.close(); } catch (ex) { } return; } // Check if we have right to get recordings
4290 const filefullpath = obj.path.join(recordingsPath, req.query.file);
4291
4292 obj.fs.stat(filefullpath, function (err, stats) {
4293 if (err) {
4294 try { ws.close(); } catch (ex) { } // File does not exist
4295 } else {
4296 obj.fs.open(filefullpath, 'r', function (err, fd) {
4297 if (err == null) {
4298 // When data is received from the web socket
4299 ws.on('message', function (msg) {
4300 if (typeof msg != 'string') return;
4301 var command;
4302 try { command = JSON.parse(msg); } catch (e) { return; }
4303 if ((command == null) || (typeof command.action != 'string')) return;
4304 switch (command.action) {
4305 case 'get': {
4306 const buffer = Buffer.alloc(8 + command.size);
4307 //buffer.writeUInt32BE((command.ptr >> 32), 0);
4308 buffer.writeUInt32BE((command.ptr & 0xFFFFFFFF), 4);
4309 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); });
4310 break;
4311 }
4312 }
4313 });
4314
4315 // If error, do nothing
4316 ws.on('error', function (err) { try { ws.close(); } catch (ex) { } obj.fs.close(fd, function (err) { }); });
4317
4318 // If the web socket is closed
4319 ws.on('close', function (req) { try { ws.close(); } catch (ex) { } obj.fs.close(fd, function (err) { }); });
4320
4321 ws.send(JSON.stringify({ "action": "info", "name": req.query.file, "size": stats.size }));
4322 } else {
4323 try { ws.close(); } catch (ex) { }
4324 }
4325 });
4326 }
4327 });
4328 }
4329
4330 // Serve the player page
4331 function handlePlayerRequest(req, res) {
4332 const domain = checkUserIpAddress(req, res);
4333 if (domain == null) { return; }
4334
4335 parent.debug('web', 'handlePlayerRequest: sending player');
4336 res.set({ 'Cache-Control': 'no-store' });
4337 render(req, res, getRenderPage('player', req, domain), getRenderArgs({}, req, domain));
4338 }
4339
4340 // Serve the guest sharing page
4341 function handleSharingRequest(req, res) {
4342 const domain = getDomain(req, res);
4343 if (domain == null) { return; }
4344 if (req.query.c == null) { res.sendStatus(404); return; }
4345 if (domain.guestdevicesharing === false) { res.sendStatus(404); return; } // This feature is not allowed.
4346
4347 // Check the inbound guest sharing cookie
4348 var c = obj.parent.decodeCookie(req.query.c, obj.parent.invitationLinkEncryptionKey, 9999999999); // Decode cookies with unlimited time.
4349 if (c == null) { res.sendStatus(404); return; }
4350
4351 if (c.a === 5) {
4352 // This is the older style sharing cookie with everything encoded within it.
4353 // This cookie style gives a very large URL, so it's not used anymore.
4354 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; }
4355 handleSharingRequestEx(req, res, domain, c);
4356 return;
4357 }
4358 if (c.a === 6) {
4359 // This is the new style sharing cookie, just encodes the pointer to the sharing information in the database.
4360 // Gives a much more compact URL.
4361 if (typeof c.pid != 'string') { res.sendStatus(404); return; }
4362
4363 // Check the expired time, expire message.
4364 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; }
4365
4366 obj.db.Get('deviceshare-' + c.pid, function (err, docs) {
4367 if ((err != null) || (docs == null) || (docs.length != 1)) { res.sendStatus(404); return; }
4368 const doc = docs[0];
4369
4370 // If this is a recurrent share, check if we are at the correct time to make use of it
4371 if (typeof doc.recurring == 'number') {
4372 const now = Date.now();
4373 if (now >= doc.startTime) { // We don't want to move the validity window before the start time
4374 const deltaTime = (now - doc.startTime);
4375 if (doc.recurring === 1) {
4376 // This moves the start time to the next valid daily window
4377 const oneDay = (24 * 60 * 60 * 1000);
4378 var addition = Math.floor(deltaTime / oneDay);
4379 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.
4380 doc.startTime += (addition * oneDay);
4381 } else if (doc.recurring === 2) {
4382 // This moves the start time to the next valid weekly window
4383 const oneWeek = (7 * 24 * 60 * 60 * 1000);
4384 var addition = Math.floor(deltaTime / oneWeek);
4385 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.
4386 doc.startTime += (addition * oneWeek);
4387 }
4388 }
4389 }
4390
4391 // Generate an old style cookie from the information in the database
4392 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 };
4393 if (doc.userid) { cookie.uid = doc.userid; }
4394 if ((cookie.userid == null) && (cookie.pid.startsWith('AS:node/'))) { cookie.nouser = 1; }
4395 if (doc.startTime != null) {
4396 if (doc.expireTime != null) { cookie.start = doc.startTime; cookie.expire = doc.expireTime; }
4397 else if (doc.duration != null) { cookie.start = doc.startTime; cookie.expire = doc.startTime + (doc.duration * 60000); }
4398 }
4399 if (doc.viewOnly === true) { cookie.vo = 1; }
4400 handleSharingRequestEx(req, res, domain, cookie);
4401 });
4402 return;
4403 }
4404 res.sendStatus(404); return;
4405 }
4406
4407 // Serve the guest sharing page
4408 function handleSharingRequestEx(req, res, domain, c) {
4409 // Check the expired time, expire message.
4410 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; }
4411
4412 // Check the public id
4413 obj.db.GetAllTypeNodeFiltered([c.nid], domain.id, 'deviceshare', null, function (err, docs) {
4414 // Check if any sharing links are present, expire message.
4415 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; }
4416
4417 // Search for the device share public identifier, expire message.
4418 var found = false;
4419 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; } }
4420 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; }
4421
4422 // Get information about this node
4423 obj.db.Get(c.nid, function (err, nodes) {
4424 if ((err != null) || (nodes == null) || (nodes.length != 1)) { res.sendStatus(404); return; }
4425 var node = nodes[0];
4426
4427 // Check the start time, not yet valid message.
4428 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; }
4429
4430 // If this is a web relay share, check if this feature is active
4431 if ((c.p == 8) || (c.p == 16)) {
4432 // This is a HTTP or HTTPS share
4433 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));
4434 if (webRelayPort == 0) { res.sendStatus(404); return; }
4435
4436 // Create the authentication cookie
4437 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 };
4438 if ((authCookieData.userid == null) && (authCookieData.pid.startsWith('AS:node/'))) { authCookieData.nouser = 1; }
4439 const authCookie = obj.parent.encodeCookie(authCookieData, obj.parent.loginCookieEncryptionKey);
4440
4441 // Redirect to a URL
4442 var webRelayDns = (args.relaydns != null) ? args.relaydns[0] : obj.getWebServerName(domain, req);
4443 var url = 'https://' + webRelayDns + ':' + webRelayPort + '/control-redirect.ashx?n=' + c.nid + '&p=' + c.port + '&appid=' + c.p + '&c=' + authCookie;
4444 if (c.addr != null) { url += '&addr=' + c.addr; }
4445 if (c.pid != null) { url += '&relayid=' + c.pid; }
4446 parent.debug('web', 'handleSharingRequest: Redirecting guest to HTTP relay page for \"' + c.uid + '\", guest \"' + c.gn + '\".');
4447 res.redirect(url);
4448 } else {
4449 // Looks good, let's create the outbound session cookies.
4450 // This is a desktop, terminal or files share. We need to display the sharing page.
4451 // Consent flags are 1 = Notify, 8 = Prompt, 64 = Privacy Bar.
4452 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 };
4453 if ((authCookieData.userid == null) && (authCookieData.pid.startsWith('AS:node/'))) { authCookieData.nouser = 1; }
4454 if (c.k != null) { authCookieData.k = c.k; }
4455 const authCookie = obj.parent.encodeCookie(authCookieData, obj.parent.loginCookieEncryptionKey);
4456
4457 // Server features
4458 var features2 = 0;
4459 if (obj.args.allowhighqualitydesktop !== false) { features2 += 1; } // Enable AllowHighQualityDesktop (Default true)
4460
4461 // Lets respond by sending out the desktop viewer.
4462 var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
4463 parent.debug('web', 'handleSharingRequest: Sending guest sharing page for \"' + c.uid + '\", guest \"' + c.gn + '\".');
4464 res.set({ 'Cache-Control': 'no-store' });
4465 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));
4466 }
4467 });
4468 });
4469 }
4470
4471 // Handle domain redirection
4472 obj.handleDomainRedirect = function (req, res) {
4473 const domain = checkUserIpAddress(req, res);
4474 if (domain == null) { return; }
4475 if (domain.redirects == null) { res.sendStatus(404); return; }
4476 var urlArgs = '', urlName = null, splitUrl = req.originalUrl.split('?');
4477 if (splitUrl.length > 1) { urlArgs = '?' + splitUrl[1]; }
4478 if ((splitUrl.length > 0) && (splitUrl[0].length > 1)) { urlName = splitUrl[0].substring(1).toLowerCase(); }
4479 if ((urlName == null) || (domain.redirects[urlName] == null) || (urlName[0] == '_')) { res.sendStatus(404); return; }
4480 if (domain.redirects[urlName] == '~showversion') {
4481 // Show the current version
4482 res.end('MeshCentral v' + obj.parent.currentVer);
4483 } else {
4484 // Perform redirection
4485 res.redirect(domain.redirects[urlName] + urlArgs + getQueryPortion(req));
4486 }
4487 }
4488
4489 // Take a "user/domain/userid/path/file" format and return the actual server disk file path if access is allowed
4490 obj.getServerFilePath = function (user, domain, path) {
4491 var splitpath = path.split('/'), serverpath = obj.path.join(obj.filespath, 'domain'), filename = '';
4492 if ((splitpath.length < 3) || (splitpath[0] != 'user' && splitpath[0] != 'mesh') || (splitpath[1] != domain.id)) return null; // Basic validation
4493 var objid = splitpath[0] + '/' + splitpath[1] + '/' + splitpath[2];
4494 if (splitpath[0] == 'user' && (objid != user._id)) return null; // User validation, only self allowed
4495 if (splitpath[0] == 'mesh') { if ((obj.GetMeshRights(user, objid) & 32) == 0) { return null; } } // Check mesh server file rights
4496 if (splitpath[1] != '') { serverpath += '-' + splitpath[1]; } // Add the domain if needed
4497 serverpath += ('/' + splitpath[0] + '-' + splitpath[2]);
4498 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
4499 return { fullpath: obj.path.resolve(obj.filespath, serverpath), path: serverpath, name: filename, quota: obj.getQuota(objid, domain) };
4500 };
4501
4502 // Return the maximum number of bytes allowed in the user account "My Files".
4503 obj.getQuota = function (objid, domain) {
4504 if (objid == null) return 0;
4505 if (objid.startsWith('user/')) {
4506 var user = obj.users[objid];
4507 if (user == null) return 0;
4508 if (user.siteadmin == 0xFFFFFFFF) return null; // Administrators have no user limit
4509 if ((user.quota != null) && (typeof user.quota == 'number')) { return user.quota; }
4510 if ((domain != null) && (domain.userquota != null) && (typeof domain.userquota == 'number')) { return domain.userquota; }
4511 return null; // By default, the user will have no limit
4512 } else if (objid.startsWith('mesh/')) {
4513 var mesh = obj.meshes[objid];
4514 if (mesh == null) return 0;
4515 if ((mesh.quota != null) && (typeof mesh.quota == 'number')) { return mesh.quota; }
4516 if ((domain != null) && (domain.meshquota != null) && (typeof domain.meshquota == 'number')) { return domain.meshquota; }
4517 return null; // By default, the mesh will have no limit
4518 }
4519 return 0;
4520 };
4521
4522 // Download a file from the server
4523 function handleDownloadFile(req, res) {
4524 const domain = checkUserIpAddress(req, res);
4525 if (domain == null) { return; }
4526 if ((req.query.link == null) || (req.session == null) || (req.session.userid == null) || (domain == null) || (domain.userQuota == -1)) { res.sendStatus(404); return; }
4527 const user = obj.users[req.session.userid];
4528 if (user == null) { res.sendStatus(404); return; }
4529 const file = obj.getServerFilePath(user, domain, req.query.link);
4530 if (file == null) { res.sendStatus(404); return; }
4531 setContentDispositionHeader(res, 'application/octet-stream', file.name, null, 'file.bin');
4532 obj.fs.exists(file.fullpath, function (exists) { if (exists == true) { res.sendFile(file.fullpath); } else { res.sendStatus(404); } });
4533 }
4534
4535 // Download the MeshCommander web page
4536 function handleMeshCommander(req, res) {
4537 const domain = checkUserIpAddress(req, res);
4538 if (domain == null) { return; }
4539 if ((req.session == null) || (req.session.userid == null)) { res.sendStatus(404); return; }
4540
4541 // Find the correct MeshCommander language to send
4542 const acceptableLanguages = obj.getLanguageCodes(req);
4543 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' };
4544 for (var i in acceptableLanguages) {
4545 const meshCommanderLanguage = commandLanguageTranslations[acceptableLanguages[i]];
4546 if (meshCommanderLanguage != null) {
4547 try { res.sendFile(obj.parent.path.join(parent.webPublicPath, 'commander' + meshCommanderLanguage + '.htm')); } catch (ex) { }
4548 return;
4549 }
4550 }
4551
4552 // Send out the default english MeshCommander
4553 try { res.sendFile(obj.parent.path.join(parent.webPublicPath, 'commander.htm')); } catch (ex) { }
4554 }
4555
4556 // Upload a MeshCore.js file to the server
4557 function handleUploadMeshCoreFile(req, res) {
4558 const domain = checkUserIpAddress(req, res);
4559 if (domain == null) { return; }
4560 if (domain.id !== '') { res.sendStatus(401); return; }
4561
4562 var authUserid = null;
4563 if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
4564
4565 const multiparty = require('multiparty');
4566 const form = new multiparty.Form();
4567 form.parse(req, function (err, fields, files) {
4568 // If an authentication cookie is embedded in the form, use that.
4569 if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
4570 var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
4571 if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
4572 if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
4573 }
4574 if (authUserid == null) { res.sendStatus(401); return; }
4575 if ((fields == null) || (fields.attrib == null) || (fields.attrib.length != 1)) { res.sendStatus(404); return; }
4576
4577 // Get the user
4578 const user = obj.users[authUserid];
4579 if (user == null) { res.sendStatus(401); return; } // Check this user exists
4580
4581 // Get the node and check node rights
4582 const nodeid = fields.attrib[0];
4583 obj.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
4584 if ((node == null) || (rights != 0xFFFFFFFF) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device
4585 files.files.forEach(function (file) {
4586 obj.fs.readFile(file.path, 'utf8', function (err, data) {
4587 if (err != null) return;
4588 data = obj.common.IntToStr(0) + data; // Add the 4 bytes encoding type & flags (Set to 0 for raw)
4589 obj.sendMeshAgentCore(user, domain, fields.attrib[0], 'custom', data); // Upload the core
4590 try { obj.fs.unlinkSync(file.path); } catch (e) { }
4591 });
4592 });
4593 res.send('');
4594 });
4595 });
4596 }
4597
4598 // Upload a MeshCore.js file to the server
4599 function handleOneClickRecoveryFile(req, res) {
4600 const domain = checkUserIpAddress(req, res);
4601 if (domain == null) { return; }
4602 if (domain.id !== '') { res.sendStatus(401); return; }
4603
4604 var authUserid = null;
4605 if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
4606
4607 const multiparty = require('multiparty');
4608 const form = new multiparty.Form();
4609 form.parse(req, function (err, fields, files) {
4610 // If an authentication cookie is embedded in the form, use that.
4611 if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
4612 var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
4613 if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
4614 if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
4615 }
4616 if (authUserid == null) { res.sendStatus(401); return; }
4617 if ((fields == null) || (fields.attrib == null) || (fields.attrib.length != 1)) { res.sendStatus(404); return; }
4618
4619 // Get the user
4620 const user = obj.users[authUserid];
4621 if (user == null) { res.sendStatus(401); return; } // Check this user exists
4622
4623 // Get the node and check node rights
4624 const nodeid = fields.attrib[0];
4625 obj.GetNodeWithRights(domain, user, nodeid, function (node, rights, visible) {
4626 if ((node == null) || (rights != 0xFFFFFFFF) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device
4627 files.files.forEach(function (file) {
4628 // Event Intel AMT One Click Recovery, this will cause Intel AMT wake operations on this and other servers.
4629 parent.DispatchEvent('*', obj, { action: 'oneclickrecovery', userid: user._id, username: user.name, nodeids: [node._id], domain: domain.id, nolog: 1, file: file.path });
4630
4631 //try { obj.fs.unlinkSync(file.path); } catch (e) { } // TODO: Remove this file after 30 minutes.
4632 });
4633 res.send('');
4634 });
4635 });
4636 }
4637
4638 // Upload a file to the server
4639 function handleUploadFile(req, res) {
4640 const domain = checkUserIpAddress(req, res);
4641 if (domain == null) { return; }
4642 if (domain.userQuota == -1) { res.sendStatus(401); return; }
4643 var authUserid = null;
4644 if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
4645 const multiparty = require('multiparty');
4646 const form = new multiparty.Form();
4647 form.parse(req, function (err, fields, files) {
4648 // If an authentication cookie is embedded in the form, use that.
4649 if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
4650 var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
4651 if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
4652 if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
4653 }
4654 if (authUserid == null) { res.sendStatus(401); return; }
4655
4656 // Get the user
4657 const user = obj.users[authUserid];
4658 if ((user == null) || (user.siteadmin & 8) == 0) { res.sendStatus(401); return; } // Check if we have file rights
4659
4660 if ((fields == null) || (fields.link == null) || (fields.link.length != 1)) { /*console.log('UploadFile, Invalid Fields:', fields, files);*/ console.log('err4'); res.sendStatus(404); return; }
4661 var xfile = null;
4662 try { xfile = obj.getServerFilePath(user, domain, decodeURIComponent(fields.link[0])); } catch (ex) { }
4663 if (xfile == null) { res.sendStatus(404); return; }
4664 // Get total bytes in the path
4665 var totalsize = readTotalFileSize(xfile.fullpath);
4666 if ((xfile.quota == null) || (totalsize < xfile.quota)) { // Check if the quota is not already broken
4667 if (fields.name != null) {
4668
4669 // See if we need to create the folder
4670 var domainx = 'domain';
4671 if (domain.id.length > 0) { domainx = 'domain-' + usersplit[1]; }
4672 try { obj.fs.mkdirSync(obj.parent.filespath); } catch (ex) { }
4673 try { obj.fs.mkdirSync(obj.parent.path.join(obj.parent.filespath, domainx)); } catch (ex) { }
4674 try { obj.fs.mkdirSync(xfile.fullpath); } catch (ex) { }
4675
4676 // Upload method where all the file data is within the fields.
4677 var names = fields.name[0].split('*'), sizes = fields.size[0].split('*'), types = fields.type[0].split('*'), datas = fields.data[0].split('*');
4678 if ((names.length == sizes.length) && (types.length == datas.length) && (names.length == types.length)) {
4679 for (var i = 0; i < names.length; i++) {
4680 if (obj.common.IsFilenameValid(names[i]) == false) { res.sendStatus(404); return; }
4681 var filedata = Buffer.from(datas[i].split(',')[1], 'base64');
4682 if ((xfile.quota == null) || ((totalsize + filedata.length) < xfile.quota)) { // Check if quota would not be broken if we add this file
4683 // Create the user folder if needed
4684 (function (fullpath, filename, filedata) {
4685 obj.fs.mkdir(xfile.fullpath, function () {
4686 // Write the file
4687 obj.fs.writeFile(obj.path.join(xfile.fullpath, filename), filedata, function () {
4688 obj.parent.DispatchEvent([user._id], obj, 'updatefiles'); // Fire an event causing this user to update this files
4689 });
4690 });
4691 })(xfile.fullpath, names[i], filedata);
4692 } else {
4693 // Send a notification
4694 obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: "Disk quota exceed", value: names[i], nolog: 1, id: Math.random() });
4695 }
4696 }
4697 }
4698 } else {
4699 // More typical upload method, the file data is in a multipart mime post.
4700 files.files.forEach(function (file) {
4701 var fpath = obj.path.join(xfile.fullpath, file.originalFilename);
4702 if (obj.common.IsFilenameValid(file.originalFilename) && ((xfile.quota == null) || ((totalsize + file.size) < xfile.quota))) { // Check if quota would not be broken if we add this file
4703
4704 // See if we need to create the folder
4705 var domainx = 'domain';
4706 if (domain.id.length > 0) { domainx = 'domain-' + domain.id; }
4707 try { obj.fs.mkdirSync(obj.parent.filespath); } catch (e) { }
4708 try { obj.fs.mkdirSync(obj.parent.path.join(obj.parent.filespath, domainx)); } catch (e) { }
4709 try { obj.fs.mkdirSync(xfile.fullpath); } catch (e) { }
4710
4711 // Rename the file
4712 obj.fs.rename(file.path, fpath, function (err) {
4713 if (err && (err.code === 'EXDEV')) {
4714 // On some Linux, the rename will fail with a "EXDEV" error, do a copy+unlink instead.
4715 obj.common.copyFile(file.path, fpath, function (err) {
4716 obj.fs.unlink(file.path, function (err) {
4717 obj.parent.DispatchEvent([user._id], obj, 'updatefiles'); // Fire an event causing this user to update this files
4718 });
4719 });
4720 } else {
4721 obj.parent.DispatchEvent([user._id], obj, 'updatefiles'); // Fire an event causing this user to update this files
4722 }
4723 });
4724 } else {
4725 // Send a notification
4726 obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: "Disk quota exceed", value: file.originalFilename, nolog: 1, id: Math.random() });
4727 try { obj.fs.unlink(file.path, function (err) { }); } catch (e) { }
4728 }
4729 });
4730 }
4731 } else {
4732 // Send a notification
4733 obj.parent.DispatchEvent([user._id], obj, { action: 'notify', value: "Disk quota exceed", nolog: 1, id: Math.random() });
4734 }
4735 res.send('');
4736 });
4737 }
4738
4739 // Upload a file to the server and then batch upload to many agents
4740 function handleUploadFileBatch(req, res) {
4741 const domain = checkUserIpAddress(req, res);
4742 if (domain == null) { return; }
4743 var authUserid = null;
4744 if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
4745 const multiparty = require('multiparty');
4746 const form = new multiparty.Form();
4747 form.parse(req, function (err, fields, files) {
4748 // If an authentication cookie is embedded in the form, use that.
4749 if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
4750 var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
4751 if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
4752 if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
4753 }
4754 if (authUserid == null) { res.sendStatus(401); return; }
4755
4756 // Get the user
4757 const user = obj.users[authUserid];
4758 if (user == null) { parent.debug('web', 'Batch upload error, invalid user.'); res.sendStatus(401); return; } // Check if user exists
4759
4760 // Get fields
4761 if ((fields == null) || (fields.nodeIds == null) || (fields.nodeIds.length != 1)) { res.sendStatus(404); return; }
4762 var cmd = { nodeids: fields.nodeIds[0].split(','), files: [], user: user, domain: domain, overwrite: false, createFolder: false };
4763 if ((fields.winpath != null) && (fields.winpath.length == 1)) { cmd.windowsPath = fields.winpath[0]; }
4764 if ((fields.linuxpath != null) && (fields.linuxpath.length == 1)) { cmd.linuxPath = fields.linuxpath[0]; }
4765 if ((fields.overwriteFiles != null) && (fields.overwriteFiles.length == 1) && (fields.overwriteFiles[0] == 'on')) { cmd.overwrite = true; }
4766 if ((fields.createFolder != null) && (fields.createFolder.length == 1) && (fields.createFolder[0] == 'on')) { cmd.createFolder = true; }
4767
4768 // Check if we have at least one target path
4769 if ((cmd.windowsPath == null) && (cmd.linuxPath == null)) {
4770 parent.debug('web', 'Batch upload error, invalid fields: ' + JSON.stringify(fields));
4771 res.send('');
4772 return;
4773 }
4774
4775 // Get server temporary path
4776 var serverpath = obj.path.join(obj.filespath, 'tmp')
4777 try { obj.fs.mkdirSync(obj.parent.filespath); } catch (ex) { }
4778 try { obj.fs.mkdirSync(serverpath); } catch (ex) { }
4779
4780 // More typical upload method, the file data is in a multipart mime post.
4781 files.files.forEach(function (file) {
4782 var ftarget = getRandomPassword() + '-' + file.originalFilename, fpath = obj.path.join(serverpath, ftarget);
4783 cmd.files.push({ name: file.originalFilename, target: ftarget });
4784 // Rename the file
4785 obj.fs.rename(file.path, fpath, function (err) {
4786 if (err && (err.code === 'EXDEV')) {
4787 // On some Linux, the rename will fail with a "EXDEV" error, do a copy+unlink instead.
4788 obj.common.copyFile(file.path, fpath, function (err) { obj.fs.unlink(file.path, function (err) { }); });
4789 }
4790 });
4791 });
4792
4793 // Instruct one of more agents to download a URL to a given local drive location.
4794 var tlsCertHash = null;
4795 if ((parent.args.ignoreagenthashcheck == null) || (parent.args.ignoreagenthashcheck === false)) { // TODO: If ignoreagenthashcheck is an array of IP addresses, not sure how to handle this.
4796 tlsCertHash = obj.webCertificateFullHashs[cmd.domain.id];
4797 if (tlsCertHash != null) { tlsCertHash = Buffer.from(tlsCertHash, 'binary').toString('hex'); }
4798 }
4799 for (var i in cmd.nodeids) {
4800 obj.GetNodeWithRights(cmd.domain, cmd.user, cmd.nodeids[i], function (node, rights, visible) {
4801 if ((node == null) || ((rights & 8) == 0) || (visible == false)) return; // We don't have remote control rights to this device
4802 var agentPath = (((node.agent.id > 0) && (node.agent.id < 5)) || (node.agent.id == 34)) ? cmd.windowsPath : cmd.linuxPath;
4803 if (agentPath == null) return;
4804
4805 // Compute user consent
4806 var consent = 0;
4807 var mesh = obj.meshes[node.meshid];
4808 if (typeof domain.userconsentflags == 'number') { consent |= domain.userconsentflags; } // Add server required consent flags
4809 if ((mesh != null) && (typeof mesh.consent == 'number')) { consent |= mesh.consent; } // Add device group user consent
4810 if (typeof node.consent == 'number') { consent |= node.consent; } // Add node user consent
4811 if (typeof user.consent == 'number') { consent |= user.consent; } // Add user consent
4812
4813 // Check if we need to add consent flags because of a user group link
4814 if ((mesh != null) && (user.links != null) && (user.links[mesh._id] == null) && (user.links[node._id] == null)) {
4815 // This user does not have a direct link to the device group or device. Find all user groups the would cause the link.
4816 for (var i in user.links) {
4817 var ugrp = obj.userGroups[i];
4818 if ((ugrp != null) && (ugrp.consent != null) && (ugrp.links != null) && ((ugrp.links[mesh._id] != null) || (ugrp.links[node._id] != null))) {
4819 consent |= ugrp.consent; // Add user group consent flags
4820 }
4821 }
4822 }
4823
4824 // Event that this operation is being performed.
4825 var targets = obj.CreateNodeDispatchTargets(node.meshid, node._id, ['server-users', cmd.user._id]);
4826 var msgid = 103; // "Batch upload of {0} file(s) to folder {1}"
4827 var event = { etype: 'node', userid: cmd.user._id, username: cmd.user.name, nodeid: node._id, action: 'batchupload', msg: 'Performing batch upload of ' + cmd.files.length + ' file(s) to ' + agentPath, msgid: msgid, msgArgs: [cmd.files.length, agentPath], domain: cmd.domain.id };
4828 parent.DispatchEvent(targets, obj, event);
4829
4830 // Send the agent commands to perform the batch upload operation
4831 for (var f in cmd.files) {
4832 if (cmd.files[f].name != null) {
4833 const acmd = { action: 'wget', userid: user._id, username: user.name, realname: user.realname, remoteaddr: req.clientIp, consent: consent, rights: rights, overwrite: cmd.overwrite, createFolder: cmd.createFolder, urlpath: '/agentdownload.ashx?c=' + obj.parent.encodeCookie({ a: 'tmpdl', d: cmd.domain.id, nid: node._id, f: cmd.files[f].target }, obj.parent.loginCookieEncryptionKey), path: obj.path.join(agentPath, cmd.files[f].name), folder: agentPath, servertlshash: tlsCertHash };
4834 var agent = obj.wsagents[node._id];
4835 if (agent != null) { try { agent.send(JSON.stringify(acmd)); } catch (ex) { } }
4836 // TODO: Add support for peer servers.
4837 }
4838 }
4839 });
4840 }
4841
4842 res.send('');
4843 });
4844 }
4845
4846 // Subscribe to all events we are allowed to receive
4847 obj.subscribe = function (userid, target) {
4848 const user = obj.users[userid];
4849 if (user == null) return;
4850 const subscriptions = [userid, 'server-allusers'];
4851 if (user.siteadmin != null) {
4852 // Allow full site administrators of users with all events rights to see all events.
4853 if ((user.siteadmin == 0xFFFFFFFF) || ((user.siteadmin & 2048) != 0)) { subscriptions.push('*'); }
4854 else if ((user.siteadmin & 2) != 0) {
4855 if ((user.groups == null) || (user.groups.length == 0)) {
4856 // Subscribe to all user changes
4857 subscriptions.push('server-users');
4858 } else {
4859 // Subscribe to user changes for some groups
4860 for (var i in user.groups) { subscriptions.push('server-users:' + i); }
4861 }
4862 }
4863 }
4864 if (user.links != null) { for (var i in user.links) { subscriptions.push(i); } }
4865 obj.parent.RemoveAllEventDispatch(target);
4866 obj.parent.AddEventDispatch(subscriptions, target);
4867 return subscriptions;
4868 };
4869
4870 // Handle a web socket relay request
4871 function handleRelayWebSocket(ws, req, domain, user, cookie) {
4872 if (!(req.query.host)) { console.log('ERR: No host target specified'); try { ws.close(); } catch (e) { } return; } // Disconnect websocket
4873 parent.debug('web', 'Websocket relay connected from ' + user.name + ' for ' + req.query.host + '.');
4874
4875 try { ws._socket.setKeepAlive(true, 240000); } catch (ex) { } // Set TCP keep alive
4876
4877 // Fetch information about the target
4878 obj.db.Get(req.query.host, function (err, docs) {
4879 if (docs.length == 0) { console.log('ERR: Node not found'); try { ws.close(); } catch (e) { } return; } // Disconnect websocket
4880 var xusername = '', xdevicename = '', xdevicename2 = null, node = null;
4881 node = docs[0]; xdevicename2 = node.name; xdevicename = '-' + parent.common.makeFilename(node.name); ws.id = getRandomPassword(); ws.time = Date.now();
4882 if (!node.intelamt) { console.log('ERR: Not AMT node'); try { ws.close(); } catch (e) { } return; } // Disconnect websocket
4883 var ciraconn = parent.mpsserver.GetConnectionToNode(req.query.host, null, false);
4884
4885 // Check if this user has permission to manage this computer
4886 if ((obj.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0) { console.log('ERR: Access denied (3)'); try { ws.close(); } catch (e) { } return; }
4887
4888 // Check what connectivity is available for this node
4889 var state = parent.GetConnectivityState(req.query.host);
4890 var conn = 0;
4891 if (!state || state.connectivity == 0) { parent.debug('web', 'ERR: No routing possible (1)'); try { ws.close(); } catch (e) { } return; } else { conn = state.connectivity; }
4892
4893 // Check what server needs to handle this connection
4894 if ((obj.parent.multiServer != null) && ((cookie == null) || (cookie.ps != 1))) { // If a cookie is provided and is from a peer server, don't allow the connection to jump again to a different server
4895 var server = obj.parent.GetRoutingServerId(req.query.host, 2); // Check for Intel CIRA connection
4896 if (server != null) {
4897 if (server.serverid != obj.parent.serverId) {
4898 // Do local Intel CIRA routing using a different server
4899 parent.debug('web', 'Route Intel AMT CIRA connection to peer server: ' + server.serverid);
4900 obj.parent.multiServer.createPeerRelay(ws, req, server.serverid, user);
4901 return;
4902 }
4903 } else {
4904 server = obj.parent.GetRoutingServerId(req.query.host, 4); // Check for local Intel AMT connection
4905 if ((server != null) && (server.serverid != obj.parent.serverId)) {
4906 // Do local Intel AMT routing using a different server
4907 parent.debug('web', 'Route Intel AMT direct connection to peer server: ' + server.serverid);
4908 obj.parent.multiServer.createPeerRelay(ws, req, server.serverid, user);
4909 return;
4910 }
4911 }
4912 }
4913
4914 // Setup session recording if needed
4915 if (domain.sessionrecording == true || ((typeof domain.sessionrecording == 'object') && ((domain.sessionrecording.protocols == null) || (domain.sessionrecording.protocols.indexOf((req.query.p == 2) ? 101 : 100) >= 0)))) { // TODO 100
4916 // Check again if we need to do recording
4917 var record = true;
4918
4919 // Check user or device group recording
4920 if ((typeof domain.sessionrecording == 'object') && ((domain.sessionrecording.onlyselectedusers === true) || (domain.sessionrecording.onlyselecteddevicegroups === true))) {
4921 record = false;
4922
4923 // Check device group recording
4924 if (domain.sessionrecording.onlyselecteddevicegroups === true) {
4925 var mesh = obj.meshes[node.meshid];
4926 if ((mesh.flags != null) && ((mesh.flags & 4) != 0)) { record = true; } // Record the session
4927 }
4928
4929 // Check user recording
4930 if (domain.sessionrecording.onlyselectedusers === true) {
4931 if ((user.flags != null) && ((user.flags & 2) != 0)) { record = true; } // Record the session
4932 }
4933 }
4934
4935 if (record == true) {
4936 var now = new Date(Date.now());
4937 // Get the username and make it acceptable as a filename
4938 if (user._id) { xusername = '-' + parent.common.makeFilename(user._id.split('/')[2]); }
4939 var xsessionid = ws.id;
4940 var recFilename = 'relaysession' + ((domain.id == '') ? '' : '-') + domain.id + '-' + now.getUTCFullYear() + '-' + obj.common.zeroPad(now.getUTCMonth() + 1, 2) + '-' + obj.common.zeroPad(now.getUTCDate(), 2) + '-' + obj.common.zeroPad(now.getUTCHours(), 2) + '-' + obj.common.zeroPad(now.getUTCMinutes(), 2) + '-' + obj.common.zeroPad(now.getUTCSeconds(), 2) + xusername + xdevicename + '-' + xsessionid + '.mcrec';
4941 var recFullFilename = null;
4942 if (domain.sessionrecording.filepath) {
4943 try { obj.fs.mkdirSync(domain.sessionrecording.filepath); } catch (e) { }
4944 recFullFilename = obj.path.join(domain.sessionrecording.filepath, recFilename);
4945 } else {
4946 try { obj.fs.mkdirSync(parent.recordpath); } catch (e) { }
4947 recFullFilename = obj.path.join(parent.recordpath, recFilename);
4948 }
4949 var fd = obj.fs.openSync(recFullFilename, 'w');
4950 if (fd != null) {
4951 // Write the recording file header
4952 parent.debug('relay', 'Relay: Started recording to file: ' + recFullFilename);
4953 var metadata = {
4954 magic: 'MeshCentralRelaySession',
4955 ver: 1,
4956 userid: user._id,
4957 username: user.name,
4958 sessionid: ws.id,
4959 ipaddr1: req.clientIp,
4960 time: new Date().toLocaleString(),
4961 protocol: (req.query.p == 2) ? 101 : 100,
4962 nodeid: node._id,
4963 intelamt: true
4964 };
4965 if (ciraconn != null) { metadata.ipaddr2 = ciraconn.remoteAddr; }
4966 else if ((conn & 4) != 0) { metadata.ipaddr2 = node.host; }
4967 if (xdevicename2 != null) { metadata.devicename = xdevicename2; }
4968 var firstBlock = JSON.stringify(metadata)
4969 ws.logfile = { fd: fd, lock: false, filename: recFullFilename, startTime: Date.now(), size: 0, text: 0, req: req };
4970 obj.meshRelayHandler.recordingEntry(ws.logfile, 1, 0, firstBlock, function () { });
4971 if (node != null) { ws.logfile.nodeid = node._id; ws.logfile.meshid = node.meshid; ws.logfile.name = node.name; ws.logfile.icon = node.icon; }
4972 if (req.query.p == 2) { ws.send(Buffer.from(String.fromCharCode(0xF0), 'binary')); } // Intel AMT Redirection: Indicate the session is being recorded
4973 }
4974 }
4975 }
4976
4977 // If Intel AMT CIRA connection is available, use it
4978 if (ciraconn != null) {
4979 parent.debug('web', 'Opening relay CIRA channel connection to ' + req.query.host + '.');
4980
4981 // TODO: If the CIRA connection is a relay or LMS connection, we can't detect the TLS state like this.
4982 // Compute target port, look at the CIRA port mappings, if non-TLS is allowed, use that, if not use TLS
4983 var port = 16993;
4984 //if (node.intelamt.tls == 0) port = 16992; // DEBUG: Allow TLS flag to set TLS mode within CIRA
4985 if (ciraconn.tag.boundPorts.indexOf(16992) >= 0) port = 16992; // RELEASE: Always use non-TLS mode if available within CIRA
4986 if (req.query.p == 2) port += 2;
4987
4988 // Setup a new CIRA channel
4989 if ((port == 16993) || (port == 16995)) {
4990 // Perform TLS
4991 var ser = new SerialTunnel();
4992 var chnl = parent.mpsserver.SetupChannel(ciraconn, port);
4993
4994 // Let's chain up the TLSSocket <-> SerialTunnel <-> CIRA APF (chnl)
4995 // Anything that needs to be forwarded by SerialTunnel will be encapsulated by chnl write
4996 ser.forwardwrite = function (data) { if (data.length > 0) { chnl.write(data); } }; // TLS ---> CIRA
4997
4998 // When APF tunnel return something, update SerialTunnel buffer
4999 chnl.onData = function (ciraconn, data) { if (data.length > 0) { try { ser.updateBuffer(data); } catch (ex) { console.log(ex); } } }; // CIRA ---> TLS
5000
5001 // Handle CIRA tunnel state change
5002 chnl.onStateChange = function (ciraconn, state) {
5003 parent.debug('webrelay', 'Relay TLS CIRA state change', state);
5004 if (state == 0) { try { ws.close(); } catch (e) { } }
5005 if (state == 2) {
5006 // TLSSocket to encapsulate TLS communication, which then tunneled via SerialTunnel an then wrapped through CIRA APF
5007 const tlsoptions = { socket: ser, ciphers: 'RSA+AES:!aNULL:!MD5:!DSS', secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE | constants.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION, rejectUnauthorized: false };
5008 if (req.query.tls1only == 1) {
5009 tlsoptions.secureProtocol = 'TLSv1_method';
5010 } else {
5011 tlsoptions.minVersion = 'TLSv1';
5012 }
5013 var tlsock = obj.tls.connect(tlsoptions, function () { parent.debug('webrelay', "CIRA Secure TLS Connection"); ws._socket.resume(); });
5014 tlsock.chnl = chnl;
5015 tlsock.setEncoding('binary');
5016 tlsock.on('error', function (err) { parent.debug('webrelay', "CIRA TLS Connection Error", err); });
5017
5018 // Decrypted tunnel from TLS communication to be forwarded to websocket
5019 tlsock.on('data', function (data) {
5020 // AMT/TLS ---> WS
5021 if (ws.interceptor) { data = ws.interceptor.processAmtData(data); } // Run data thru interceptor
5022 try { ws.send(data); } catch (ex) { }
5023 });
5024
5025 // If TLS is on, forward it through TLSSocket
5026 ws.forwardclient = tlsock;
5027 ws.forwardclient.xtls = 1;
5028
5029 ws.forwardclient.onStateChange = function (ciraconn, state) {
5030 parent.debug('webrelay', 'Relay CIRA state change', state);
5031 if (state == 0) { try { ws.close(); } catch (e) { } }
5032 };
5033
5034 ws.forwardclient.onData = function (ciraconn, data) {
5035 // Run data thru interceptor
5036 if (ws.interceptor) { data = ws.interceptor.processAmtData(data); }
5037
5038 if (data.length > 0) {
5039 if (ws.logfile == null) {
5040 try { ws.send(data); } catch (e) { }
5041 } else {
5042 // Log to recording file
5043 obj.meshRelayHandler.recordingEntry(ws.logfile, 2, 0, data, function () { try { ws.send(data); } catch (ex) { console.log(ex); } }); // TODO: Add TLS support
5044 }
5045 }
5046 };
5047
5048 // TODO: Flow control? (Dont' really need it with AMT, but would be nice)
5049 ws.forwardclient.onSendOk = function (ciraconn) { };
5050 }
5051 };
5052 } else {
5053 // Without TLS
5054 ws.forwardclient = parent.mpsserver.SetupChannel(ciraconn, port);
5055 ws.forwardclient.xtls = 0;
5056 ws._socket.resume();
5057
5058 ws.forwardclient.onStateChange = function (ciraconn, state) {
5059 parent.debug('webrelay', 'Relay CIRA state change', state);
5060 if (state == 0) { try { ws.close(); } catch (e) { } }
5061 };
5062
5063 ws.forwardclient.onData = function (ciraconn, data) {
5064 //parent.debug('webrelaydata', 'Relay CIRA data to WS', data.length);
5065
5066 // Run data thru interceptor
5067 if (ws.interceptor) { data = ws.interceptor.processAmtData(data); }
5068
5069 //console.log('AMT --> WS', Buffer.from(data, 'binary').toString('hex'));
5070 if (data.length > 0) {
5071 if (ws.logfile == null) {
5072 try { ws.send(data); } catch (e) { }
5073 } else {
5074 // Log to recording file
5075 obj.meshRelayHandler.recordingEntry(ws.logfile, 2, 0, data, function () { try { ws.send(data); } catch (ex) { console.log(ex); } });
5076 }
5077 }
5078 };
5079
5080 // TODO: Flow control? (Dont' really need it with AMT, but would be nice)
5081 ws.forwardclient.onSendOk = function (ciraconn) { };
5082 }
5083
5084 // When data is received from the web socket, forward the data into the associated CIRA channel.
5085 // If the CIRA connection is pending, the CIRA channel has built-in buffering, so we are ok sending anyway.
5086 ws.on('message', function (data) {
5087 //parent.debug('webrelaydata', 'Relay WS data to CIRA', data.length);
5088 if (typeof data == 'string') { data = Buffer.from(data, 'binary'); }
5089
5090 // WS ---> AMT/TLS
5091 if (ws.interceptor) { data = ws.interceptor.processBrowserData(data); } // Run data thru interceptor
5092
5093 // Log to recording file
5094 if (ws.logfile == null) {
5095 // Forward data to the associated TCP connection.
5096 try { ws.forwardclient.write(data); } catch (ex) { }
5097 } else {
5098 // Log to recording file
5099 obj.meshRelayHandler.recordingEntry(ws.logfile, 2, 2, data, function () { try { ws.forwardclient.write(data); } catch (ex) { } });
5100 }
5101 });
5102
5103 // If error, close the associated TCP connection.
5104 ws.on('error', function (err) {
5105 console.log('CIRA server websocket error from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.');
5106 parent.debug('webrelay', 'Websocket relay closed on error.');
5107
5108 // Log the disconnection
5109 if (ws.time) {
5110 if (req.query.p == 2) { // Only log event if Intel Redirection, otherwise hundreds of logs for WSMAN are recorded
5111 var msg = 'Ended relay session', msgid = 9, ip = ((ciraconn != null) ? ciraconn.remoteAddr : (((conn & 4) != 0) ? node.host : req.clientIp));
5112 if (user) {
5113 var event = { etype: 'relay', action: 'relaylog', domain: domain.id, userid: user._id, username: user.name, msgid: msgid, msgArgs: [ws.id, req.clientIp, ip, Math.floor((Date.now() - ws.time) / 1000)], msg: msg + ' \"' + ws.id + '\" from ' + req.clientIp + ' to ' + ip + ', ' + Math.floor((Date.now() - ws.time) / 1000) + ' second(s)', protocol: 101, nodeid: node._id };
5114 obj.parent.DispatchEvent(['*', user._id, node._id, node.meshid], obj, event);
5115 }
5116 }
5117 }
5118
5119 // Websocket closed, close the CIRA channel and TLS session.
5120 if (ws.forwardclient) {
5121 if (ws.forwardclient.close) { ws.forwardclient.close(); } // NonTLS, close the CIRA channel
5122 if (ws.forwardclient.end) { ws.forwardclient.end(); } // TLS, close the TLS session
5123 if (ws.forwardclient.chnl) { ws.forwardclient.chnl.close(); } // TLS, close the CIRA channel
5124 delete ws.forwardclient;
5125 }
5126
5127 // Close the recording file
5128 if (ws.logfile != null) {
5129 setTimeout(function(){ // wait 5 seconds before finishing file for some reason?
5130 obj.meshRelayHandler.recordingEntry(ws.logfile, 3, 0, 'MeshCentralMCREC', function (logfile, ws) {
5131 obj.fs.close(logfile.fd);
5132 parent.debug('relay', 'Relay: Finished recording to file: ' + ws.logfile.filename);
5133 // Compute session length
5134 var sessionLength = null;
5135 if (ws.logfile.startTime != null) { sessionLength = Math.round((Date.now() - ws.logfile.startTime) / 1000) - 5; }
5136 // Add a event entry about this recording
5137 var basefile = parent.path.basename(ws.logfile.filename);
5138 var event = { etype: 'relay', action: 'recording', domain: domain.id, nodeid: ws.logfile.nodeid, msg: "Finished recording session" + (sessionLength ? (', ' + sessionLength + ' second(s)') : ''), filename: basefile, size: ws.logfile.size };
5139 if (user) { event.userids = [user._id]; } else if (peer.user) { event.userids = [peer.user._id]; }
5140 var xprotocol = (((ws.logfile.req == null) || (ws.logfile.req.query == null)) ? null : (ws.logfile.req.query.p == 2) ? 101 : 100);
5141 if (xprotocol != null) { event.protocol = parseInt(xprotocol); }
5142 var mesh = obj.meshes[ws.logfile.meshid];
5143 if (mesh != null) { event.meshname = mesh.name; event.meshid = mesh._id; }
5144 if (ws.logfile.startTime) { event.startTime = ws.logfile.startTime; event.lengthTime = sessionLength; }
5145 if (ws.logfile.name) { event.name = ws.logfile.name; }
5146 if (ws.logfile.icon) { event.icon = ws.logfile.icon; }
5147 obj.parent.DispatchEvent(['*', 'recording', ws.logfile.nodeid, ws.logfile.meshid], obj, event);
5148 delete ws.logfile;
5149 }, ws);
5150 }, 5000);
5151 }
5152 });
5153
5154 // If the web socket is closed, close the associated TCP connection.
5155 ws.on('close', function () {
5156 parent.debug('webrelay', 'Websocket relay closed.');
5157
5158 // Log the disconnection
5159 if (ws.time) {
5160 if (req.query.p == 2) { // Only log event if Intel Redirection, otherwise hundreds of logs for WSMAN are recorded
5161 var msg = 'Ended relay session', msgid = 9, ip = ((ciraconn != null) ? ciraconn.remoteAddr : (((conn & 4) != 0) ? node.host : req.clientIp));
5162 var nodeid = node._id;
5163 var meshid = node.meshid;
5164 if (user) {
5165 var event = { etype: 'relay', action: 'relaylog', domain: domain.id, userid: user._id, username: user.name, msgid: msgid, msgArgs: [ws.id, req.clientIp, ip, Math.floor((Date.now() - ws.time) / 1000)], msg: msg + ' \"' + ws.id + '\" from ' + req.clientIp + ' to ' + ip + ', ' + Math.floor((Date.now() - ws.time) / 1000) + ' second(s)', protocol: ((req.query.p == 2) ? 101 : 100), nodeid: nodeid };
5166 obj.parent.DispatchEvent(['*', user._id, nodeid, meshid], obj, event);
5167 }
5168 }
5169 }
5170
5171 // Websocket closed, close the CIRA channel and TLS session.
5172 if (ws.forwardclient) {
5173 if (ws.forwardclient.close) { ws.forwardclient.close(); } // NonTLS, close the CIRA channel
5174 if (ws.forwardclient.end) { ws.forwardclient.end(); } // TLS, close the TLS session
5175 if (ws.forwardclient.chnl) { ws.forwardclient.chnl.close(); } // TLS, close the CIRA channel
5176 delete ws.forwardclient;
5177 }
5178
5179 // Close the recording file
5180 if (ws.logfile != null) {
5181 setTimeout(function(){ // wait 5 seconds before finishing file for some reason?
5182 obj.meshRelayHandler.recordingEntry(ws.logfile, 3, 0, 'MeshCentralMCREC', function (logfile, ws) {
5183 obj.fs.close(logfile.fd);
5184 parent.debug('relay', 'Relay: Finished recording to file: ' + ws.logfile.filename);
5185 // Compute session length
5186 var sessionLength = null;
5187 if (ws.logfile.startTime != null) { sessionLength = Math.round((Date.now() - ws.logfile.startTime) / 1000) - 5; }
5188 // Add a event entry about this recording
5189 var basefile = parent.path.basename(ws.logfile.filename);
5190 var event = { etype: 'relay', action: 'recording', domain: domain.id, nodeid: ws.logfile.nodeid, msg: "Finished recording session" + (sessionLength ? (', ' + sessionLength + ' second(s)') : ''), filename: basefile, size: ws.logfile.size };
5191 if (user) { event.userids = [user._id]; }
5192 var xprotocol = (((ws.logfile.req == null) || (ws.logfile.req.query == null)) ? null : (ws.logfile.req.query.p == 2) ? 101 : 100);
5193 if (xprotocol != null) { event.protocol = parseInt(xprotocol); }
5194 var mesh = obj.meshes[ws.logfile.meshid];
5195 if (mesh != null) { event.meshname = mesh.name; event.meshid = mesh._id; }
5196 if (ws.logfile.startTime) { event.startTime = ws.logfile.startTime; event.lengthTime = sessionLength; }
5197 if (ws.logfile.name) { event.name = ws.logfile.name; }
5198 if (ws.logfile.icon) { event.icon = ws.logfile.icon; }
5199 obj.parent.DispatchEvent(['*', 'recording', ws.logfile.nodeid, ws.logfile.meshid], obj, event);
5200 delete ws.logfile;
5201 }, ws);
5202 }, 5000);
5203 }
5204 });
5205
5206 // Note that here, req.query.p: 1 = WSMAN with server auth, 2 = REDIR with server auth, 3 = WSMAN without server auth, 4 = REDIR with server auth
5207
5208 // Fetch Intel AMT credentials & Setup interceptor
5209 if (req.query.p == 1) {
5210 parent.debug('webrelaydata', 'INTERCEPTOR1', { host: node.host, port: port, user: node.intelamt.user, pass: node.intelamt.pass });
5211 ws.interceptor = obj.interceptor.CreateHttpInterceptor({ host: node.host, port: port, user: node.intelamt.user, pass: node.intelamt.pass });
5212 ws.interceptor.blockAmtStorage = true;
5213 } else if (req.query.p == 2) {
5214 parent.debug('webrelaydata', 'INTERCEPTOR2', { user: node.intelamt.user, pass: node.intelamt.pass });
5215 ws.interceptor = obj.interceptor.CreateRedirInterceptor({ user: node.intelamt.user, pass: node.intelamt.pass });
5216 ws.interceptor.blockAmtStorage = true;
5217 }
5218 } else if ((conn & 4) != 0) { // If Intel AMT direct connection is possible, option a direct socket
5219 // We got a new web socket connection, initiate a TCP connection to the target Intel AMT host/port.
5220 parent.debug('webrelay', 'Opening relay TCP socket connection to ' + req.query.host + '.');
5221
5222 // When data is received from the web socket, forward the data into the associated TCP connection.
5223 ws.on('message', function (msg) {
5224 //parent.debug('webrelaydata', 'TCP relay data to ' + node.host + ', ' + msg.length + ' bytes');
5225
5226 if (typeof msg == 'string') { msg = Buffer.from(msg, 'binary'); }
5227 if (ws.interceptor) { msg = ws.interceptor.processBrowserData(msg); } // Run data thru interceptor
5228
5229 // Log to recording file
5230 if (ws.logfile == null) {
5231 // Forward data to the associated TCP connection.
5232 try { ws.forwardclient.write(msg); } catch (ex) { }
5233 } else {
5234 // Log to recording file
5235 obj.meshRelayHandler.recordingEntry(ws.logfile, 2, 2, msg, function () { try { ws.forwardclient.write(msg); } catch (ex) { } });
5236 }
5237 });
5238
5239 // If error, close the associated TCP connection.
5240 ws.on('error', function (err) {
5241 console.log('Error with relay web socket connection from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.');
5242 parent.debug('webrelay', 'Error with relay web socket connection from ' + req.clientIp + '.');
5243 // Log the disconnection
5244 if (ws.time) {
5245 if (req.query.p == 2) { // Only log event if Intel Redirection, otherwise hundreds of logs for WSMAN are recorded
5246 var msg = 'Ended relay session', msgid = 9, ip = ((ciraconn != null) ? ciraconn.remoteAddr : (((conn & 4) != 0) ? node.host : req.clientIp));
5247 if (user) {
5248 var event = { etype: 'relay', action: 'relaylog', domain: domain.id, userid: user._id, username: user.name, msgid: msgid, msgArgs: [ws.id, req.clientIp, ip, Math.floor((Date.now() - ws.time) / 1000)], msg: msg + ' \"' + ws.id + '\" from ' + req.clientIp + ' to ' + ip + ', ' + Math.floor((Date.now() - ws.time) / 1000) + ' second(s)', protocol: ((req.query.p == 2) ? 101 : 100), nodeid: node._id };
5249 obj.parent.DispatchEvent(['*', user._id, node._id, node.meshid], obj, event);
5250 }
5251 }
5252 }
5253 if (ws.forwardclient) { try { ws.forwardclient.destroy(); } catch (e) { } }
5254
5255 // Close the recording file
5256 if (ws.logfile != null) {
5257 setTimeout(function(){ // wait 5 seconds before finishing file for some reason?
5258 obj.meshRelayHandler.recordingEntry(ws.logfile, 3, 0, 'MeshCentralMCREC', function (logfile, ws) {
5259 obj.fs.close(logfile.fd);
5260 parent.debug('relay', 'Relay: Finished recording to file: ' + ws.logfile.filename);
5261 // Compute session length
5262 var sessionLength = null;
5263 if (ws.logfile.startTime != null) { sessionLength = Math.round((Date.now() - ws.logfile.startTime) / 1000); }
5264 // Add a event entry about this recording
5265 var basefile = parent.path.basename(ws.logfile.filename);
5266 var event = { etype: 'relay', action: 'recording', domain: domain.id, nodeid: ws.logfile.nodeid, msg: "Finished recording session" + (sessionLength ? (', ' + sessionLength + ' second(s)') : ''), filename: basefile, size: ws.logfile.size };
5267 if (user) { event.userids = [user._id]; } else if (peer.user) { event.userids = [peer.user._id]; }
5268 var xprotocol = (((ws.logfile.req == null) || (ws.logfile.req.query == null)) ? null : (ws.logfile.req.query.p == 2) ? 101 : 100);
5269 if (xprotocol != null) { event.protocol = parseInt(xprotocol); }
5270 var mesh = obj.meshes[ws.logfile.meshid];
5271 if (mesh != null) { event.meshname = mesh.name; event.meshid = mesh._id; }
5272 if (ws.logfile.startTime) { event.startTime = ws.logfile.startTime; event.lengthTime = sessionLength; }
5273 if (ws.logfile.name) { event.name = ws.logfile.name; }
5274 if (ws.logfile.icon) { event.icon = ws.logfile.icon; }
5275 obj.parent.DispatchEvent(['*', 'recording', ws.logfile.nodeid, ws.logfile.meshid], obj, event);
5276 delete ws.logfile;
5277 }, ws);
5278 }, 5000);
5279 }
5280 });
5281
5282 // If the web socket is closed, close the associated TCP connection.
5283 ws.on('close', function () {
5284 parent.debug('webrelay', 'Closing relay web socket connection to ' + req.query.host + '.');
5285 // Log the disconnection
5286 if (ws.time) {
5287 if (req.query.p == 2) { // Only log event if Intel Redirection, otherwise hundreds of logs for WSMAN are recorded
5288 var msg = 'Ended relay session', msgid = 9, ip = ((ciraconn != null) ? ciraconn.remoteAddr : (((conn & 4) != 0) ? node.host : req.clientIp));
5289 if (user) {
5290 var event = { etype: 'relay', action: 'relaylog', domain: domain.id, userid: user._id, username: user.name, msgid: msgid, msgArgs: [ws.id, req.clientIp, ip, Math.floor((Date.now() - ws.time) / 1000)], msg: msg + ' \"' + ws.id + '\" from ' + req.clientIp + ' to ' + ip + ', ' + Math.floor((Date.now() - ws.time) / 1000) + ' second(s)', protocol: ((req.query.p == 2) ? 101 : 100), nodeid: node._id };
5291 obj.parent.DispatchEvent(['*', user._id, node._id, node.meshid], obj, event);
5292 }
5293 }
5294 }
5295 if (ws.forwardclient) { try { ws.forwardclient.destroy(); } catch (e) { } }
5296
5297 // Close the recording file
5298 if (ws.logfile != null) {
5299 setTimeout(function(){ // wait 5 seconds before finishing file for some reason?
5300 obj.meshRelayHandler.recordingEntry(ws.logfile, 3, 0, 'MeshCentralMCREC', function (logfile, ws) {
5301 obj.fs.close(logfile.fd);
5302 parent.debug('relay', 'Relay: Finished recording to file: ' + ws.logfile.filename);
5303 // Compute session length
5304 var sessionLength = null;
5305 if (ws.logfile.startTime != null) { sessionLength = Math.round((Date.now() - ws.logfile.startTime) / 1000); }
5306 // Add a event entry about this recording
5307 var basefile = parent.path.basename(ws.logfile.filename);
5308 var event = { etype: 'relay', action: 'recording', domain: domain.id, nodeid: ws.logfile.nodeid, msg: "Finished recording session" + (sessionLength ? (', ' + sessionLength + ' second(s)') : ''), filename: basefile, size: ws.logfile.size };
5309 if (user) { event.userids = [user._id]; } else if (peer.user) { event.userids = [peer.user._id]; }
5310 var xprotocol = (((ws.logfile.req == null) || (ws.logfile.req.query == null)) ? null : (ws.logfile.req.query.p == 2) ? 101 : 100);
5311 if (xprotocol != null) { event.protocol = parseInt(xprotocol); }
5312 var mesh = obj.meshes[ws.logfile.meshid];
5313 if (mesh != null) { event.meshname = mesh.name; event.meshid = mesh._id; }
5314 if (ws.logfile.startTime) { event.startTime = ws.logfile.startTime; event.lengthTime = sessionLength; }
5315 if (ws.logfile.name) { event.name = ws.logfile.name; }
5316 if (ws.logfile.icon) { event.icon = ws.logfile.icon; }
5317 obj.parent.DispatchEvent(['*', 'recording', ws.logfile.nodeid, ws.logfile.meshid], obj, event);
5318 delete ws.logfile;
5319 }, ws);
5320 }, 5000);
5321 }
5322 });
5323
5324 // Compute target port
5325 var port = 16992;
5326 if (node.intelamt.tls > 0) port = 16993; // This is a direct connection, use TLS when possible
5327 if ((req.query.p == 2) || (req.query.p == 4)) port += 2;
5328
5329 if (node.intelamt.tls == 0) {
5330 // If this is TCP (without TLS) set a normal TCP socket
5331 ws.forwardclient = new obj.net.Socket();
5332 ws.forwardclient.setEncoding('binary');
5333 ws.forwardclient.xstate = 0;
5334 ws.forwardclient.forwardwsocket = ws;
5335 ws._socket.resume();
5336 } else {
5337 // If TLS is going to be used, setup a TLS socket
5338 var tlsoptions = { ciphers: 'RSA+AES:!aNULL:!MD5:!DSS', secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE | constants.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION, rejectUnauthorized: false };
5339 if (req.query.tls1only == 1) {
5340 tlsoptions.secureProtocol = 'TLSv1_method';
5341 } else {
5342 tlsoptions.minVersion = 'TLSv1';
5343 }
5344 ws.forwardclient = obj.tls.connect(port, node.host, tlsoptions, function () {
5345 // The TLS connection method is the same as TCP, but located a bit differently.
5346 parent.debug('webrelay', user.name + ' - TLS connected to ' + node.host + ':' + port + '.');
5347 ws.forwardclient.xstate = 1;
5348 ws._socket.resume();
5349 });
5350 ws.forwardclient.setEncoding('binary');
5351 ws.forwardclient.xstate = 0;
5352 ws.forwardclient.forwardwsocket = ws;
5353 }
5354
5355 // When we receive data on the TCP connection, forward it back into the web socket connection.
5356 ws.forwardclient.on('data', function (data) {
5357 if (typeof data == 'string') { data = Buffer.from(data, 'binary'); }
5358 if (obj.parent.debugLevel >= 1) { // DEBUG
5359 parent.debug('webrelaydata', user.name + ' - TCP relay data from ' + node.host + ', ' + data.length + ' bytes.');
5360 //if (obj.parent.debugLevel >= 4) { Debug(4, ' ' + Buffer.from(data, 'binary').toString('hex')); }
5361 }
5362 if (ws.interceptor) { data = ws.interceptor.processAmtData(data); } // Run data thru interceptor
5363 if (ws.logfile == null) {
5364 // No logging
5365 try { ws.send(data); } catch (e) { }
5366 } else {
5367 // Log to recording file
5368 obj.meshRelayHandler.recordingEntry(ws.logfile, 2, 0, data, function () { try { ws.send(data); } catch (e) { } });
5369 }
5370 });
5371
5372 // If the TCP connection closes, disconnect the associated web socket.
5373 ws.forwardclient.on('close', function () {
5374 parent.debug('webrelay', user.name + ' - TCP relay disconnected from ' + node.host + ':' + port + '.');
5375 try { ws.close(); } catch (e) { }
5376 });
5377
5378 // If the TCP connection causes an error, disconnect the associated web socket.
5379 ws.forwardclient.on('error', function (err) {
5380 parent.debug('webrelay', user.name + ' - TCP relay error from ' + node.host + ':' + port + ': ' + err);
5381 try { ws.close(); } catch (e) { }
5382 });
5383
5384 // Fetch Intel AMT credentials & Setup interceptor
5385 if (req.query.p == 1) { ws.interceptor = obj.interceptor.CreateHttpInterceptor({ host: node.host, port: port, user: node.intelamt.user, pass: node.intelamt.pass }); }
5386 else if (req.query.p == 2) { ws.interceptor = obj.interceptor.CreateRedirInterceptor({ user: node.intelamt.user, pass: node.intelamt.pass }); }
5387
5388 if (node.intelamt.tls == 0) {
5389 // A TCP connection to Intel AMT just connected, start forwarding.
5390 ws.forwardclient.connect(port, node.host, function () {
5391 parent.debug('webrelay', user.name + ' - TCP relay connected to ' + node.host + ':' + port + '.');
5392 ws.forwardclient.xstate = 1;
5393 ws._socket.resume();
5394 });
5395 }
5396 }
5397
5398 // Log the connection
5399 if (user != null) {
5400 if (req.query.p == 2) { // Only log event if Intel Redirection, otherwise hundreds of logs for WSMAN are recorded
5401 var msg = 'Started relay session', msgid = 13, ip = ((ciraconn != null) ? ciraconn.remoteAddr : (((conn & 4) != 0) ? node.host : req.clientIp));
5402 var event = { etype: 'relay', action: 'relaylog', domain: domain.id, userid: user._id, username: user.name, msgid: msgid, msgArgs: [ws.id, req.clientIp, ip], msg: msg + ' \"' + ws.id + '\" from ' + req.clientIp + ' to ' + ip, protocol: 101, nodeid: node._id };
5403 obj.parent.DispatchEvent(['*', user._id], obj, event);
5404 }
5405
5406 // Update user last access time
5407 if ((user != null)) {
5408 const timeNow = Math.floor(Date.now() / 1000);
5409 if (user.access < (timeNow - 300)) { // Only update user access time if longer than 5 minutes
5410 user.access = timeNow;
5411 obj.parent.db.SetUser(user);
5412
5413 // Event the change
5414 var message = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', domain: domain.id, nolog: 1 };
5415 if (parent.db.changeStream) { message.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
5416 var targets = ['*', 'server-users', user._id];
5417 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
5418 obj.parent.DispatchEvent(targets, obj, message);
5419 }
5420 }
5421 }
5422 });
5423 }
5424
5425 // Setup agent to/from server file transfer handler
5426 function handleAgentFileTransfer(ws, req) {
5427 var domain = checkAgentIpAddress(ws, req);
5428 if (domain == null) { parent.debug('web', 'Got agent file transfer connection with bad domain or blocked IP address ' + req.clientIp + ', dropping.'); ws.close(); return; }
5429 if (req.query.c == null) { parent.debug('web', 'Got agent file transfer connection without a cookie from ' + req.clientIp + ', dropping.'); ws.close(); return; }
5430 var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 10); // 10 minute timeout
5431 if ((c == null) || (c.a != 'aft')) { parent.debug('web', 'Got agent file transfer connection with invalid cookie from ' + req.clientIp + ', dropping.'); ws.close(); return; }
5432 ws.xcmd = c.b; ws.xarg = c.c, ws.xfilelen = 0;
5433 ws.send('c'); // Indicate connection of the tunnel. In this case, we are the termination point.
5434 ws.send('5'); // Indicate we want to perform file transfers (5 = Files).
5435 if (ws.xcmd == 'coredump') {
5436 // Check the agent core dump folder if not already present.
5437 var coreDumpPath = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps');
5438 if (obj.fs.existsSync(coreDumpPath) == false) { try { obj.fs.mkdirSync(coreDumpPath); } catch (ex) { } }
5439 ws.xfilepath = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps', ws.xarg);
5440 ws.xid = 'coredump';
5441 ws.send(JSON.stringify({ action: 'download', sub: 'start', ask: 'coredump', id: 'coredump' })); // Ask for a core dump file
5442 }
5443
5444 // When data is received from the web socket, echo it back
5445 ws.on('message', function (data) {
5446 if (typeof data == 'string') {
5447 // Control message
5448 var cmd = null;
5449 try { cmd = JSON.parse(data); } catch (ex) { }
5450 if ((cmd == null) || (cmd.action != 'download') || (cmd.sub == null)) return;
5451 switch (cmd.sub) {
5452 case 'start': {
5453 // Perform an async file open
5454 var callback = function onFileOpen(err, fd) {
5455 onFileOpen.xws.xfile = fd;
5456 try { onFileOpen.xws.send(JSON.stringify({ action: 'download', sub: 'startack', id: onFileOpen.xws.xid, ack: 1 })); } catch (ex) { } // Ask for a directory (test)
5457 };
5458 callback.xws = this;
5459 obj.fs.open(this.xfilepath + '.part', 'w', callback);
5460 break;
5461 }
5462 }
5463 } else {
5464 // Binary message
5465 if (data.length < 4) return;
5466 var flags = data.readInt32BE(0);
5467 if ((data.length > 4)) {
5468 // Write the file
5469 this.xfilelen += (data.length - 4);
5470 try {
5471 var callback = function onFileDataWritten(err, bytesWritten, buffer) {
5472 if (onFileDataWritten.xflags & 1) {
5473 // End of file
5474 parent.debug('web', "Completed downloads of agent dumpfile, " + onFileDataWritten.xws.xfilelen + " bytes.");
5475 if (onFileDataWritten.xws.xfile) {
5476 obj.fs.close(onFileDataWritten.xws.xfile, function (err) { });
5477 obj.fs.rename(onFileDataWritten.xws.xfilepath + '.part', onFileDataWritten.xws.xfilepath, function (err) { });
5478 onFileDataWritten.xws.xfile = null;
5479 }
5480 try { onFileDataWritten.xws.send(JSON.stringify({ action: 'markcoredump' })); } catch (ex) { } // Ask to delete the core dump file
5481 try { onFileDataWritten.xws.close(); } catch (ex) { }
5482 } else {
5483 // Send ack
5484 try { onFileDataWritten.xws.send(JSON.stringify({ action: 'download', sub: 'ack', id: onFileDataWritten.xws.xid })); } catch (ex) { } // Ask for a directory (test)
5485 }
5486 };
5487 callback.xws = this;
5488 callback.xflags = flags;
5489 obj.fs.write(this.xfile, data, 4, data.length - 4, callback);
5490 } catch (ex) { }
5491 } else {
5492 if (flags & 1) {
5493 // End of file
5494 parent.debug('web', "Completed downloads of agent dumpfile, " + this.xfilelen + " bytes.");
5495 if (this.xfile) {
5496 obj.fs.close(this.xfile, function (err) { });
5497 obj.fs.rename(this.xfilepath + '.part', this.xfilepath, function (err) { });
5498 this.xfile = null;
5499 }
5500 this.send(JSON.stringify({ action: 'markcoredump' })); // Ask to delete the core dump file
5501 try { this.close(); } catch (ex) { }
5502 } else {
5503 // Send ack
5504 this.send(JSON.stringify({ action: 'download', sub: 'ack', id: this.xid })); // Ask for a directory (test)
5505 }
5506 }
5507 }
5508 });
5509
5510 // If error, do nothing.
5511 ws.on('error', function (err) { console.log('Agent file transfer server error from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.'); });
5512
5513 // If closed, do nothing
5514 ws.on('close', function (req) {
5515 if (this.xfile) {
5516 obj.fs.close(this.xfile, function (err) { });
5517 obj.fs.unlink(this.xfilepath + '.part', function (err) { }); // Remove a partial file
5518 }
5519 });
5520 }
5521
5522 // Handle the web socket echo request, just echo back the data sent
5523 function handleEchoWebSocket(ws, req) {
5524 const domain = checkUserIpAddress(ws, req);
5525 if (domain == null) { return; }
5526 ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive
5527
5528 // When data is received from the web socket, echo it back
5529 ws.on('message', function (data) {
5530 if (data.toString('utf8') == 'close') {
5531 try { ws.close(); } catch (e) { console.log(e); }
5532 } else {
5533 try { ws.send(data); } catch (e) { console.log(e); }
5534 }
5535 });
5536
5537 // If error, do nothing.
5538 ws.on('error', function (err) { console.log('Echo server error from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.'); });
5539
5540 // If closed, do nothing
5541 ws.on('close', function (req) { });
5542 }
5543
5544 // Handle the 2FA hold web socket
5545 // Accept an hold a web socket connection until the 2FA response is received.
5546 function handle2faHoldWebSocket(ws, req) {
5547 const domain = checkUserIpAddress(ws, req);
5548 if (domain == null) { return; }
5549 if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.push2factor == false)) { ws.close(); return; } // Push 2FA is disabled
5550 if (typeof req.query.c !== 'string') { ws.close(); return; }
5551 const cookie = parent.decodeCookie(req.query.c, null, 1);
5552 if ((cookie == null) || (cookie.d != domain.id)) { ws.close(); return; }
5553 var user = obj.users[cookie.u];
5554 if ((user == null) || (typeof user.otpdev != 'string')) { ws.close(); return; }
5555 ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive
5556
5557 // 2FA event subscription
5558 obj.parent.AddEventDispatch(['2fadev-' + cookie.s], ws);
5559 ws.cookie = cookie;
5560 ws.HandleEvent = function (source, event, ids, id) {
5561 obj.parent.RemoveAllEventDispatch(this);
5562 if ((event.approved === true) && (event.userid == this.cookie.u)) {
5563 // Create a login cookie
5564 const loginCookie = obj.parent.encodeCookie({ a: 'pushAuth', u: event.userid, d: event.domain }, obj.parent.loginCookieEncryptionKey);
5565 try { ws.send(JSON.stringify({ approved: true, token: loginCookie })); } catch (ex) { }
5566 } else {
5567 // Reject the login
5568 try { ws.send(JSON.stringify({ approved: false })); } catch (ex) { }
5569 }
5570 }
5571
5572 // We do not accept any data on this connection.
5573 ws.on('message', function (data) { this.close(); });
5574
5575 // If error, do nothing.
5576 ws.on('error', function (err) { });
5577
5578 // If closed, unsubscribe
5579 ws.on('close', function (req) { obj.parent.RemoveAllEventDispatch(this); });
5580
5581 // Perform push notification to device
5582 try {
5583 const deviceCookie = parent.encodeCookie({ a: 'checkAuth', c: cookie.c, u: cookie.u, n: cookie.n, s: cookie.s });
5584 var code = Buffer.from(cookie.c, 'base64').toString();
5585 var payload = { notification: { title: (domain.title ? domain.title : 'MeshCentral'), body: "Authentication - " + code }, data: { url: '2fa://auth?code=' + cookie.c + '&c=' + deviceCookie } };
5586 var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute
5587 parent.firebase.sendToDevice(user.otpdev, payload, options, function (id, err, errdesc) {
5588 if (err == null) {
5589 try { ws.send(JSON.stringify({ sent: true, code: code })); } catch (ex) { }
5590 } else {
5591 try { ws.send(JSON.stringify({ sent: false })); } catch (ex) { }
5592 }
5593 });
5594 } catch (ex) { console.log(ex); }
5595 }
5596
5597 // Get the total size of all files in a folder and all sub-folders. (TODO: try to make all async version)
5598 function readTotalFileSize(path) {
5599 var r = 0, dir;
5600 try { dir = obj.fs.readdirSync(path); } catch (e) { return 0; }
5601 for (var i in dir) {
5602 var stat = obj.fs.statSync(path + '/' + dir[i]);
5603 if ((stat.mode & 0x004000) == 0) { r += stat.size; } else { r += readTotalFileSize(path + '/' + dir[i]); }
5604 }
5605 return r;
5606 }
5607
5608 // Delete a folder and all sub items. (TODO: try to make all async version)
5609 function deleteFolderRec(path) {
5610 if (obj.fs.existsSync(path) == false) return;
5611 try {
5612 obj.fs.readdirSync(path).forEach(function (file, index) {
5613 var pathx = path + '/' + file;
5614 if (obj.fs.lstatSync(pathx).isDirectory()) { deleteFolderRec(pathx); } else { obj.fs.unlinkSync(pathx); }
5615 });
5616 obj.fs.rmdirSync(path);
5617 } catch (ex) { }
5618 }
5619
5620 // Handle Intel AMT events
5621 // To subscribe, add "http://server:port/amtevents.ashx" to Intel AMT subscriptions.
5622 obj.handleAmtEventRequest = function (req, res) {
5623 const domain = getDomain(req);
5624 try {
5625 if (req.headers.authorization) {
5626 var authstr = req.headers.authorization;
5627 if (authstr.substring(0, 7) == 'Digest ') {
5628 var auth = obj.common.parseNameValueList(obj.common.quoteSplit(authstr.substring(7)));
5629 if ((req.url === auth.uri) && (obj.httpAuthRealm === auth.realm) && (auth.opaque === obj.crypto.createHmac('SHA384', obj.httpAuthRandom).update(auth.nonce).digest('hex'))) {
5630
5631 // Read the data, we need to get the arg field
5632 var eventData = '';
5633 req.on('data', function (chunk) { eventData += chunk; });
5634 req.on('end', function () {
5635
5636 // Completed event read, let get the argument that must contain the nodeid
5637 var i = eventData.indexOf('<m:arg xmlns:m="http://x.com">');
5638 if (i > 0) {
5639 var nodeid = eventData.substring(i + 30, i + 30 + 64);
5640 if (nodeid.length == 64) {
5641 var nodekey = 'node/' + domain.id + '/' + nodeid;
5642
5643 // See if this node exists in the database
5644 obj.db.Get(nodekey, function (err, nodes) {
5645 if (nodes.length == 1) {
5646 // Yes, the node exists, compute Intel AMT digest password
5647 var node = nodes[0];
5648 var amtpass = obj.crypto.createHash('sha384').update(auth.username.toLowerCase() + ':' + nodeid + ":" + obj.parent.dbconfig.amtWsEventSecret).digest('base64').substring(0, 12).split('/').join('x').split('\\').join('x');
5649
5650 // Check the MD5 hash
5651 if (auth.response === obj.common.ComputeDigesthash(auth.username, amtpass, auth.realm, 'POST', auth.uri, auth.qop, auth.nonce, auth.nc, auth.cnonce)) {
5652
5653 // This is an authenticated Intel AMT event, update the host address
5654 var amthost = req.clientIp;
5655 if (amthost.substring(0, 7) === '::ffff:') { amthost = amthost.substring(7); }
5656 if (node.host != amthost) {
5657 // Get the mesh for this device
5658 var mesh = obj.meshes[node.meshid];
5659 if (mesh) {
5660 // Update the database
5661 var oldname = node.host;
5662 node.host = amthost;
5663 obj.db.Set(obj.cleanDevice(node));
5664
5665 // Event the node change
5666 var event = { etype: 'node', action: 'changenode', nodeid: node._id, domain: domain.id, msg: 'Intel(R) AMT host change ' + node.name + ' from group ' + mesh.name + ': ' + oldname + ' to ' + amthost };
5667
5668 // Remove the Intel AMT password before eventing this.
5669 event.node = node;
5670 if (event.node.intelamt && event.node.intelamt.pass) {
5671 event.node = Object.assign({}, event.node); // Shallow clone
5672 event.node.intelamt = Object.assign({}, event.node.intelamt); // Shallow clone
5673 delete event.node.intelamt.pass;
5674 }
5675
5676 if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the node. Another event will come.
5677 obj.parent.DispatchEvent(['*', node.meshid], obj, event);
5678 }
5679 }
5680
5681 if (parent.amtEventHandler) { parent.amtEventHandler.handleAmtEvent(eventData, nodeid, amthost); }
5682 //res.send('OK');
5683
5684 return;
5685 }
5686 }
5687 });
5688 }
5689 }
5690 });
5691 }
5692 }
5693 }
5694 } catch (e) { console.log(e); }
5695
5696 // Send authentication response
5697 obj.crypto.randomBytes(48, function (err, buf) {
5698 var nonce = buf.toString('hex'), opaque = obj.crypto.createHmac('SHA384', obj.httpAuthRandom).update(nonce).digest('hex');
5699 res.set({ 'WWW-Authenticate': 'Digest realm="' + obj.httpAuthRealm + '", qop="auth,auth-int", nonce="' + nonce + '", opaque="' + opaque + '"' });
5700 res.sendStatus(401);
5701 });
5702 };
5703
5704 // Handle a server backup request
5705 function handleBackupRequest(req, res) {
5706 const domain = checkUserIpAddress(req, res);
5707 if (domain == null) { return; }
5708 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
5709 if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
5710 if ((domain.myserver === false) || ((domain.myserver != null) && (domain.myserver.backup !== true))) { res.sendStatus(401); return; }
5711
5712 var user = obj.users[req.session.userid];
5713 if ((user == null) || ((user.siteadmin & 1) == 0)) { res.sendStatus(401); return; } // Check if we have server backup rights
5714
5715 // Require modules
5716 const archive = require('archiver')('zip', { level: 9 }); // Sets the compression method to maximum.
5717
5718 // Good practice to catch this error explicitly
5719 archive.on('error', function (err) { throw err; });
5720
5721 // Set the archive name
5722 res.attachment((domain.title ? domain.title : 'MeshCentral') + '-Backup-' + new Date().toLocaleDateString().replace('/', '-').replace('/', '-') + '.zip');
5723
5724 // Pipe archive data to the file
5725 archive.pipe(res);
5726
5727 // Append files from a glob pattern
5728 archive.directory(obj.parent.datapath, false);
5729
5730 // Finalize the archive (ie we are done appending files but streams have to finish yet)
5731 archive.finalize();
5732 }
5733
5734 // Handle a server restore request
5735 function handleRestoreRequest(req, res) {
5736 const domain = checkUserIpAddress(req, res);
5737 if (domain == null) { return; }
5738 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
5739 if ((domain.myserver === false) || ((domain.myserver != null) && (domain.myserver.restore !== true))) { res.sendStatus(401); return; }
5740
5741 var authUserid = null;
5742 if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
5743 const multiparty = require('multiparty');
5744 const form = new multiparty.Form();
5745 form.parse(req, function (err, fields, files) {
5746 // If an authentication cookie is embedded in the form, use that.
5747 if ((fields != null) && (fields.auth != null) && (fields.auth.length == 1) && (typeof fields.auth[0] == 'string')) {
5748 var loginCookie = obj.parent.decodeCookie(fields.auth[0], obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
5749 if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // Check cookie IP binding.
5750 if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { authUserid = loginCookie.userid; } // Use cookie authentication
5751 }
5752 if (authUserid == null) { res.sendStatus(401); return; }
5753
5754 // Get the user
5755 const user = obj.users[req.session.userid];
5756 if ((user == null) || ((user.siteadmin & 4) == 0)) { res.sendStatus(401); return; } // Check if we have server restore rights
5757
5758 res.set('Content-Type', 'text/html');
5759 res.end('<html><body>Server must be restarted, <a href="' + domain.url + '">click here to login</a>.</body></html>');
5760 parent.Stop(files.datafile[0].path);
5761 });
5762 }
5763
5764 // Handle a request to download a mesh agent
5765 obj.handleMeshAgentRequest = function (req, res) {
5766 var domain = getDomain(req, res);
5767 if (domain == null) { parent.debug('web', 'handleRootRequest: invalid domain.'); try { res.sendStatus(404); } catch (ex) { } return; }
5768
5769 // If required, check if this user has rights to do this
5770 if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true)) && (req.session.userid == null)) { res.sendStatus(401); return; }
5771
5772 if ((req.query.meshinstall != null) && (req.query.id != null)) {
5773 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { try { res.sendStatus(404); } catch (ex) { } return; } // Check 3FA URL key
5774
5775 // Send meshagent with included self installer for a specific platform back
5776 // Start by getting the .msh for this request
5777 var meshsettings = getMshFromRequest(req, res, domain);
5778 if (meshsettings == null) { try { res.sendStatus(401); } catch (ex) { } return; }
5779
5780 // Get the interactive install script, this only works for non-Windows agents
5781 var agentid = parseInt(req.query.meshinstall);
5782 var argentInfo = obj.parent.meshAgentBinaries[agentid];
5783 if (domain.meshAgentBinaries && domain.meshAgentBinaries[agentid]) { argentInfo = domain.meshAgentBinaries[agentid]; }
5784 var scriptInfo = obj.parent.meshAgentInstallScripts[6];
5785 if ((argentInfo == null) || (scriptInfo == null) || (argentInfo.platform == 'win32')) { try { res.sendStatus(404); } catch (ex) { } return; }
5786
5787 // Change the .msh file into JSON format and merge it into the install script
5788 var tokens, msh = {}, meshsettingslines = meshsettings.split('\r').join('').split('\n');
5789 for (var i in meshsettingslines) { tokens = meshsettingslines[i].split('='); if (tokens.length == 2) { msh[tokens[0]] = tokens[1]; } }
5790 var js = scriptInfo.data.replace('var msh = {};', 'var msh = ' + JSON.stringify(msh) + ';');
5791
5792 // Get the agent filename
5793 var meshagentFilename = 'meshagent';
5794 if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) { meshagentFilename = domain.agentcustomization.filename; }
5795
5796 setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename, null, 'meshagent');
5797 if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
5798 res.statusCode = 200;
5799 obj.parent.exeHandler.streamExeWithJavaScript({ platform: argentInfo.platform, sourceFileName: argentInfo.path, destinationStream: res, js: Buffer.from(js, 'utf8'), peinfo: argentInfo.pe });
5800 } else if (req.query.id != null) {
5801 // Send a specific mesh agent back
5802 var argentInfo = obj.parent.meshAgentBinaries[req.query.id];
5803 if (domain.meshAgentBinaries && domain.meshAgentBinaries[req.query.id]) { argentInfo = domain.meshAgentBinaries[req.query.id]; }
5804 if (argentInfo == null) { try { res.sendStatus(404); } catch (ex) { } return; }
5805
5806 // Download PDB debug files, only allowed for administrator or accounts with agent dump access
5807 if (req.query.pdb == 1) {
5808 if ((req.session == null) || (req.session.userid == null)) { try { res.sendStatus(404); } catch (ex) { } return; }
5809 var user = obj.users[req.session.userid];
5810 if (user == null) { try { res.sendStatus(404); } catch (ex) { } return; }
5811 if ((user != null) && ((user.siteadmin == 0xFFFFFFFF) || ((Array.isArray(obj.parent.config.settings.agentcoredumpusers)) && (obj.parent.config.settings.agentcoredumpusers.indexOf(user._id) >= 0)))) {
5812 if (argentInfo.id == 3) {
5813 setContentDispositionHeader(res, 'application/octet-stream', 'MeshService.pdb', null, 'MeshService.pdb');
5814 if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
5815 try { res.sendFile(argentInfo.path.split('MeshService-signed.exe').join('MeshService.pdb')); } catch (ex) { }
5816 return;
5817 }
5818 if (argentInfo.id == 4) {
5819 setContentDispositionHeader(res, 'application/octet-stream', 'MeshService64.pdb', null, 'MeshService64.pdb');
5820 if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
5821 try { res.sendFile(argentInfo.path.split('MeshService64-signed.exe').join('MeshService64.pdb')); } catch (ex) { }
5822 return;
5823 }
5824 }
5825 try { res.sendStatus(404); } catch (ex) { }
5826 return;
5827 }
5828
5829 if ((req.query.meshid == null) || (argentInfo.platform != 'win32')) {
5830 // Get the agent filename
5831 var meshagentFilename = argentInfo.rname;
5832 if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) { meshagentFilename = domain.agentcustomization.filename; }
5833 if (argentInfo.rname.endsWith('.apk') && !meshagentFilename.endsWith('.apk')) { meshagentFilename = meshagentFilename + '.apk'; }
5834 if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
5835 if (req.query.zip == 1) { if (argentInfo.zdata != null) { setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename + '.zip', null, 'meshagent.zip'); res.send(argentInfo.zdata); } else { try { res.sendStatus(404); } catch (ex) { } } return; } // Send compressed agent
5836 setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename, null, 'meshagent');
5837 if (argentInfo.data == null) { res.sendFile(argentInfo.path); } else { res.send(argentInfo.data); }
5838 return;
5839 } else {
5840 // Check if the meshid is a time limited, encrypted cookie
5841 var meshcookie = obj.parent.decodeCookie(req.query.meshid, obj.parent.invitationLinkEncryptionKey);
5842 if ((meshcookie != null) && (meshcookie.m != null)) { req.query.meshid = meshcookie.m; }
5843
5844 // We are going to embed the .msh file into the Windows executable (signed or not).
5845 // First, fetch the mesh object to build the .msh file
5846 var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.meshid];
5847 if (mesh == null) { try { res.sendStatus(401); } catch (ex) { } return; }
5848
5849 // If required, check if this user has rights to do this
5850 if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true))) {
5851 if ((domain.id != mesh.domain) || ((obj.GetMeshRights(req.session.userid, mesh) & 1) == 0)) { try { res.sendStatus(401); } catch (ex) { } return; }
5852 }
5853
5854 var meshidhex = Buffer.from(req.query.meshid.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
5855 var serveridhex = Buffer.from(obj.agentCertificateHashBase64.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
5856 var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port if specified
5857 if (obj.args.agentport != null) { httpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
5858 if (obj.args.agentaliasport != null) { httpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
5859
5860 // Prepare a mesh agent file name using the device group name.
5861 var meshfilename = mesh.name
5862 meshfilename = meshfilename.split('\\').join('').split('/').join('').split(':').join('').split('*').join('').split('?').join('').split('"').join('').split('<').join('').split('>').join('').split('|').join('').split(' ').join('').split('\'').join('');
5863 if (argentInfo.rname.endsWith('.exe')) { meshfilename = argentInfo.rname.substring(0, argentInfo.rname.length - 4) + '-' + meshfilename + '.exe'; } else { meshfilename = argentInfo.rname + '-' + meshfilename; }
5864
5865 // Customize the mesh agent file name
5866 if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) {
5867 meshfilename = meshfilename.split('meshagent').join(domain.agentcustomization.filename).split('MeshAgent').join(domain.agentcustomization.filename);
5868 }
5869
5870 // Get the agent connection server name
5871 var serverName = obj.getWebServerName(domain, req);
5872 if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
5873
5874 // Build the agent connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
5875 var xdomain = (domain.dns == null) ? domain.id : '';
5876 if (xdomain != '') xdomain += '/';
5877 var meshsettings = '';
5878 if (req.query.ac != '4') { // If MeshCentral Assistant Monitor Mode, DONT INCLUDE SERVER DETAILS!
5879 meshsettings += '\r\nMeshName=' + mesh.name + '\r\nMeshType=' + mesh.mtype + '\r\nMeshID=0x' + meshidhex + '\r\nServerID=' + serveridhex + '\r\n';
5880 if (obj.args.lanonly != true) { meshsettings += 'MeshServer=wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'agent.ashx\r\n'; } else {
5881 meshsettings += 'MeshServer=local\r\n';
5882 if ((obj.args.localdiscovery != null) && (typeof obj.args.localdiscovery.key == 'string') && (obj.args.localdiscovery.key.length > 0)) { meshsettings += 'DiscoveryKey=' + obj.args.localdiscovery.key + '\r\n'; }
5883 }
5884 if ((req.query.tag != null) && (typeof req.query.tag == 'string') && (obj.common.isAlphaNumeric(req.query.tag) == true)) { meshsettings += 'Tag=' + encodeURIComponent(req.query.tag) + '\r\n'; }
5885 if ((req.query.installflags != null) && (req.query.installflags != 0) && (parseInt(req.query.installflags) == req.query.installflags)) { meshsettings += 'InstallFlags=' + parseInt(req.query.installflags) + '\r\n'; }
5886 }
5887 if (req.query.id == '10006') { // Assistant settings and customizations
5888 if ((req.query.ac != null)) { meshsettings += 'AutoConnect=' + req.query.ac + '\r\n'; } // Set MeshCentral Assistant flags if needed. 0x01 = Always Connected, 0x02 = Not System Tray
5889 if (obj.args.assistantconfig) { for (var i in obj.args.assistantconfig) { meshsettings += obj.args.assistantconfig[i] + '\r\n'; } }
5890 if (domain.assistantconfig) { for (var i in domain.assistantconfig) { meshsettings += domain.assistantconfig[i] + '\r\n'; } }
5891 if ((domain.assistantnoproxy === true) || (obj.args.lanonly == true)) { meshsettings += 'ignoreProxyFile=1\r\n'; }
5892 if ((domain.assistantcustomization != null) && (typeof domain.assistantcustomization == 'object')) {
5893 if (typeof domain.assistantcustomization.title == 'string') { meshsettings += 'Title=' + domain.assistantcustomization.title + '\r\n'; }
5894 if (typeof domain.assistantcustomization.image == 'string') {
5895 try { meshsettings += 'Image=' + Buffer.from(obj.fs.readFileSync(parent.getConfigFilePath(domain.assistantcustomization.image)), 'binary').toString('base64') + '\r\n'; } catch (ex) { console.log(ex); }
5896 }
5897 if (req.query.ac != '4') {
5898 // Send with custom filename followed by device group name
5899 if (typeof domain.assistantcustomization.filename == 'string') { meshfilename = meshfilename.split('MeshCentralAssistant').join(domain.assistantcustomization.filename); }
5900 } else {
5901 // Send with custom filename, no device group name
5902 if (typeof domain.assistantcustomization.filename == 'string') { meshfilename = domain.assistantcustomization.filename + '.exe'; } else { meshfilename = 'MeshCentralAssistant.exe'; }
5903 }
5904 }
5905 } else { // Add agent customization, not for Assistant
5906 if (obj.args.agentconfig) { for (var i in obj.args.agentconfig) { meshsettings += obj.args.agentconfig[i] + '\r\n'; } }
5907 if (domain.agentconfig) { for (var i in domain.agentconfig) { meshsettings += domain.agentconfig[i] + '\r\n'; } }
5908 if ((domain.agentnoproxy === true) || (obj.args.lanonly == true)) { meshsettings += 'ignoreProxyFile=1\r\n'; }
5909 if (domain.agentcustomization != null) {
5910 if (domain.agentcustomization.displayname != null) { meshsettings += 'displayName=' + domain.agentcustomization.displayname + '\r\n'; }
5911 if (domain.agentcustomization.description != null) { meshsettings += 'description=' + domain.agentcustomization.description + '\r\n'; }
5912 if (domain.agentcustomization.companyname != null) { meshsettings += 'companyName=' + domain.agentcustomization.companyname + '\r\n'; }
5913 if (domain.agentcustomization.servicename != null) { meshsettings += 'meshServiceName=' + domain.agentcustomization.servicename + '\r\n'; }
5914 if (domain.agentcustomization.filename != null) { meshsettings += 'fileName=' + domain.agentcustomization.filename + '\r\n'; }
5915 if (domain.agentcustomization.image != null) { meshsettings += 'image=' + domain.agentcustomization.image + '\r\n'; }
5916 if (domain.agentcustomization.foregroundcolor != null) { meshsettings += checkAgentColorString('foreground=', domain.agentcustomization.foregroundcolor); }
5917 if (domain.agentcustomization.backgroundcolor != null) { meshsettings += checkAgentColorString('background=', domain.agentcustomization.backgroundcolor); }
5918 }
5919 if (domain.agentTranslations != null) { meshsettings += 'translation=' + domain.agentTranslations + '\r\n'; } // Translation strings, not for MeshCentral Assistant
5920 }
5921 setContentDispositionHeader(res, 'application/octet-stream', meshfilename, null, argentInfo.rname);
5922 if (argentInfo.mtime != null) { res.setHeader('Last-Modified', argentInfo.mtime.toUTCString()); }
5923 if (domain.meshAgentBinaries && domain.meshAgentBinaries[req.query.id]) {
5924 obj.parent.exeHandler.streamExeWithMeshPolicy({ platform: 'win32', sourceFileName: domain.meshAgentBinaries[req.query.id].path, destinationStream: res, msh: meshsettings, peinfo: domain.meshAgentBinaries[req.query.id].pe });
5925 } else {
5926 obj.parent.exeHandler.streamExeWithMeshPolicy({ platform: 'win32', sourceFileName: obj.parent.meshAgentBinaries[req.query.id].path, destinationStream: res, msh: meshsettings, peinfo: obj.parent.meshAgentBinaries[req.query.id].pe });
5927 }
5928 return;
5929 }
5930 } else if (req.query.script != null) {
5931 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { try { res.sendStatus(404); } catch (ex) { } return; } // Check 3FA URL key
5932
5933 // Send a specific mesh install script back
5934 var scriptInfo = obj.parent.meshAgentInstallScripts[req.query.script];
5935 if (scriptInfo == null) { try { res.sendStatus(404); } catch (ex) { } return; }
5936 setContentDispositionHeader(res, 'application/octet-stream', scriptInfo.rname, null, 'script');
5937 var data = scriptInfo.data;
5938 var cmdoptions = { wgetoptionshttp: '', wgetoptionshttps: '', curloptionshttp: '-L ', curloptionshttps: '-L ' }
5939 if (obj.isTrustedCert(domain) != true) {
5940 cmdoptions.wgetoptionshttps += '--no-check-certificate ';
5941 cmdoptions.curloptionshttps += '-k ';
5942 }
5943 if (domain.agentnoproxy === true) {
5944 cmdoptions.wgetoptionshttp += '--no-proxy ';
5945 cmdoptions.wgetoptionshttps += '--no-proxy ';
5946 cmdoptions.curloptionshttp += '--noproxy \'*\' ';
5947 cmdoptions.curloptionshttps += '--noproxy \'*\' ';
5948 }
5949 for (var i in cmdoptions) { data = data.split('{{{' + i + '}}}').join(cmdoptions[i]); }
5950 res.send(data);
5951 return;
5952 } else if (req.query.meshcmd != null) {
5953 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { try { res.sendStatus(404); } catch (ex) { } return; } // Check 3FA URL key
5954
5955 // Send meshcmd for a specific platform back
5956 var agentid = parseInt(req.query.meshcmd);
5957
5958 // If the agentid is 3 or 4, check if we have a signed MeshCmd.exe
5959 if ((agentid == 3) && (obj.parent.meshAgentBinaries[11000] != null)) { // Signed Windows MeshCmd.exe x86-32
5960 var stats = null, meshCmdPath = obj.parent.meshAgentBinaries[11000].path;
5961 try { stats = obj.fs.statSync(meshCmdPath); } catch (e) { }
5962 if ((stats != null)) {
5963 setContentDispositionHeader(res, 'application/octet-stream', 'meshcmd.exe', null, 'meshcmd');
5964 res.sendFile(meshCmdPath); return;
5965 }
5966 } else if ((agentid == 4) && (obj.parent.meshAgentBinaries[11001] != null)) { // Signed Windows MeshCmd64.exe x86-64
5967 var stats = null, meshCmd64Path = obj.parent.meshAgentBinaries[11001].path;
5968 try { stats = obj.fs.statSync(meshCmd64Path); } catch (e) { }
5969 if ((stats != null)) {
5970 setContentDispositionHeader(res, 'application/octet-stream', 'meshcmd.exe', null, 'meshcmd');
5971 res.sendFile(meshCmd64Path); return;
5972 }
5973 } else if ((agentid == 43) && (obj.parent.meshAgentBinaries[11002] != null)) { // Signed Windows MeshCmd64.exe ARM-64
5974 var stats = null, meshCmdAMR64Path = obj.parent.meshAgentBinaries[11002].path;
5975 try { stats = obj.fs.statSync(meshCmdAMR64Path); } catch (e) { }
5976 if ((stats != null)) {
5977 setContentDispositionHeader(res, 'application/octet-stream', 'meshcmd-arm64.exe', null, 'meshcmd');
5978 res.sendFile(meshCmdAMR64Path); return;
5979 }
5980 }
5981
5982 // No signed agents, we are going to merge a new MeshCmd.
5983 if (((agentid == 3) || (agentid == 4)) && (obj.parent.meshAgentBinaries[agentid + 10000] != null)) { agentid += 10000; } // Avoid merging javascript to a signed mesh agent.
5984 var argentInfo = obj.parent.meshAgentBinaries[agentid];
5985 if (domain.meshAgentBinaries && domain.meshAgentBinaries[agentid]) { argentInfo = domain.meshAgentBinaries[agentid]; }
5986 if ((argentInfo == null) || (obj.parent.defaultMeshCmd == null)) { try { res.sendStatus(404); } catch (ex) { } return; }
5987 setContentDispositionHeader(res, 'application/octet-stream', 'meshcmd' + ((req.query.meshcmd <= 4) ? '.exe' : ''), null, 'meshcmd');
5988 res.statusCode = 200;
5989
5990 if (argentInfo.signedMeshCmdPath != null) {
5991 // If we have a pre-signed MeshCmd, send that.
5992 res.sendFile(argentInfo.signedMeshCmdPath);
5993 } else {
5994 // Merge JavaScript to a unsigned agent and send that.
5995 obj.parent.exeHandler.streamExeWithJavaScript({ platform: argentInfo.platform, sourceFileName: argentInfo.path, destinationStream: res, js: Buffer.from(obj.parent.defaultMeshCmd, 'utf8'), peinfo: argentInfo.pe });
5996 }
5997 return;
5998 } else if (req.query.meshaction != null) {
5999 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { try { res.sendStatus(404); } catch (ex) { } return; } // Check 3FA URL key
6000 var user = obj.users[req.session.userid];
6001 if (user == null) {
6002 // Check if we have an authentication cookie
6003 var c = obj.parent.decodeCookie(req.query.auth, obj.parent.loginCookieEncryptionKey);
6004 if (c == null) { try { res.sendStatus(404); } catch (ex) { } return; }
6005
6006 // Download tools using a cookie
6007 if (c.download == req.query.meshaction) {
6008 if (req.query.meshaction == 'winrouter') {
6009 var p = null;
6010 if (obj.parent.meshToolsBinaries['MeshCentralRouter']) { p = obj.parent.meshToolsBinaries['MeshCentralRouter'].path; }
6011 if ((p == null) || (!obj.fs.existsSync(p))) { p = obj.path.join(__dirname, 'agents', 'MeshCentralRouter.exe'); }
6012 if (obj.fs.existsSync(p)) {
6013 setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralRouter.exe', null, 'MeshCentralRouter.exe');
6014 try { res.sendFile(p); } catch (ex) { }
6015 } else { try { res.sendStatus(404); } catch (ex) { } }
6016 return;
6017 } else if (req.query.meshaction == 'winassistant') {
6018 var p = null;
6019 if (obj.parent.meshToolsBinaries['MeshCentralAssistant']) { p = obj.parent.meshToolsBinaries['MeshCentralAssistant'].path; }
6020 if ((p == null) || (!obj.fs.existsSync(p))) { p = obj.path.join(__dirname, 'agents', 'MeshCentralAssistant.exe'); }
6021 if (obj.fs.existsSync(p)) {
6022 setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralAssistant.exe', null, 'MeshCentralAssistant.exe');
6023 try { res.sendFile(p); } catch (ex) { }
6024 } else { try { res.sendStatus(404); } catch (ex) { } }
6025 return;
6026 } else if (req.query.meshaction == 'macrouter') {
6027 var p = null;
6028 if (obj.parent.meshToolsBinaries['MeshCentralRouterMacOS']) { p = obj.parent.meshToolsBinaries['MeshCentralRouterMacOS'].path; }
6029 if ((p == null) || (!obj.fs.existsSync(p))) { p = obj.path.join(__dirname, 'agents', 'MeshCentralRouter.dmg'); }
6030 if (obj.fs.existsSync(p)) {
6031 setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralRouter.dmg', null, 'MeshCentralRouter.dmg');
6032 try { res.sendFile(p); } catch (ex) { }
6033 } else { try { res.sendStatus(404); } catch (ex) { } }
6034 return;
6035 }
6036 return;
6037 }
6038
6039 // Check if the cookie authenticates a user
6040 var user = null;
6041 if (c.userid != null) {
6042 user = obj.users[c.userid];
6043 }
6044
6045 // If no cookie auth, try JWT authentication (RMM+PSA Integration)
6046 if (user == null && obj.parent.jwtAuth) {
6047 const token = obj.parent.jwtAuth.extractToken(req);
6048 if (token) {
6049 // Synchronous check needed here, but we'll do our best
6050 // Note: This is a simplified approach for this specific endpoint
6051 obj.parent.debug('web', 'JWT auth check for meshaction endpoint');
6052 // For this endpoint, we'll need to refactor to use async/await pattern
6053 // For now, fall through if no JWT validation
6054 }
6055 }
6056
6057 if (user == null) { try { res.sendStatus(404); } catch (ex) { } return; }
6058 }
6059 if ((req.query.meshaction == 'route') && (req.query.nodeid != null)) {
6060 var nodeIdSplit = req.query.nodeid.split('/');
6061 if ((nodeIdSplit[0] != 'node') || (nodeIdSplit[1] != domain.id)) { try { res.sendStatus(401); } catch (ex) { } return; }
6062 obj.db.Get(req.query.nodeid, function (err, nodes) {
6063 if ((err != null) || (nodes.length != 1)) { try { res.sendStatus(401); } catch (ex) { } return; }
6064 var node = nodes[0];
6065
6066 // Create the meshaction.txt file for meshcmd.exe
6067 var meshaction = {
6068 action: req.query.meshaction,
6069 localPort: 1234,
6070 remoteName: node.name,
6071 remoteNodeId: node._id,
6072 remoteTarget: null,
6073 remotePort: 3389,
6074 username: '',
6075 password: '',
6076 serverId: obj.agentCertificateHashHex.toUpperCase(), // SHA384 of server HTTPS public key
6077 serverHttpsHash: Buffer.from(obj.webCertificateHashs[domain.id], 'binary').toString('hex').toUpperCase(), // SHA384 of server HTTPS certificate
6078 debugLevel: 0
6079 };
6080 if (user != null) { meshaction.username = user.name; }
6081 if (req.query.key != null) { meshaction.loginKey = req.query.key; }
6082 var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
6083 if (obj.args.lanonly != true) { meshaction.serverUrl = 'wss://' + obj.getWebServerName(domain, req) + ':' + httpsPort + '/' + ((domain.id == '') ? '' : (domain.id + '/')) + 'meshrelay.ashx'; }
6084
6085 setContentDispositionHeader(res, 'application/octet-stream', 'meshaction.txt', null, 'meshaction.txt');
6086 res.send(JSON.stringify(meshaction, null, ' '));
6087 return;
6088 });
6089 } else if (req.query.meshaction == 'generic') {
6090 var meshaction = {
6091 username: user.name,
6092 password: '',
6093 serverId: obj.agentCertificateHashHex.toUpperCase(), // SHA384 of server HTTPS public key
6094 serverHttpsHash: Buffer.from(obj.webCertificateHashs[domain.id], 'binary').toString('hex').toUpperCase(), // SHA384 of server HTTPS certificate
6095 debugLevel: 0
6096 };
6097 if (user != null) { meshaction.username = user.name; }
6098 if (req.query.key != null) { meshaction.loginKey = req.query.key; }
6099 var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
6100 if (obj.args.lanonly != true) { meshaction.serverUrl = 'wss://' + obj.getWebServerName(domain, req) + ':' + httpsPort + '/' + ((domain.id == '') ? '' : ('/' + domain.id)) + 'meshrelay.ashx'; }
6101 setContentDispositionHeader(res, 'application/octet-stream', 'meshaction.txt', null, 'meshaction.txt');
6102 res.send(JSON.stringify(meshaction, null, ' '));
6103 return;
6104 } else if (req.query.meshaction == 'winrouter') {
6105 var p = null;
6106 if (parent.meshToolsBinaries['MeshCentralRouter']) { p = parent.meshToolsBinaries['MeshCentralRouter'].path; }
6107 if ((p == null) || !obj.fs.existsSync(p)) { p = obj.path.join(__dirname, 'agents', 'MeshCentralRouter.exe'); }
6108 if (obj.fs.existsSync(p)) {
6109 setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralRouter.exe', null, 'MeshCentralRouter.exe');
6110 try { res.sendFile(p); } catch (ex) { }
6111 } else { try { res.sendStatus(404); } catch (ex) { } }
6112 return;
6113 } else if (req.query.meshaction == 'winassistant') {
6114 var p = null;
6115 if (parent.meshToolsBinaries['MeshCentralAssistant']) { p = parent.meshToolsBinaries['MeshCentralAssistant'].path; }
6116 if ((p == null) || !obj.fs.existsSync(p)) { p = obj.path.join(__dirname, 'agents', 'MeshCentralAssistant.exe'); }
6117 if (obj.fs.existsSync(p)) {
6118 setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralAssistant.exe', null, 'MeshCentralAssistant.exe');
6119 try { res.sendFile(p); } catch (ex) { }
6120 } else { try { res.sendStatus(404); } catch (ex) { } }
6121 return;
6122 } else if (req.query.meshaction == 'macrouter') {
6123 var p = null;
6124 if (parent.meshToolsBinaries['MeshCentralRouterMacOS']) { p = parent.meshToolsBinaries['MeshCentralRouterMacOS'].path; }
6125 if ((p == null) || !obj.fs.existsSync(p)) { p = obj.path.join(__dirname, 'agents', 'MeshCentralRouter.dmg'); }
6126 if (obj.fs.existsSync(p)) {
6127 setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralRouter.dmg', null, 'MeshCentralRouter.dmg');
6128 try { res.sendFile(p); } catch (ex) { }
6129 } else { try { res.sendStatus(404); } catch (ex) { } }
6130 return;
6131 } else {
6132 try { res.sendStatus(401); } catch (ex) { }
6133 return;
6134 }
6135 } else {
6136 domain = checkUserIpAddress(req, res); // Recheck the domain to apply user IP filtering.
6137 if (domain == null) return;
6138 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { try { res.sendStatus(404); } catch (ex) { } return; } // Check 3FA URL key
6139 if ((req.session == null) || (req.session.userid == null)) { try { res.sendStatus(404); } catch (ex) { } return; }
6140 var user = null, coreDumpsAllowed = false;
6141 if (typeof req.session.userid == 'string') { user = obj.users[req.session.userid]; }
6142 if (user == null) { try { res.sendStatus(404); } catch (ex) { } return; }
6143
6144 // Check if this user has access to agent core dumps
6145 if ((obj.parent.config.settings.agentcoredump === true) && ((user.siteadmin == 0xFFFFFFFF) || ((Array.isArray(obj.parent.config.settings.agentcoredumpusers)) && (obj.parent.config.settings.agentcoredumpusers.indexOf(user._id) >= 0)))) {
6146 coreDumpsAllowed = true;
6147
6148 if ((req.query.dldump != null) && obj.common.IsFilenameValid(req.query.dldump)) {
6149 // Download a dump file
6150 var dumpFile = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps', req.query.dldump);
6151 if (obj.fs.existsSync(dumpFile)) {
6152 setContentDispositionHeader(res, 'application/octet-stream', req.query.dldump, null, 'file.bin');
6153 res.sendFile(dumpFile); return;
6154 } else {
6155 try { res.sendStatus(404); } catch (ex) { } return;
6156 }
6157 }
6158
6159 if ((req.query.deldump != null) && obj.common.IsFilenameValid(req.query.deldump)) {
6160 // Delete a dump file
6161 try { obj.fs.unlinkSync(obj.path.join(parent.datapath, '..', 'meshcentral-coredumps', req.query.deldump)); } catch (ex) { console.log(ex); }
6162 }
6163
6164 if ((req.query.dumps != null) || (req.query.deldump != null)) {
6165 // Send list of agent core dumps
6166 var response = '<html><head><title>Mesh Agents Core Dumps</title><style>table,th,td { border:1px solid black;border-collapse:collapse;padding:3px; }</style></head><body style=overflow:auto><table>';
6167 response += '<tr style="background-color:lightgray"><th>ID</th><th>Upload Date</th><th>Description</th><th>Current</th><th>Dump</th><th>Size</th><th>Agent</th><th>Agent SHA384</th><th>NodeID</th><th></th></tr>';
6168
6169 var coreDumpPath = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps');
6170 if (obj.fs.existsSync(coreDumpPath)) {
6171 var files = obj.fs.readdirSync(coreDumpPath);
6172 var coredumps = [];
6173 for (var i in files) {
6174 var file = files[i];
6175 if (file.endsWith('.dmp')) {
6176 var fileSplit = file.substring(0, file.length - 4).split('-');
6177 if (fileSplit.length == 3) {
6178 var agentid = parseInt(fileSplit[0]);
6179 if ((isNaN(agentid) == false) && (obj.parent.meshAgentBinaries[agentid] != null)) {
6180 var agentinfo = obj.parent.meshAgentBinaries[agentid];
6181 if (domain.meshAgentBinaries && domain.meshAgentBinaries[agentid]) { argentInfo = domain.meshAgentBinaries[agentid]; }
6182 var filestats = obj.fs.statSync(obj.path.join(parent.datapath, '..', 'meshcentral-coredumps', file));
6183 coredumps.push({
6184 fileSplit: fileSplit,
6185 agentinfo: agentinfo,
6186 filestats: filestats,
6187 currentAgent: agentinfo.hashhex.startsWith(fileSplit[1].toLowerCase()),
6188 downloadUrl: req.originalUrl.split('?')[0] + '?dldump=' + file + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : ''),
6189 deleteUrl: req.originalUrl.split('?')[0] + '?deldump=' + file + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : ''),
6190 agentUrl: req.originalUrl.split('?')[0] + '?id=' + agentinfo.id + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : ''),
6191 time: new Date(filestats.ctime)
6192 });
6193 }
6194 }
6195 }
6196 }
6197 coredumps.sort(function (a, b) { if (a.time > b.time) return -1; if (a.time < b.time) return 1; return 0; });
6198 for (var i in coredumps) {
6199 var d = coredumps[i];
6200 response += '<tr><td>' + d.agentinfo.id + '</td><td>' + d.time.toDateString().split(' ').join('&nbsp;') + '</td><td>' + d.agentinfo.desc.split(' ').join('&nbsp;') + '</td>';
6201 response += '<td style=text-align:center>' + d.currentAgent + '</td><td><a download href="' + d.downloadUrl + '">Download</a></td><td style=text-align:right>' + d.filestats.size + '</td>';
6202 if (d.currentAgent) { response += '<td><a download href="' + d.agentUrl + '">Download</a></td>'; } else { response += '<td></td>'; }
6203 response += '<td>' + d.fileSplit[1].toLowerCase() + '</td><td>' + d.fileSplit[2] + '</td><td><a href="' + d.deleteUrl + '">Delete</a></td></tr>';
6204 }
6205 }
6206 response += '</table><a href="' + req.originalUrl.split('?')[0] + (req.query.key ? ('?key=' + encodeURIComponent(req.query.key)) : '') + '">Mesh Agents</a></body></html>';
6207 res.send(response);
6208 return;
6209 }
6210 }
6211
6212 if (req.query.cores != null) {
6213 // Send list of agent cores
6214 var response = '<html><head><title>Mesh Agents Cores</title><style>table,th,td { border:1px solid black;border-collapse:collapse;padding:3px; }</style></head><body style=overflow:auto><table>';
6215 response += '<tr style="background-color:lightgray"><th>Name</th><th>Size</th><th>Comp</th><th>Decompressed Hash SHA384</th></tr>';
6216 for (var i in parent.defaultMeshCores) {
6217 response += '<tr><td>' + i.split(' ').join('&nbsp;') + '</td><td style="text-align:right"><a download href="/meshagents?dlcore=' + i + '">' + parent.defaultMeshCores[i].length + (req.query.key ? ('?key=' + encodeURIComponent(req.query.key)) : '') + '</a></td><td style="text-align:right"><a download href="/meshagents?dlccore=' + i + (req.query.key ? ('?key=' + encodeURIComponent(req.query.key)) : '') + '">' + parent.defaultMeshCoresDeflate[i].length + '</a></td><td>' + Buffer.from(parent.defaultMeshCoresHash[i], 'binary').toString('hex') + '</td></tr>';
6218 }
6219 response += '</table><a href="' + req.originalUrl.split('?')[0] + (req.query.key ? ('?key=' + encodeURIComponent(req.query.key)) : '') + '">Mesh Agents</a></body></html>';
6220 res.send(response);
6221 return;
6222 }
6223
6224 if (req.query.dlcore != null) {
6225 // Download mesh core
6226 var bin = parent.defaultMeshCores[req.query.dlcore];
6227 if ((bin == null) || (bin.length < 5)) { try { res.sendStatus(404); } catch (ex) { } return; }
6228 setContentDispositionHeader(res, 'application/octet-stream', encodeURIComponent(req.query.dlcore) + '.js', null, 'meshcore.js');
6229 res.send(bin.slice(4));
6230 return;
6231 }
6232
6233 if (req.query.dlccore != null) {
6234 // Download compressed mesh core
6235 var bin = parent.defaultMeshCoresDeflate[req.query.dlccore];
6236 if (bin == null) { try { res.sendStatus(404); } catch (ex) { } return; }
6237 setContentDispositionHeader(res, 'application/octet-stream', req.query.dlccore + '.js.deflate', null, 'meshcore.js.deflate');
6238 res.send(bin);
6239 return;
6240 }
6241
6242 // Send a list of available mesh agents
6243 var response = '<html><head><title>Mesh Agents</title><style>table,th,td { border:1px solid black;border-collapse:collapse;padding:3px; }</style></head><body style=overflow:auto><table>';
6244 response += '<tr style="background-color:lightgray"><th>ID</th><th>Description</th><th>Link</th><th>Size</th><th>SHA384</th><th>MeshCmd</th></tr>';
6245 var originalUrl = req.originalUrl.split('?')[0];
6246 for (var agentid in obj.parent.meshAgentBinaries) {
6247 if ((agentid >= 10000) && (agentid != 10005)) continue;
6248 var agentinfo = obj.parent.meshAgentBinaries[agentid];
6249 if (domain.meshAgentBinaries && domain.meshAgentBinaries[agentid]) { argentInfo = domain.meshAgentBinaries[agentid]; }
6250 response += '<tr><td>' + agentinfo.id + '</td><td>' + agentinfo.desc.split(' ').join('&nbsp;') + '</td>';
6251 response += '<td><a download href="' + originalUrl + '?id=' + agentinfo.id + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : '') + '">' + agentinfo.rname + '</a>';
6252 if ((user.siteadmin == 0xFFFFFFFF) || ((Array.isArray(obj.parent.config.settings.agentcoredumpusers)) && (obj.parent.config.settings.agentcoredumpusers.indexOf(user._id) >= 0))) {
6253 if ((agentid == 3) || (agentid == 4)) { response += ', <a download href="' + originalUrl + '?id=' + agentinfo.id + '&pdb=1' + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : '') + '">PDB</a>'; }
6254 }
6255 if (agentinfo.zdata != null) { response += ', <a download href="' + originalUrl + '?id=' + agentinfo.id + '&zip=1' + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : '') + '">ZIP</a>'; }
6256 response += '</td>';
6257 response += '<td>' + agentinfo.size + '</td><td>' + agentinfo.hashhex + '</td>';
6258 response += '<td><a download href="' + originalUrl + '?meshcmd=' + agentinfo.id + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : '') + '">' + agentinfo.rname.replace('agent', 'cmd') + '</a></td></tr>';
6259 }
6260 response += '</table>';
6261 response += '<a href="' + originalUrl + '?cores=1' + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : '') + '">MeshCores</a> ';
6262 if (coreDumpsAllowed) { response += '<a href="' + originalUrl + '?dumps=1' + (req.query.key ? ('&key=' + encodeURIComponent(req.query.key)) : '') + '">MeshAgent Crash Dumps</a>'; }
6263 response += '</body></html>';
6264 res.send(response);
6265 return;
6266 }
6267 };
6268
6269 // generate the server url
6270 obj.generateBaseURL = function (domain, req) {
6271 var serverName = obj.getWebServerName(domain, req);
6272 var httpsPort = ((args.aliasport == null) ? args.port : args.aliasport); // Use HTTPS alias port is specified
6273 var xdomain = (domain.dns == null) ? domain.id : '';
6274 if (xdomain != '') xdomain += '/';
6275 return ('https://' + serverName + ':' + httpsPort + '/' + xdomain);
6276 }
6277
6278 // Get the web server hostname. This may change if using a domain with a DNS name.
6279 obj.getWebServerName = function (domain, req) {
6280 if (domain.dns != null) return domain.dns;
6281 if ((obj.certificates.CommonName == 'un-configured') && (req != null) && (req.headers != null) && (typeof req.headers.host == 'string')) { return req.headers.host.split(':')[0]; }
6282 return obj.certificates.CommonName;
6283 }
6284
6285 // Return true if this is an allowed HTTP request origin hostname.
6286 obj.CheckWebServerOriginName = function (domain, req) {
6287 if (domain.allowedorigin === true) return true; // Ignore origin
6288 if (typeof req.headers.origin != 'string') return true; // No origin in the header, this is a desktop app
6289 const originUrl = require('url').parse(req.headers.origin, true);
6290 if (typeof originUrl.hostname != 'string') return false; // Origin hostname is not valid
6291 if (Array.isArray(domain.allowedorigin)) return (domain.allowedorigin.indexOf(originUrl.hostname) >= 0); // Check if this is an allowed origin from an explicit list
6292 if (obj.isTrustedCert(domain) === false) return true; // This server does not have a trusted certificate.
6293 if (domain.dns != null) return (domain.dns == originUrl.hostname); // Match the domain DNS
6294 return (obj.certificates.CommonName == originUrl.hostname); // Match the default server name
6295 }
6296
6297 // Create a OSX mesh agent installer
6298 obj.handleMeshOsxAgentRequest = function (req, res) {
6299 const domain = getDomain(req, res);
6300 if (domain == null) { parent.debug('web', 'handleRootRequest: invalid domain.'); try { res.sendStatus(404); } catch (ex) { } return; }
6301 if (req.query.id == null) { res.sendStatus(404); return; }
6302
6303 // If required, check if this user has rights to do this
6304 if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true)) && (req.session.userid == null)) { res.sendStatus(401); return; }
6305
6306 // Send a specific mesh agent back
6307 var argentInfo = obj.parent.meshAgentBinaries[req.query.id];
6308 if (domain.meshAgentBinaries && domain.meshAgentBinaries[req.query.id]) { argentInfo = domain.meshAgentBinaries[req.query.id]; }
6309 if ((argentInfo == null) || (req.query.meshid == null)) { res.sendStatus(404); return; }
6310
6311 // Check if the meshid is a time limited, encrypted cookie
6312 var meshcookie = obj.parent.decodeCookie(req.query.meshid, obj.parent.invitationLinkEncryptionKey);
6313 if ((meshcookie != null) && (meshcookie.m != null)) { req.query.meshid = meshcookie.m; }
6314
6315 // We are going to embed the .msh file into the Windows executable (signed or not).
6316 // First, fetch the mesh object to build the .msh file
6317 var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.meshid];
6318 if (mesh == null) { res.sendStatus(401); return; }
6319
6320 // If required, check if this user has rights to do this
6321 if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true))) {
6322 if ((domain.id != mesh.domain) || ((obj.GetMeshRights(req.session.userid, mesh) & 1) == 0)) { res.sendStatus(401); return; }
6323 }
6324
6325 var meshidhex = Buffer.from(req.query.meshid.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
6326 var serveridhex = Buffer.from(obj.agentCertificateHashBase64.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
6327
6328 // Get the agent connection server name
6329 var serverName = obj.getWebServerName(domain, req);
6330 if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
6331
6332 // Build the agent connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
6333 var xdomain = (domain.dns == null) ? domain.id : '';
6334 if (xdomain != '') xdomain += '/';
6335 var meshsettings = '\r\nMeshName=' + mesh.name + '\r\nMeshType=' + mesh.mtype + '\r\nMeshID=0x' + meshidhex + '\r\nServerID=' + serveridhex + '\r\n';
6336 var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
6337 if (obj.args.agentport != null) { httpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
6338 if (obj.args.agentaliasport != null) { httpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
6339 if (obj.args.lanonly != true) { meshsettings += 'MeshServer=wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'agent.ashx\r\n'; } else {
6340 meshsettings += 'MeshServer=local\r\n';
6341 if ((obj.args.localdiscovery != null) && (typeof obj.args.localdiscovery.key == 'string') && (obj.args.localdiscovery.key.length > 0)) { meshsettings += 'DiscoveryKey=' + obj.args.localdiscovery.key + '\r\n'; }
6342 }
6343 if ((req.query.tag != null) && (typeof req.query.tag == 'string') && (obj.common.isAlphaNumeric(req.query.tag) == true)) { meshsettings += 'Tag=' + encodeURIComponent(req.query.tag) + '\r\n'; }
6344 if ((req.query.installflags != null) && (req.query.installflags != 0) && (parseInt(req.query.installflags) == req.query.installflags)) { meshsettings += 'InstallFlags=' + parseInt(req.query.installflags) + '\r\n'; }
6345 if ((domain.agentnoproxy === true) || (obj.args.lanonly == true)) { meshsettings += 'ignoreProxyFile=1\r\n'; }
6346 if (obj.args.agentconfig) { for (var i in obj.args.agentconfig) { meshsettings += obj.args.agentconfig[i] + '\r\n'; } }
6347 if (domain.agentconfig) { for (var i in domain.agentconfig) { meshsettings += domain.agentconfig[i] + '\r\n'; } }
6348 if (domain.agentcustomization != null) { // Add agent customization
6349 if (domain.agentcustomization.displayname != null) { meshsettings += 'displayName=' + domain.agentcustomization.displayname + '\r\n'; }
6350 if (domain.agentcustomization.description != null) { meshsettings += 'description=' + domain.agentcustomization.description + '\r\n'; }
6351 if (domain.agentcustomization.companyname != null) { meshsettings += 'companyName=' + domain.agentcustomization.companyname + '\r\n'; }
6352 if (domain.agentcustomization.servicename != null) { meshsettings += 'meshServiceName=' + domain.agentcustomization.servicename + '\r\n'; }
6353 if (domain.agentcustomization.filename != null) { meshsettings += 'fileName=' + domain.agentcustomization.filename + '\r\n'; }
6354 if (domain.agentcustomization.image != null) { meshsettings += 'image=' + domain.agentcustomization.image + '\r\n'; }
6355 if (domain.agentcustomization.foregroundcolor != null) { meshsettings += checkAgentColorString('foreground=', domain.agentcustomization.foregroundcolor); }
6356 if (domain.agentcustomization.backgroundcolor != null) { meshsettings += checkAgentColorString('background=', domain.agentcustomization.backgroundcolor); }
6357 }
6358 if (domain.agentTranslations != null) { meshsettings += 'translation=' + domain.agentTranslations + '\r\n'; }
6359
6360 // Setup the response output
6361 var archive = require('archiver')('zip', { level: 5 }); // Sets the compression method.
6362 archive.on('error', function (err) { throw err; });
6363
6364 // Customize the mesh agent file name
6365 var meshfilename = 'MeshAgent-' + mesh.name + '.zip';
6366 var meshexecutablename = 'meshagent';
6367 var meshmpkgname = 'MeshAgent.mpkg';
6368 if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) {
6369 meshfilename = meshfilename.split('MeshAgent').join(domain.agentcustomization.filename);
6370 meshexecutablename = meshexecutablename.split('meshagent').join(domain.agentcustomization.filename);
6371 meshmpkgname = meshmpkgname.split('MeshAgent').join(domain.agentcustomization.filename);
6372 }
6373
6374 // Customise the mesh agent display name
6375 var meshdisplayname = 'Mesh Agent';
6376 if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.displayname == 'string')) {
6377 meshdisplayname = meshdisplayname.split('Mesh Agent').join(domain.agentcustomization.displayname);
6378 }
6379
6380 // Customise the mesh agent service name
6381 var meshservicename = 'meshagent';
6382 if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.servicename == 'string')) {
6383 meshservicename = meshservicename.split('meshagent').join(domain.agentcustomization.servicename);
6384 }
6385
6386 // Customise the mesh agent company name
6387 var meshcompanyname = 'meshagent';
6388 if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.companyname == 'string')) {
6389 meshcompanyname = meshcompanyname.split('meshagent').join(domain.agentcustomization.companyname);
6390 }
6391
6392 // Set the agent download including the mesh name.
6393 setContentDispositionHeader(res, 'application/octet-stream', meshfilename, null, 'MeshAgent.zip');
6394 archive.pipe(res);
6395
6396 // Opens the "MeshAgentOSXPackager.zip"
6397 var yauzl = require('yauzl');
6398 yauzl.open(obj.path.join(__dirname, 'agents', 'MeshAgentOSXPackager.zip'), { lazyEntries: true }, function (err, zipfile) {
6399 if (err) { res.sendStatus(500); return; }
6400 zipfile.readEntry();
6401 zipfile.on('entry', function (entry) {
6402 if (/\/$/.test(entry.fileName)) {
6403 // Skip all folder entries
6404 zipfile.readEntry();
6405 } else {
6406 if (entry.fileName == 'MeshAgent.mpkg/Contents/distribution.dist') {
6407 // This is a special file entry, we need to fix it.
6408 zipfile.openReadStream(entry, function (err, readStream) {
6409 readStream.on('data', function (data) { if (readStream.xxdata) { readStream.xxdata += data; } else { readStream.xxdata = data; } });
6410 readStream.on('end', function () {
6411 var meshname = mesh.name.split(']').join('').split('[').join(''); // We can't have ']]' in the string since it will terminate the CDATA.
6412 var welcomemsg = 'Welcome to the MeshCentral agent for MacOS\n\nThis installer will install the mesh agent for "' + meshname + '" and allow the administrator to remotely monitor and control this computer over the internet. For more information, go to https://meshcentral.com.\n\nThis software is provided under Apache 2.0 license.\n';
6413 var installsize = Math.floor((argentInfo.size + meshsettings.length) / 1024);
6414 archive.append(readStream.xxdata.toString().split('###DISPLAYNAME###').join(meshdisplayname).split('###WELCOMEMSG###').join(welcomemsg).split('###INSTALLSIZE###').join(installsize), { name: entry.fileName.replace('MeshAgent.mpkg',meshmpkgname) });
6415 zipfile.readEntry();
6416 });
6417 });
6418 } else if (entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/meshagent_osx64_LaunchAgent.plist' ||
6419 entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/meshagent_osx64_LaunchDaemon.plist' ||
6420 entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/Info.plist' ||
6421 entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/Resources/postflight' ||
6422 entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/Resources/Postflight.sh' ||
6423 entry.fileName == 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/Uninstall.command' ||
6424 entry.fileName == 'MeshAgent.mpkg/Uninstall.command') {
6425 // This is a special file entry, we need to fix it.
6426 zipfile.openReadStream(entry, function (err, readStream) {
6427 readStream.on('data', function (data) { if (readStream.xxdata) { readStream.xxdata += data; } else { readStream.xxdata = data; } });
6428 readStream.on('end', function () {
6429 var options = { name: entry.fileName.replace('MeshAgent.mpkg',meshmpkgname) };
6430 if (entry.fileName.endsWith('postflight') || entry.fileName.endsWith('Uninstall.command')) { options.mode = 493; }
6431 archive.append(readStream.xxdata.toString().split('###SERVICENAME###').join(meshservicename).split('###COMPANYNAME###').join(meshcompanyname).split('###EXECUTABLENAME###').join(meshexecutablename), options);
6432 zipfile.readEntry();
6433 });
6434 });
6435 } else {
6436 // Normal file entry
6437 zipfile.openReadStream(entry, function (err, readStream) {
6438 if (err) { throw err; }
6439 var options = { name: entry.fileName.replace('MeshAgent.mpkg',meshmpkgname) };
6440 if (entry.fileName.endsWith('postflight') || entry.fileName.endsWith('Uninstall.command')) { options.mode = 493; }
6441 archive.append(readStream, options);
6442 readStream.on('end', function () { zipfile.readEntry(); });
6443 });
6444 }
6445 }
6446 });
6447 zipfile.on('end', function () {
6448 archive.file(argentInfo.path, { name: 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/meshagent_osx64.bin'.replace('MeshAgent.mpkg',meshmpkgname) });
6449 archive.append(meshsettings, { name: 'MeshAgent.mpkg/Contents/Packages/internal.pkg/Contents/meshagent_osx64.msh'.replace('MeshAgent.mpkg',meshmpkgname) });
6450 archive.finalize();
6451 });
6452 });
6453 }
6454
6455 // Return a .msh file from a given request, id is the device group identifier or encrypted cookie with the identifier.
6456 function getMshFromRequest(req, res, domain) {
6457 // If required, check if this user has rights to do this
6458 if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true)) && (req.session.userid == null)) { return null; }
6459
6460 // Check if the meshid is a time limited, encrypted cookie
6461 var meshcookie = obj.parent.decodeCookie(req.query.id, obj.parent.invitationLinkEncryptionKey);
6462 if ((meshcookie != null) && (meshcookie.m != null)) { req.query.id = meshcookie.m; }
6463
6464 // Fetch the mesh object
6465 var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.id];
6466 if (mesh == null) { return null; }
6467
6468 // If needed, check if this user has rights to do this
6469 if ((obj.parent.config.settings != null) && ((obj.parent.config.settings.lockagentdownload == true) || (domain.lockagentdownload == true))) {
6470 if ((domain.id != mesh.domain) || ((obj.GetMeshRights(req.session.userid, mesh) & 1) == 0)) { return null; }
6471 }
6472
6473 var meshidhex = Buffer.from(req.query.id.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
6474 var serveridhex = Buffer.from(obj.agentCertificateHashBase64.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64').toString('hex').toUpperCase();
6475
6476 // Get the agent connection server name
6477 var serverName = obj.getWebServerName(domain, req);
6478 if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }
6479
6480 // Build the agent connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
6481 var xdomain = (domain.dns == null) ? domain.id : '';
6482 if (xdomain != '') xdomain += '/';
6483 var meshsettings = '\r\nMeshName=' + mesh.name + '\r\nMeshType=' + mesh.mtype + '\r\nMeshID=0x' + meshidhex + '\r\nServerID=' + serveridhex + '\r\n';
6484 var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
6485 if (obj.args.agentport != null) { httpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
6486 if (obj.args.agentaliasport != null) { httpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
6487 if (obj.args.lanonly != true) { meshsettings += 'MeshServer=wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'agent.ashx\r\n'; } else {
6488 meshsettings += 'MeshServer=local\r\n';
6489 if ((obj.args.localdiscovery != null) && (typeof obj.args.localdiscovery.key == 'string') && (obj.args.localdiscovery.key.length > 0)) { meshsettings += 'DiscoveryKey=' + obj.args.localdiscovery.key + '\r\n'; }
6490 }
6491 if ((req.query.tag != null) && (typeof req.query.tag == 'string') && (obj.common.isAlphaNumeric(req.query.tag) == true)) { meshsettings += 'Tag=' + encodeURIComponent(req.query.tag) + '\r\n'; }
6492 if ((req.query.installflags != null) && (req.query.installflags != 0) && (parseInt(req.query.installflags) == req.query.installflags)) { meshsettings += 'InstallFlags=' + parseInt(req.query.installflags) + '\r\n'; }
6493 if ((domain.agentnoproxy === true) || (obj.args.lanonly == true)) { meshsettings += 'ignoreProxyFile=1\r\n'; }
6494 if (obj.args.agentconfig) { for (var i in obj.args.agentconfig) { meshsettings += obj.args.agentconfig[i] + '\r\n'; } }
6495 if (domain.agentconfig) { for (var i in domain.agentconfig) { meshsettings += domain.agentconfig[i] + '\r\n'; } }
6496 if (domain.agentcustomization != null) { // Add agent customization
6497 if (domain.agentcustomization.displayname != null) { meshsettings += 'displayName=' + domain.agentcustomization.displayname + '\r\n'; }
6498 if (domain.agentcustomization.description != null) { meshsettings += 'description=' + domain.agentcustomization.description + '\r\n'; }
6499 if (domain.agentcustomization.companyname != null) { meshsettings += 'companyName=' + domain.agentcustomization.companyname + '\r\n'; }
6500 if (domain.agentcustomization.servicename != null) { meshsettings += 'meshServiceName=' + domain.agentcustomization.servicename + '\r\n'; }
6501 if (domain.agentcustomization.filename != null) { meshsettings += 'fileName=' + domain.agentcustomization.filename + '\r\n'; }
6502 if (domain.agentcustomization.image != null) { meshsettings += 'image=' + domain.agentcustomization.image + '\r\n'; }
6503 if (domain.agentcustomization.foregroundcolor != null) { meshsettings += checkAgentColorString('foreground=', domain.agentcustomization.foregroundcolor); }
6504 if (domain.agentcustomization.backgroundcolor != null) { meshsettings += checkAgentColorString('background=', domain.agentcustomization.backgroundcolor); }
6505 }
6506 if (domain.agentTranslations != null) { meshsettings += 'translation=' + domain.agentTranslations + '\r\n'; }
6507 return meshsettings;
6508 }
6509
6510 // Handle a request to download a mesh settings
6511 obj.handleMeshSettingsRequest = function (req, res) {
6512 const domain = getDomain(req);
6513 if (domain == null) { return; }
6514 //if ((domain.id !== '') || (!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
6515
6516 var meshsettings = getMshFromRequest(req, res, domain);
6517 if (meshsettings == null) { res.sendStatus(401); return; }
6518
6519 // Get the agent filename
6520 var meshagentFilename = 'meshagent';
6521 if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) { meshagentFilename = domain.agentcustomization.filename; }
6522
6523 setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename + '.msh', null, 'meshagent.msh');
6524 res.send(meshsettings);
6525 };
6526
6527 // Handle a request for power events
6528 obj.handleDevicePowerEvents = function (req, res) {
6529 const domain = checkUserIpAddress(req, res);
6530 if (domain == null) { return; }
6531 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
6532 if ((domain.id !== '') || (!req.session) || (req.session == null) || (!req.session.userid) || (req.query.id == null) || (typeof req.query.id != 'string')) { res.sendStatus(401); return; }
6533 var x = req.query.id.split('/');
6534 var user = obj.users[req.session.userid];
6535 if ((x.length != 3) || (x[0] != 'node') || (x[1] != domain.id) || (user == null) || (user.links == null)) { res.sendStatus(401); return; }
6536
6537 obj.db.Get(req.query.id, function (err, docs) {
6538 if (docs.length != 1) {
6539 res.sendStatus(401);
6540 } else {
6541 var node = docs[0];
6542
6543 // Check if we have right to this node
6544 if (obj.GetNodeRights(user, node.meshid, node._id) == 0) { res.sendStatus(401); return; }
6545
6546 // See how we will convert UTC time to local time
6547 var localTimeOffset = 0;
6548 var timeConversionSystem = 0;
6549 if ((req.query.l != null) && (req.query.tz != null)) {
6550 timeConversionSystem = 1;
6551 } else if (req.query.tf != null) {
6552 // Get local time offset (bad way)
6553 timeConversionSystem = 2;
6554 localTimeOffset = parseInt(req.query.tf);
6555 if (isNaN(localTimeOffset)) { localTimeOffset = 0; }
6556 }
6557
6558 // Get the list of power events and send them
6559 setContentDispositionHeader(res, 'application/octet-stream', 'powerevents.csv', null, 'powerevents.csv');
6560 obj.db.getPowerTimeline(node._id, function (err, docs) {
6561 var xevents = ['UTC Time, Local Time, State, Previous State'], prevState = 0;
6562 for (var i in docs) {
6563 if (docs[i].power != prevState) {
6564 var timedoc = docs[i].time;
6565 if (typeof timedoc == 'string') {
6566 timedoc = new Date(timedoc);
6567 }
6568 prevState = docs[i].power;
6569 var localTime = '';
6570 if (timeConversionSystem == 1) { // Good way
6571 localTime = new Date(timedoc.getTime()).toLocaleString(req.query.l, { timeZone: req.query.tz })
6572 } else if (timeConversionSystem == 2) { // Bad way
6573 localTime = new Date(timedoc.getTime() + (localTimeOffset * 60000)).toISOString();
6574 localTime = localTime.substring(0, localTime.length - 1);
6575 }
6576 if (docs[i].oldPower != null) {
6577 xevents.push('\"' + timedoc.toISOString() + '\",\"' + localTime + '\",' + docs[i].power + ',' + docs[i].oldPower);
6578 } else {
6579 xevents.push('\"' + timedoc.toISOString() + '\",\"' + localTime + '\",' + docs[i].power);
6580 }
6581 }
6582 }
6583 res.send(xevents.join('\r\n'));
6584 });
6585 }
6586 });
6587 }
6588
6589 if (parent.pluginHandler != null) {
6590 // Handle a plugin admin request
6591 obj.handlePluginAdminReq = function (req, res) {
6592 const domain = checkUserIpAddress(req, res);
6593 if (domain == null) { return; }
6594 if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
6595 var user = obj.users[req.session.userid];
6596 if (user == null) { res.sendStatus(401); return; }
6597
6598 parent.pluginHandler.handleAdminReq(req, res, user, obj);
6599 }
6600
6601 obj.handlePluginAdminPostReq = function (req, res) {
6602 const domain = checkUserIpAddress(req, res);
6603 if (domain == null) { return; }
6604 if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
6605 var user = obj.users[req.session.userid];
6606 if (user == null) { res.sendStatus(401); return; }
6607
6608 parent.pluginHandler.handleAdminPostReq(req, res, user, obj);
6609 }
6610
6611 obj.handlePluginJS = function (req, res) {
6612 const domain = checkUserIpAddress(req, res);
6613 if (domain == null) { return; }
6614 if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
6615 var user = obj.users[req.session.userid];
6616 if (user == null) { res.sendStatus(401); return; }
6617
6618 parent.pluginHandler.refreshJS(req, res);
6619 }
6620 }
6621
6622 // Starts the HTTPS server, this should be called after the user/mesh tables are loaded
6623 function serverStart() {
6624 // Start the server, only after users and meshes are loaded from the database.
6625 if (obj.args.tlsoffload) {
6626 // Setup the HTTP server without TLS
6627 obj.expressWs = require('express-ws')(obj.app, null, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
6628 } else {
6629 var ciphers = [
6630 'TLS_AES_256_GCM_SHA384',
6631 'TLS_AES_128_GCM_SHA256',
6632 'TLS_AES_128_CCM_8_SHA256',
6633 'TLS_AES_128_CCM_SHA256',
6634 'TLS_CHACHA20_POLY1305_SHA256',
6635 'ECDHE-RSA-AES256-GCM-SHA384',
6636 'ECDHE-ECDSA-AES256-GCM-SHA384',
6637 'ECDHE-RSA-AES128-GCM-SHA256',
6638 'ECDHE-ECDSA-AES128-GCM-SHA256',
6639 'DHE-RSA-AES128-GCM-SHA256',
6640 'ECDHE-RSA-CHACHA20-POLY1305', // TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xcca8)
6641 'ECDHE-ARIA128-GCM-SHA256',
6642 'ECDHE-ARIA256-GCM-SHA384',
6643 'ECDHE-RSA-AES128-SHA256', // SSLlabs considers this cipher suite weak, but it's needed for older browers.
6644 'ECDHE-RSA-AES256-SHA384', // SSLlabs considers this cipher suite weak, but it's needed for older browers.
6645 '!aNULL',
6646 '!eNULL',
6647 '!EXPORT',
6648 '!DES',
6649 '!RC4',
6650 '!MD5',
6651 '!PSK',
6652 '!SRP',
6653 '!CAMELLIA'
6654 ].join(':');
6655
6656 if (obj.useNodeDefaultTLSCiphers) {
6657 ciphers = require("tls").DEFAULT_CIPHERS;
6658 }
6659
6660 if (obj.tlsCiphers) {
6661 ciphers = obj.tlsCiphers;
6662 if (Array.isArray(obj.tlsCiphers)) {
6663 ciphers = obj.tlsCiphers.join(":");
6664 }
6665 }
6666
6667 // Setup the HTTP server with TLS, use only TLS 1.2 and higher with perfect forward secrecy (PFS).
6668 //const tlsOptions = { cert: obj.certificates.web.cert, key: obj.certificates.web.key, ca: obj.certificates.web.ca, rejectUnauthorized: true, ciphers: "HIGH:!aNULL:!eNULL:!EXPORT:!RSA:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA", secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE | constants.SSL_OP_NO_TLSv1 | constants.SSL_OP_NO_TLSv1_1 }; // This does not work with TLS 1.3
6669 const tlsOptions = { cert: obj.certificates.web.cert, key: obj.certificates.web.key, ca: obj.certificates.web.ca, rejectUnauthorized: true, ciphers: ciphers, secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE | constants.SSL_OP_NO_TLSv1 | constants.SSL_OP_NO_TLSv1_1 };
6670 if (obj.tlsSniCredentials != null) { tlsOptions.SNICallback = TlsSniCallback; } // We have multiple web server certificate used depending on the domain name
6671 obj.tlsServer = require('https').createServer(tlsOptions, obj.app);
6672 obj.tlsServer.on('secureConnection', function () { /*console.log('tlsServer secureConnection');*/ });
6673 obj.tlsServer.on('error', function (err) { console.log('tlsServer error', err); });
6674 //obj.tlsServer.on('tlsClientError', function (err) { console.log('tlsClientError', err); });
6675 obj.tlsServer.on('newSession', function (id, data, cb) { if (tlsSessionStoreCount > 1000) { tlsSessionStoreCount = 0; tlsSessionStore = {}; } tlsSessionStore[id.toString('hex')] = data; tlsSessionStoreCount++; cb(); });
6676 obj.tlsServer.on('resumeSession', function (id, cb) { cb(null, tlsSessionStore[id.toString('hex')] || null); });
6677 obj.expressWs = require('express-ws')(obj.app, obj.tlsServer, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
6678 }
6679
6680 // Start a second agent-only server if needed
6681 if (obj.args.agentport) {
6682 var agentPortTls = true;
6683 if (obj.args.tlsoffload != null && obj.args.tlsoffload != false) { agentPortTls = false; }
6684 if (typeof obj.args.agentporttls == 'boolean') { agentPortTls = obj.args.agentporttls; }
6685 if (obj.certificates.webdefault == null) { agentPortTls = false; }
6686
6687 if (agentPortTls == false) {
6688 // Setup the HTTP server without TLS
6689 obj.expressWsAlt = require('express-ws')(obj.agentapp, null, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
6690 } else {
6691 // Setup the agent HTTP server with TLS, use only TLS 1.2 and higher with perfect forward secrecy (PFS).
6692 // If TLS is used on the agent port, we always use the default TLS certificate.
6693 const tlsOptions = { cert: obj.certificates.webdefault.cert, key: obj.certificates.webdefault.key, ca: obj.certificates.webdefault.ca, rejectUnauthorized: true, ciphers: "HIGH:TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_AES_128_CCM_8_SHA256:TLS_AES_128_CCM_SHA256:TLS_CHACHA20_POLY1305_SHA256", secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE | constants.SSL_OP_NO_TLSv1 | constants.SSL_OP_NO_TLSv1_1 };
6694 obj.tlsAltServer = require('https').createServer(tlsOptions, obj.agentapp);
6695 obj.tlsAltServer.on('secureConnection', function () { /*console.log('tlsAltServer secureConnection');*/ });
6696 obj.tlsAltServer.on('error', function (err) { console.log('tlsAltServer error', err); });
6697 //obj.tlsAltServer.on('tlsClientError', function (err) { console.log('tlsClientError', err); });
6698 obj.tlsAltServer.on('newSession', function (id, data, cb) { if (tlsSessionStoreCount > 1000) { tlsSessionStoreCount = 0; tlsSessionStore = {}; } tlsSessionStore[id.toString('hex')] = data; tlsSessionStoreCount++; cb(); });
6699 obj.tlsAltServer.on('resumeSession', function (id, cb) { cb(null, tlsSessionStore[id.toString('hex')] || null); });
6700 obj.expressWsAlt = require('express-ws')(obj.agentapp, obj.tlsAltServer, { wsOptions: { perMessageDeflate: (args.wscompression === true) } });
6701 }
6702 }
6703
6704 // Setup middleware
6705 obj.app.engine('handlebars', obj.exphbs.engine({ defaultLayout: false }));
6706 obj.app.set('view engine', 'handlebars');
6707 if (obj.args.trustedproxy) {
6708 // Reverse proxy should add the "X-Forwarded-*" headers
6709 try {
6710 obj.app.set('trust proxy', obj.args.trustedproxy);
6711 } catch (ex) {
6712 // If there is an error, try to resolve the string
6713 if ((obj.args.trustedproxy.length == 1) && (typeof obj.args.trustedproxy[0] == 'string')) {
6714 require('dns').lookup(obj.args.trustedproxy[0], function (err, address, family) { if (err == null) { obj.app.set('trust proxy', address); obj.args.trustedproxy = [address]; } });
6715 }
6716 }
6717 }
6718 else if (typeof obj.args.tlsoffload == 'object') {
6719 // Reverse proxy should add the "X-Forwarded-*" headers
6720 try {
6721 obj.app.set('trust proxy', obj.args.tlsoffload);
6722 } catch (ex) {
6723 // If there is an error, try to resolve the string
6724 if ((Array.isArray(obj.args.tlsoffload)) && (obj.args.tlsoffload.length == 1) && (typeof obj.args.tlsoffload[0] == 'string')) {
6725 require('dns').lookup(obj.args.tlsoffload[0], function (err, address, family) { if (err == null) { obj.app.set('trust proxy', address); obj.args.tlsoffload = [address]; } });
6726 }
6727 }
6728 }
6729
6730 // Setup a keygrip instance with higher default security, default hash is SHA1, we want to bump that up with SHA384
6731 // If multiple instances of this server are behind a load-balancer, this secret must be the same for all instances
6732 // If args.sessionkey is a string, use it as a single key, but args.sessionkey can also be used as an array of keys.
6733 const keygrip = require('keygrip')((typeof obj.args.sessionkey == 'string') ? [obj.args.sessionkey] : obj.args.sessionkey, 'sha384', 'base64');
6734
6735 // Setup the cookie session
6736 const sessionOptions = {
6737 name: 'xid', // Recommended security practice to not use the default cookie name
6738 httpOnly: true,
6739 keys: keygrip,
6740 secure: (obj.args.tlsoffload == null), // Use this cookie only over TLS (Check this: https://expressjs.com/en/guide/behind-proxies.html)
6741 sameSite: (obj.args.sessionsamesite ? obj.args.sessionsamesite : 'lax')
6742 }
6743 if (obj.args.sessiontime != null) { sessionOptions.maxAge = (obj.args.sessiontime * 60000); } // sessiontime is minutes
6744 obj.app.use(require('cookie-session')(sessionOptions));
6745 obj.app.use(function (request, response, next) { // Patch for passport 0.6.0 - https://github.com/jaredhanson/passport/issues/904
6746 if (request.session && !request.session.regenerate) {
6747 request.session.regenerate = function (cb) {
6748 cb()
6749 }
6750 }
6751 if (request.session && !request.session.save) {
6752 request.session.save = function (cb) {
6753 cb()
6754 }
6755 }
6756 // Special Client Hint Headers for Browser Detection on every request - https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers#client_hints
6757 // note: only works in a secure context (localhost or https://)
6758 if ((obj.webRelayRouter != null) && (obj.args.relaydns.indexOf(request.hostname) == -1)) {
6759 const secCH = [
6760 'Sec-CH-UA-Arch', 'Sec-CH-UA-Bitness', 'Sec-CH-UA-Form-Factors', 'Sec-CH-UA-Full-Version',
6761 'Sec-CH-UA-Full-Version-List', 'Sec-CH-UA-Mobile', 'Sec-CH-UA-Model', 'Sec-CH-UA-Platform',
6762 'Sec-CH-UA-Platform-Version', 'Sec-CH-UA-WoW64'
6763 ];
6764 response.setHeader('Accept-CH', secCH.join(', '));
6765 response.setHeader('Critical-CH', secCH.join(', '));
6766 }
6767 next();
6768 });
6769
6770 // JWT Authentication Middleware (RMM+PSA Integration)
6771 obj.app.use(function (req, res, next) {
6772 // Skip JWT auth for health/readiness probes, public routes, and static assets
6773 const publicPaths = [
6774 '/', // Allow root page
6775 '/login',
6776 '/createaccount', // Allow for first admin account creation only
6777 '/resetpassword',
6778 '/resetaccount',
6779 '/checkemail',
6780 '/health',
6781 '/healthz',
6782 '/health.ashx',
6783 '/ping',
6784 '/api/ping',
6785 '/status',
6786 '/readiness',
6787 '/liveness'
6788 ];
6789
6790 // Check if path matches any public path, starts with /.well-known/, or is a static asset
6791 if (publicPaths.includes(req.path) ||
6792 req.path.startsWith('/.well-known/') ||
6793 req.path.startsWith('/styles/') ||
6794 req.path.startsWith('/scripts/') ||
6795 req.path.startsWith('/images/') ||
6796 req.path.startsWith('/public/') ||
6797 req.path.match(/\.(css|js|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$/i) ||
6798 req.ws) {
6799 return next();
6800 }
6801
6802 // Skip if user is already authenticated via session
6803 if (req.session && req.session.userid) {
6804 return next();
6805 }
6806
6807 // Check for JWT token
6808 if (obj.parent.jwtAuth) {
6809 const token = obj.parent.jwtAuth.extractToken(req);
6810
6811 if (token) {
6812 obj.parent.jwtAuth.validateToken(token, function (jwtUser) {
6813 if (jwtUser) {
6814 // JWT authenticated successfully
6815 req.user = jwtUser;
6816 req.session = req.session || {};
6817 req.session.userid = jwtUser._id;
6818 req.session.domainid = jwtUser.domain;
6819 req.session.currentNode = ''; // Set to empty, will be filled in dynamically
6820 obj.parent.debug('web', `JWT HTTP Auth: ${jwtUser._id}`);
6821 }
6822 next();
6823 });
6824 return; // Wait for callback
6825 }
6826 }
6827
6828 next();
6829 });
6830
6831 // Handle all incoming web sockets, see if some need to be handled as web relays
6832 obj.app.ws('/*', function (ws, req, next) {
6833 // Global error catcher
6834 ws.on('error', function (err) { parent.debug('web', 'GENERAL WSERR: ' + err); console.log(err); });
6835 if ((obj.webRelayRouter != null) && (obj.args.relaydns.indexOf(req.hostname) >= 0)) { handleWebRelayWebSocket(ws, req); return; }
6836 return next();
6837 });
6838
6839 // Add HTTP security headers to all responses
6840 obj.app.use(async function (req, res, next) {
6841 // Check if a session is destroyed
6842 if (typeof req.session.userid == 'string') {
6843 if (typeof req.session.x == 'string') {
6844 if (obj.destroyedSessions[req.session.userid + '/' + req.session.x] != null) {
6845 delete req.session.userid;
6846 delete req.session.ip;
6847 delete req.session.t;
6848 delete req.session.x;
6849 }
6850 } else {
6851 // Legacy session without a random, add one.
6852 setSessionRandom(req);
6853 }
6854 }
6855
6856 // Remove legacy values from the session to keep the session as small as possible
6857 delete req.session.u2f;
6858 delete req.session.domainid;
6859 delete req.session.nowInMinutes;
6860 delete req.session.tokenuserid;
6861 delete req.session.tokenusername;
6862 delete req.session.tokenpassword;
6863 delete req.session.tokenemail;
6864 delete req.session.tokensms;
6865 delete req.session.tokenpush;
6866 delete req.session.tusername;
6867 delete req.session.tpassword;
6868
6869 // Useful for debugging reverse proxy issues
6870 parent.debug('httpheaders', req.method, req.url, req.headers);
6871
6872 // If this request came over HTTP, redirect to HTTPS
6873 if (req.headers['x-forwarded-proto'] == 'http') {
6874 var host = req.headers.host;
6875 if (typeof host == 'string') { host = host.split(':')[0]; }
6876 if ((host == null) && (obj.certificates != null)) { host = obj.certificates.CommonName; if (obj.certificates.CommonName.indexOf('.') == -1) { host = req.headers.host; } }
6877 var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
6878 res.redirect('https://' + host + ':' + httpsPort + req.url);
6879 return;
6880 }
6881
6882 // Perform traffic accounting
6883 if (req.headers.upgrade == 'websocket') {
6884 // We don't count traffic on WebSockets since it's counted by the handling modules.
6885 obj.trafficStats.httpWebSocketCount++;
6886 } else {
6887 // Normal HTTP traffic is counted
6888 obj.trafficStats.httpRequestCount++;
6889 if (typeof req.socket.xbytesRead != 'number') {
6890 req.socket.xbytesRead = 0;
6891 req.socket.xbytesWritten = 0;
6892 req.socket.on('close', function () {
6893 // Perform final accounting
6894 obj.trafficStats.httpIn += (this.bytesRead - this.xbytesRead);
6895 obj.trafficStats.httpOut += (this.bytesWritten - this.xbytesWritten);
6896 this.xbytesRead = this.bytesRead;
6897 this.xbytesWritten = this.bytesWritten;
6898 });
6899 } else {
6900 // Update counters
6901 obj.trafficStats.httpIn += (req.socket.bytesRead - req.socket.xbytesRead);
6902 obj.trafficStats.httpOut += (req.socket.bytesWritten - req.socket.xbytesWritten);
6903 req.socket.xbytesRead = req.socket.bytesRead;
6904 req.socket.xbytesWritten = req.socket.bytesWritten;
6905 }
6906 }
6907
6908 // Set the real IP address of the request
6909 // If a trusted reverse-proxy is sending us the remote IP address, use it.
6910 var ipex = '0.0.0.0', xforwardedhost = req.headers.host;
6911 if (typeof req.connection.remoteAddress == 'string') { ipex = (req.connection.remoteAddress.startsWith('::ffff:')) ? req.connection.remoteAddress.substring(7) : req.connection.remoteAddress; }
6912 if (
6913 (obj.args.trustedproxy === true) || (obj.args.tlsoffload === true) ||
6914 ((typeof obj.args.trustedproxy == 'object') && (isIPMatch(ipex, obj.args.trustedproxy))) ||
6915 ((typeof obj.args.tlsoffload == 'object') && (isIPMatch(ipex, obj.args.tlsoffload)))
6916 ) {
6917 // Get client IP
6918 if (req.headers['cf-connecting-ip']) { // Use CloudFlare IP address if present
6919 req.clientIp = req.headers['cf-connecting-ip'].split(',')[0].trim();
6920 } else if (req.headers['x-forwarded-for']) {
6921 req.clientIp = req.headers['x-forwarded-for'].split(',')[0].trim();
6922 } else if (req.headers['x-real-ip']) {
6923 req.clientIp = req.headers['x-real-ip'].split(',')[0].trim();
6924 } else {
6925 req.clientIp = ipex;
6926 }
6927
6928 // If there is a port number, remove it. This will only work for IPv4, but nice for people that have a bad reverse proxy config.
6929 const clientIpSplit = req.clientIp.split(':');
6930 if (clientIpSplit.length == 2) { req.clientIp = clientIpSplit[0]; }
6931
6932 // Get server host
6933 if (req.headers['x-forwarded-host']) { xforwardedhost = req.headers['x-forwarded-host'].split(',')[0]; } // If multiple hosts are specified with a comma, take the first one.
6934 } else {
6935 req.clientIp = ipex;
6936 }
6937
6938 // If this is a web relay connection, handle it here.
6939 if ((obj.webRelayRouter != null) && (obj.args.relaydns.indexOf(req.hostname) >= 0)) {
6940 if (['GET', 'POST', 'PUT', 'HEAD', 'DELETE', 'OPTIONS'].indexOf(req.method) >= 0) { return obj.webRelayRouter(req, res); } else { res.sendStatus(404); return; }
6941 }
6942
6943 // Get the domain for this request
6944 const domain = req.xdomain = getDomain(req);
6945 parent.debug('webrequest', '(' + req.clientIp + ') ' + req.url);
6946
6947 // Skip the rest if this is an agent connection
6948 if ((req.url.indexOf('/meshrelay.ashx/.websocket') >= 0) || (req.url.indexOf('/agent.ashx/.websocket') >= 0) || (req.url.indexOf('/localrelay.ashx/.websocket') >= 0)) { next(); return; }
6949
6950 // Setup security headers
6951 const geourl = (domain.geolocation ? ' *.openstreetmap.org' : '');
6952 var selfurl = ' wss://' + req.headers.host;
6953 if ((xforwardedhost != null) && (xforwardedhost != req.headers.host)) { selfurl += ' wss://' + xforwardedhost; }
6954 const extraScriptSrc = (parent.config.settings.extrascriptsrc != null) ? (' ' + parent.config.settings.extrascriptsrc) : '';
6955
6956 // If the web relay port is enabled, allow the web page to redirect to it
6957 var extraFrameSrc = '';
6958 if ((parent.webrelayserver != null) && (parent.webrelayserver.port != 0)) {
6959 extraFrameSrc = ' https://' + req.headers.host + ':' + parent.webrelayserver.port;
6960 if ((xforwardedhost != null) && (xforwardedhost != req.headers.host)) { extraFrameSrc += ' https://' + xforwardedhost + ':' + parent.webrelayserver.port; }
6961 }
6962
6963
6964 // If using duo add apihostname to CSP
6965 var duoSrc = '';
6966 if ((typeof domain.duo2factor == 'object') && (typeof domain.duo2factor.apihostname == 'string')) {
6967 duoSrc = domain.duo2factor.apihostname;
6968 }
6969
6970 // Finish setup security headers
6971 const headers = {
6972 'Referrer-Policy': 'no-referrer',
6973 'X-XSS-Protection': '1; mode=block',
6974 'X-Content-Type-Options': 'nosniff',
6975 'Content-Security-Policy': "default-src 'none'; font-src 'self' fonts.gstatic.com data:; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' " + extraScriptSrc + "; connect-src 'self'" + geourl + selfurl + "; img-src 'self' blob: data:" + geourl + " data:; style-src 'self' 'unsafe-inline' fonts.googleapis.com; frame-src 'self' blob: mcrouter:" + extraFrameSrc + "; media-src 'self'; form-action 'self' " + duoSrc + "; manifest-src 'self'; frame-ancestors 'self' https://everydaytech.au https://*.everydaytech.au https://*.ondigitalocean.app"
6976 };
6977 if (req.headers['user-agent'] && (req.headers['user-agent'].indexOf('Chrome') >= 0)) { headers['Permissions-Policy'] = 'interest-cohort=()'; } // Remove Google's FLoC Network, only send this if Chrome browser
6978 if ((parent.config.settings.allowframing !== true) && (typeof parent.config.settings.allowframing !== 'string')) { headers['X-Frame-Options'] = 'sameorigin'; }
6979 if ((parent.config.settings.stricttransportsecurity === true) || ((parent.config.settings.stricttransportsecurity !== false) && (obj.isTrustedCert(domain)))) { if (typeof parent.config.settings.stricttransportsecurity == 'string') { headers['Strict-Transport-Security'] = parent.config.settings.stricttransportsecurity; } else { headers['Strict-Transport-Security'] = 'max-age=63072000'; } }
6980
6981 // If this domain has configured headers, add them. If a header is set to null, remove it.
6982 if ((domain != null) && (domain.httpheaders != null) && (typeof domain.httpheaders == 'object')) {
6983 for (var i in domain.httpheaders) { if (domain.httpheaders[i] === null) { delete headers[i]; } else { headers[i] = domain.httpheaders[i]; } }
6984 }
6985 res.set(headers);
6986
6987 // Check the session if bound to the external IP address
6988 if ((req.session.ip != null) && (req.clientIp != null) && !checkCookieIp(req.session.ip, req.clientIp)) { req.session = {}; }
6989
6990 // Extend the session time by forcing a change to the session every minute.
6991 if (req.session.userid != null) { req.session.t = Math.floor(Date.now() / 60e3); } else { delete req.session.t; }
6992
6993 // Check CrowdSec Bounser if configured
6994 if ((parent.crowdSecBounser != null) && (req.headers['upgrade'] != 'websocket') && (req.session.userid == null)) { if ((await parent.crowdSecBounser.process(domain, req, res, next)) == true) { return; } }
6995
6996 // Debugging code, this will stop the agent from crashing if two responses are made to the same request.
6997 const render = res.render;
6998 const send = res.send;
6999 res.render = function renderWrapper(...args) {
7000 Error.captureStackTrace(this);
7001 return render.apply(this, args);
7002 };
7003 res.send = function sendWrapper(...args) {
7004 try {
7005 send.apply(this, args);
7006 } catch (err) {
7007 console.error(`Error in res.send | ${err.code} | ${err.message} | ${res.stack}`);
7008 try {
7009 var errlogpath = null;
7010 if (typeof parent.args.mesherrorlogpath == 'string') { errlogpath = parent.path.join(parent.args.mesherrorlogpath, 'mesherrors.txt'); } else { errlogpath = parent.getConfigFilePath('mesherrors.txt'); }
7011 parent.fs.appendFileSync(errlogpath, new Date().toLocaleString() + ': ' + `Error in res.send | ${err.code} | ${err.message} | ${res.stack}` + '\r\n');
7012 } catch (ex) { parent.debug('error', 'Unable to write to mesherrors.txt.'); }
7013 }
7014 };
7015
7016 // Continue processing the request
7017 return next();
7018 });
7019
7020 if (obj.agentapp) {
7021 // Add HTTP security headers to all responses
7022 obj.agentapp.use(function (req, res, next) {
7023 // Set the real IP address of the request
7024 // If a trusted reverse-proxy is sending us the remote IP address, use it.
7025 var ipex = '0.0.0.0';
7026 if (typeof req.connection.remoteAddress == 'string') { ipex = (req.connection.remoteAddress.startsWith('::ffff:')) ? req.connection.remoteAddress.substring(7) : req.connection.remoteAddress; }
7027 if (
7028 (obj.args.trustedproxy === true) || (obj.args.tlsoffload === true) ||
7029 ((typeof obj.args.trustedproxy == 'object') && (isIPMatch(ipex, obj.args.trustedproxy))) ||
7030 ((typeof obj.args.tlsoffload == 'object') && (isIPMatch(ipex, obj.args.tlsoffload)))
7031 ) {
7032 if (req.headers['cf-connecting-ip']) { // Use CloudFlare IP address if present
7033 req.clientIp = req.headers['cf-connecting-ip'].split(',')[0].trim();
7034 } else if (req.headers['x-forwarded-for']) {
7035 req.clientIp = req.headers['x-forwarded-for'].split(',')[0].trim();
7036 } else if (req.headers['x-real-ip']) {
7037 req.clientIp = req.headers['x-real-ip'].split(',')[0].trim();
7038 } else {
7039 req.clientIp = ipex;
7040 }
7041 } else {
7042 req.clientIp = ipex;
7043 }
7044
7045 // Get the domain for this request
7046 const domain = req.xdomain = getDomain(req);
7047 parent.debug('webrequest', '(' + req.clientIp + ') AgentPort: ' + req.url);
7048 res.removeHeader('X-Powered-By');
7049 return next();
7050 });
7051 }
7052
7053 // Setup all sharing domains and check if auth strategies need setup
7054 var setupSSO = false
7055 for (var i in parent.config.domains) {
7056 if ((parent.config.domains[i].dns == null) && (parent.config.domains[i].share != null)) { obj.app.use(parent.config.domains[i].url, obj.express.static(parent.config.domains[i].share)); }
7057 if (typeof parent.config.domains[i].authstrategies == 'object') { setupSSO = true };
7058 }
7059
7060 if (setupSSO) {
7061 setupAllDomainAuthStrategies().then(() => finalizeWebserver());
7062 } else {
7063 finalizeWebserver()
7064 }
7065
7066 // Setup all domain auth strategy passport.js
7067 async function setupAllDomainAuthStrategies() {
7068 for (var i in parent.config.domains) {
7069 if (parent.config.domains[i].dns != null) {
7070 if (typeof parent.config.domains[''].authstrategies != 'object') { parent.config.domains[''].authstrategies = { 'authStrategyFlags': 0 }; }
7071 parent.config.domains[''].authstrategies.authStrategyFlags |= await setupDomainAuthStrategy(parent.config.domains[i]);
7072 } else {
7073 if (typeof parent.config.domains[i].authstrategies != 'object') { parent.config.domains[i].authstrategies = { 'authStrategyFlags': 0 }; }
7074 parent.config.domains[i].authstrategies.authStrategyFlags |= await setupDomainAuthStrategy(parent.config.domains[i]);
7075 }
7076 }
7077 }
7078 function setupHTTPHandlers() {
7079 // Setup all HTTP handlers
7080 if (parent.pluginHandler != null) {
7081 parent.pluginHandler.callHook('hook_setupHttpHandlers', obj, parent);
7082 }
7083 if (parent.multiServer != null) { obj.app.ws('/meshserver.ashx', function (ws, req) { parent.multiServer.CreatePeerInServer(parent.multiServer, ws, req, obj.args.tlsoffload == null); }); }
7084 for (var i in parent.config.domains) {
7085 if ((parent.config.domains[i].dns != null) || (parent.config.domains[i].share != null)) { continue; } // This is a subdomain with a DNS name, no added HTTP bindings needed.
7086 var domain = parent.config.domains[i];
7087 var url = domain.url;
7088 if (typeof domain.rootredirect == 'string') {
7089 // Root page redirects the user to a different URL
7090 obj.app.get(url, handleRootRedirect);
7091 } else {
7092 // Present the login page as the root page
7093 obj.app.get(url, handleRootRequest);
7094 obj.app.post(url, obj.bodyParser.urlencoded({ extended: false }), handleRootPostRequest);
7095 }
7096 obj.app.get(url + 'refresh.ashx', function (req, res) { res.sendStatus(200); });
7097 if ((domain.myserver !== false) && ((domain.myserver == null) || (domain.myserver.backup === true))) { obj.app.get(url + 'backup.zip', handleBackupRequest); }
7098 if ((domain.myserver !== false) && ((domain.myserver == null) || (domain.myserver.restore === true))) { obj.app.post(url + 'restoreserver.ashx', obj.bodyParser.urlencoded({ extended: false }), handleRestoreRequest); }
7099 obj.app.get(url + 'terms', handleTermsRequest);
7100 obj.app.get(url + 'xterm', handleXTermRequest);
7101 obj.app.get(url + 'login', handleRootRequest);
7102 obj.app.post(url + 'login', obj.bodyParser.urlencoded({ extended: false }), handleRootPostRequest);
7103 obj.app.post(url + 'tokenlogin', obj.bodyParser.urlencoded({ extended: false }), handleLoginRequest);
7104 obj.app.get(url + 'logout', handleLogoutRequest);
7105 obj.app.get(url + 'MeshServerRootCert.cer', handleRootCertRequest);
7106 obj.app.get(url + 'manifest.json', handleManifestRequest);
7107 obj.app.post(url + 'changepassword', obj.bodyParser.urlencoded({ extended: false }), handlePasswordChangeRequest);
7108 obj.app.post(url + 'deleteaccount', obj.bodyParser.urlencoded({ extended: false }), handleDeleteAccountRequest);
7109 obj.app.post(url + 'createaccount', obj.bodyParser.urlencoded({ extended: false }), handleCreateAccountRequest);
7110 obj.app.post(url + 'resetpassword', obj.bodyParser.urlencoded({ extended: false }), handleResetPasswordRequest);
7111 obj.app.post(url + 'resetaccount', obj.bodyParser.urlencoded({ extended: false }), handleResetAccountRequest);
7112 obj.app.get(url + 'checkmail', handleCheckMailRequest);
7113 obj.app.get(url + 'agentinvite', handleAgentInviteRequest);
7114 obj.app.get(url + 'userimage.ashx', handleUserImageRequest);
7115 obj.app.post(url + 'amtevents.ashx', obj.bodyParser.urlencoded({ extended: false }), obj.handleAmtEventRequest);
7116 obj.app.get(url + 'meshagents', obj.handleMeshAgentRequest);
7117 obj.app.get(url + 'messenger', handleMessengerRequest);
7118 obj.app.get(url + 'messenger.png', handleMessengerImageRequest);
7119 obj.app.get(url + 'meshosxagent', obj.handleMeshOsxAgentRequest);
7120 obj.app.get(url + 'meshsettings', obj.handleMeshSettingsRequest);
7121 obj.app.get(url + 'devicepowerevents.ashx', obj.handleDevicePowerEvents);
7122 obj.app.get(url + 'downloadfile.ashx', handleDownloadFile);
7123 obj.app.get(url + 'commander.ashx', handleMeshCommander);
7124 obj.app.post(url + 'uploadfile.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadFile);
7125 obj.app.post(url + 'uploadfilebatch.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadFileBatch);
7126 obj.app.post(url + 'uploadmeshcorefile.ashx', obj.bodyParser.urlencoded({ extended: false }), handleUploadMeshCoreFile);
7127 obj.app.post(url + 'oneclickrecovery.ashx', obj.bodyParser.urlencoded({ extended: false }), handleOneClickRecoveryFile);
7128 obj.app.get(url + 'userfiles/*', handleDownloadUserFiles);
7129 obj.app.ws(url + 'echo.ashx', handleEchoWebSocket);
7130 obj.app.ws(url + '2fahold.ashx', handle2faHoldWebSocket);
7131 obj.app.ws(url + 'apf.ashx', function (ws, req) { obj.parent.mpsserver.onWebSocketConnection(ws, req); })
7132 obj.app.get(url + 'webrelay.ashx', function (req, res) { res.send('Websocket connection expected'); });
7133 obj.app.get(url + 'health.ashx', function (req, res) { res.send('ok'); }); // TODO: Perform more server checking.
7134 obj.app.get(url + 'health', function (req, res) { res.status(200).send('ok'); }); // Health check endpoint for DigitalOcean
7135 obj.app.ws(url + 'webrelay.ashx', function (ws, req) { PerformWSSessionAuth(ws, req, false, handleRelayWebSocket); });
7136 obj.app.ws(url + 'webider.ashx', function (ws, req) { PerformWSSessionAuth(ws, req, false, function (ws1, req1, domain, user, cookie, authData) { obj.meshIderHandler.CreateAmtIderSession(obj, obj.db, ws1, req1, obj.args, domain, user); }); });
7137 obj.app.ws(url + 'control.ashx', function (ws, req) {
7138 getWebsocketArgs(ws, req, function (ws, req) {
7139 const domain = getDomain(req);
7140 if (obj.CheckWebServerOriginName(domain, req) == false) {
7141 try { ws.send(JSON.stringify({ action: 'close', cause: 'invalidorigin', msg: 'invalidorigin' })); } catch (ex) { }
7142 try { ws.close(); } catch (ex) { }
7143 return;
7144 }
7145 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { // Check 3FA URL key
7146 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'nokey' })); } catch (ex) { }
7147 try { ws.close(); } catch (ex) { }
7148 return;
7149 }
7150 PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
7151 if (user == null) { // User is not authenticated
7152 // Check for JWT token authentication (RMM+PSA Integration)
7153 if (obj.parent.jwtAuth) {
7154 const token = obj.parent.jwtAuth.extractToken(req);
7155 if (token) {
7156 obj.parent.jwtAuth.validateToken(token, function (jwtUser) {
7157 if (jwtUser) {
7158 // JWT authenticated successfully - create MeshUser directly
7159 obj.parent.debug('web', `JWT authenticated user: ${jwtUser._id}`);
7160 obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws, req, obj.args, domain, jwtUser, authData);
7161 } else {
7162 // Invalid JWT - try inner server authentication
7163 if (req.headers['x-meshauth'] === '*') {
7164 PerformWSSessionInnerAuth(ws, req, domain, function (ws1, req1, domain, user) {
7165 obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws1, req1, obj.args, domain, user, authData);
7166 });
7167 } else {
7168 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth' })); } catch (ex) { }
7169 try { ws.close(); } catch (ex) { }
7170 }
7171 }
7172 });
7173 return; // Exit early, callback will handle the rest
7174 }
7175 }
7176
7177 // No JWT or JWT auth not enabled - perform inner server authentication
7178 if (req.headers['x-meshauth'] === '*') {
7179 PerformWSSessionInnerAuth(ws, req, domain, function (ws1, req1, domain, user) { obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws1, req1, obj.args, domain, user, authData); }); // User is authenticated
7180 } else {
7181 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth' })); } catch (ex) { }
7182 try { ws.close(); } catch (ex) { } // user is not authenticated and inner authentication was not requested, disconnect now.
7183 }
7184 } else {
7185 obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws1, req1, obj.args, domain, user, authData); // User is authenticated
7186 }
7187 });
7188 });
7189 });
7190 obj.app.ws(url + 'devicefile.ashx', function (ws, req) { obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, ws, null, req, domain); });
7191 obj.app.get(url + 'devicefile.ashx', handleDeviceFile);
7192 obj.app.get(url + 'agentdownload.ashx', handleAgentDownloadFile);
7193 obj.app.get(url + 'logo.png', handleLogoRequest);
7194 obj.app.get(url + 'loginlogo.png', handleLoginLogoRequest);
7195 obj.app.get(url + 'pwalogo.png', handlePWALogoRequest);
7196 obj.app.post(url + 'translations', obj.bodyParser.urlencoded({ extended: false }), handleTranslationsRequest);
7197 obj.app.get(url + 'welcome.jpg', handleWelcomeImageRequest);
7198 obj.app.get(url + 'welcome.png', handleWelcomeImageRequest);
7199 obj.app.get(url + 'recordings.ashx', handleGetRecordings);
7200 obj.app.ws(url + 'recordings.ashx', handleGetRecordingsWebSocket);
7201 obj.app.get(url + 'player.htm', handlePlayerRequest);
7202 obj.app.get(url + 'player', handlePlayerRequest);
7203 obj.app.get(url + 'sharing', handleSharingRequest);
7204 obj.app.ws(url + 'agenttransfer.ashx', handleAgentFileTransfer); // Setup agent to/from server file transfer handler
7205 obj.app.ws(url + 'meshrelay.ashx', function (ws, req) {
7206 PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
7207 if (((parent.config.settings.desktopmultiplex === true) || (domain.desktopmultiplex === true)) && (req.query.p == 2)) {
7208 obj.meshDesktopMultiplexHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Desktop multiplexor 1-to-n
7209 } else {
7210 obj.meshRelayHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Normal relay 1-to-1
7211 }
7212 });
7213 });
7214 if (obj.args.wanonly != true) { // If the server is not in WAN mode, allow server relayed connections.
7215 obj.app.ws(url + 'localrelay.ashx', function (ws, req) {
7216 PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
7217 if ((user == null) || (cookie == null)) {
7218 try { ws1.close(); } catch (ex) { }
7219 } else {
7220 obj.meshRelayHandler.CreateLocalRelay(obj, ws1, req1, domain, user, cookie); // Local relay
7221 }
7222 });
7223 });
7224 }
7225
7226 // TEST ENDPOINT - Simple WebSocket test without auth
7227 obj.app.ws(url + 'test-canvas', function (ws, req) {
7228 console.log('[TEST] Test canvas endpoint hit!');
7229 ws.send(JSON.stringify({ type: 'test', message: 'Test endpoint works!' }));
7230 ws.on('message', function(msg) {
7231 ws.send(JSON.stringify({ type: 'echo', data: msg.toString() }));
7232 });
7233 });
7234
7235 // ========================================================================
7236 // CUSTOM CANVAS DESKTOP ENDPOINT (RMM+PSA Integration)
7237 // ========================================================================
7238 // Optimized WebSocket endpoint for canvas-based remote desktop viewer
7239 // Direct connection from React frontend to MeshCentral with JWT auth
7240 obj.app.ws(url + 'api/canvas-desktop/:nodeId', function (ws, req) {
7241 const nodeId = req.params.nodeId;
7242 parent.debug('web', `[CANVAS] New canvas desktop connection request for node: ${nodeId}`);
7243 console.log('[CANVAS] Phase 2 - New canvas desktop connection request for node:', nodeId);
7244
7245 // Extract JWT token from query parameter or Authorization header
7246 // Parse query string manually for WebSocket connections
7247 let token = req.query ? req.query.token : null;
7248
7249 if (!token && req.url) {
7250 const urlParts = req.url.split('?');
7251 if (urlParts.length > 1) {
7252 const queryString = urlParts[1];
7253 const params = new URLSearchParams(queryString);
7254 token = params.get('token');
7255 console.log('[CANVAS] Extracted token from URL query string');
7256 }
7257 }
7258
7259 if (!token && req.headers.authorization) {
7260 token = req.headers.authorization.split(' ')[1];
7261 console.log('[CANVAS] Extracted token from Authorization header');
7262 }
7263
7264 if (!token) {
7265 console.error('[CANVAS] No JWT token provided');
7266 console.error('[CANVAS] req.query:', req.query);
7267 console.error('[CANVAS] req.url:', req.url);
7268 console.error('[CANVAS] req.headers.authorization:', req.headers.authorization);
7269 ws.send(JSON.stringify({ type: 'error', message: 'No authentication token provided' }));
7270 ws.close();
7271 return;
7272 }
7273
7274 console.log('[CANVAS] Token received:', token.substring(0, 20) + '...');
7275
7276 // TEMPORARY: Simple bypass for testing until JWT module fixed
7277 // TODO: Remove this and restore full JWT validation once module initializes
7278 console.log('[CANVAS] DEBUG: process.env.CANVAS_TEST_MODE =', process.env.CANVAS_TEST_MODE);
7279 console.log('[CANVAS] DEBUG: typeof =', typeof process.env.CANVAS_TEST_MODE);
7280 console.log('[CANVAS] DEBUG: === "true" ?', process.env.CANVAS_TEST_MODE === 'true');
7281 const TEMP_TEST_MODE = process.env.CANVAS_TEST_MODE === 'true';
7282
7283 if (TEMP_TEST_MODE) {
7284 console.log('[CANVAS] ⚠️ TEST MODE: Bypassing JWT validation');
7285 const meshUser = {
7286 _id: 'user//test-user',
7287 name: 'Test User',
7288 tenant_id: '00000000-0000-0000-0000-000000000001',
7289 role: 'admin'
7290 };
7291 handleAuthenticatedConnection(meshUser);
7292 return;
7293 }
7294
7295 // Validate JWT token using existing JWT auth module
7296 if (!obj.parent.jwtAuth) {
7297 console.error('[CANVAS] JWT auth module not available');
7298 ws.send(JSON.stringify({ type: 'error', message: 'Authentication not configured' }));
7299 ws.close();
7300 return;
7301 }
7302
7303 obj.parent.jwtAuth.validateToken(token, function (meshUser) {
7304 if (!meshUser) {
7305 console.error('[CANVAS] Invalid JWT token or user not found');
7306 ws.send(JSON.stringify({ type: 'error', message: 'Authentication failed' }));
7307 ws.close();
7308 return;
7309 }
7310 handleAuthenticatedConnection(meshUser);
7311 });
7312
7313 // Handle authenticated connection (extracted to reuse for test mode)
7314 function handleAuthenticatedConnection(meshUser) {
7315
7316 const userId = meshUser._id;
7317 const tenantId = meshUser.tenant_id || meshUser.tenantId;
7318
7319 parent.debug('web', `[CANVAS] Phase 2 - JWT valid - User: ${userId}, Node: ${nodeId}`);
7320 console.log(`[CANVAS] Phase 2 - Authenticated user ${userId} for node ${nodeId}`);
7321
7322 // CRITICAL: Resume socket immediately - Express-ws might pause it
7323 try {
7324 if (ws._socket && typeof ws._socket.resume === 'function') {
7325 console.log('[CANVAS] Resuming socket...');
7326 ws._socket.resume();
7327 }
7328 } catch (e) {
7329 console.error('[CANVAS] Error resuming socket:', e.message);
7330 }
7331
7332 // Send immediate test message to verify WebSocket works
7333 try {
7334 console.log('[CANVAS] Sending immediate test message...');
7335 ws.send(JSON.stringify({ type: 'test', message: 'Immediate send test' }));
7336 console.log('[CANVAS] Test message sent');
7337 } catch (e) {
7338 console.error('[CANVAS] Error sending test message:', e.message);
7339 }
7340
7341 // Phase 2: Create peer object for desktop multiplexor integration
7342 const peer = {
7343 ws: ws,
7344 req: req,
7345 user: meshUser,
7346 nodeid: nodeId,
7347 meshid: null, // Will be set when we get node info
7348 // Mark this as browser viewer so the multiplexor routes screen data to us
7349 // The multiplexor checks peer.req.query.browser to identify viewers
7350 ...req
7351 };
7352
7353 // Mark as browser viewer (required by desktop multiplexor)
7354 if (!peer.req.query) peer.req.query = {};
7355 peer.req.query.browser = true;
7356
7357 // Store reference
7358 ws.canvasPeer = peer;
7359
7360 // Phase 2: Setup message handler for input events from dashboard
7361 ws.on('message', function(msg) {
7362 try {
7363 // Handle binary data (input events to agent)
7364 if (Buffer.isBuffer(msg)) {
7365 if (peer.deskMultiplexor && peer.deskMultiplexor.processData) {
7366 peer.deskMultiplexor.processData(peer, msg);
7367 }
7368 return;
7369 }
7370
7371 // Handle JSON control messages
7372 const data = JSON.parse(msg);
7373 parent.debug('web', `[CANVAS] Control message: ${data.type}`);
7374
7375 // Handle ping/pong for connection testing
7376 if (data.type === 'ping') {
7377 ws.send(JSON.stringify({
7378 type: 'pong',
7379 timestamp: Date.now(),
7380 server: 'meshcentral'
7381 }));
7382 }
7383
7384 // Handle quality/framerate adjustments
7385 if (data.type === 'quality' && data.value) {
7386 if (peer.imageCompression) {
7387 peer.imageCompression = Math.max(10, Math.min(100, data.value));
7388 console.log(`[CANVAS] Quality adjusted to ${peer.imageCompression}`);
7389 }
7390 }
7391
7392 if (data.type === 'framerate' && data.value) {
7393 if (peer.imageFrameRate) {
7394 peer.imageFrameRate = Math.max(10, Math.min(60, data.value));
7395 console.log(`[CANVAS] Framerate adjusted to ${peer.imageFrameRate}`);
7396 }
7397 }
7398
7399 } catch (err) {
7400 console.error('[CANVAS] Message parse error:', err.message);
7401 }
7402 });
7403
7404 ws.on('close', function() {
7405 parent.debug('web', `[CANVAS] Connection closed for node: ${nodeId}`);
7406 console.log(`[CANVAS] Phase 2 - Connection closed for node: ${nodeId}`);
7407
7408 // Phase 2: Remove from desktop multiplexor
7409 if (peer.deskMultiplexor && peer.deskMultiplexor.removePeer) {
7410 console.log('[CANVAS] Removing peer from desktop multiplexor');
7411 if (peer.deskMultiplexor.removePeer(peer)) {
7412 // Last peer removed, cleanup multiplexor reference
7413 delete obj.desktoprelays[nodeId];
7414 console.log('[CANVAS] Desktop multiplexor cleaned up');
7415 }
7416 }
7417 });
7418
7419 ws.on('error', function(err) {
7420 console.error('[CANVAS] WebSocket error:', err.message);
7421 });
7422
7423 // Send initial connection success message with error handling
7424 try {
7425 const connectedMsg = JSON.stringify({
7426 type: 'connected',
7427 nodeId: nodeId,
7428 userId: userId,
7429 tenantId: tenantId,
7430 message: 'Canvas desktop endpoint connected (Phase 2 - Screen Streaming)',
7431 phase: 2,
7432 capabilities: ['ping', 'auth', 'screen', 'input']
7433 });
7434
7435 ws.send(connectedMsg, function(err) {
7436 if (err) {
7437 console.error('[CANVAS] Failed to send connected message:', err.message);
7438 } else {
7439 console.log('[CANVAS] Connected message sent successfully');
7440 }
7441 });
7442
7443 console.log(`[CANVAS] Phase 2 - Initial handshake queued for dashboard`);
7444 } catch (err) {
7445 console.error('[CANVAS] ERROR sending connected message:', err.message);
7446 }
7447
7448 // Phase 2: Connect to desktop multiplexor
7449 try {
7450 console.log('[CANVAS] Accessing desktop relay handler...');
7451 console.log('[CANVAS] obj.meshDesktopMultiplexHandler exists:', !!obj.meshDesktopMultiplexHandler);
7452 console.log('[CANVAS] obj.desktoprelays exists:', !!obj.desktoprelays);
7453
7454 if (!obj.meshDesktopMultiplexHandler) {
7455 console.error('[CANVAS] ERROR: Desktop multiplexor handler not available');
7456 ws.send(JSON.stringify({ type: 'error', message: 'Desktop multiplexor handler not loaded' }));
7457 return;
7458 }
7459
7460 if (!obj.desktoprelays) {
7461 console.error('[CANVAS] ERROR: Desktop relays not initialized');
7462 ws.send(JSON.stringify({ type: 'error', message: 'Desktop relays not initialized' }));
7463 return;
7464 }
7465
7466 let deskMultiplexor = obj.desktoprelays[nodeId];
7467 console.log('[CANVAS] Existing multiplexor for node:', deskMultiplexor ? 'Found' : 'Not found');
7468
7469 if (deskMultiplexor == null || deskMultiplexor == 1) {
7470 console.log('[CANVAS] Creating new desktop multiplexor for node:', nodeId);
7471
7472 // Mark as pending creation
7473 obj.desktoprelays[nodeId] = 1;
7474
7475 // Create new multiplexor
7476 const CreateDesktopMultiplexor = obj.meshDesktopMultiplexHandler.CreateDesktopMultiplexor;
7477 console.log('[CANVAS] CreateDesktopMultiplexor function loaded:', typeof CreateDesktopMultiplexor);
7478
7479 CreateDesktopMultiplexor(
7480 obj, // Use webserver object as parent
7481 domain,
7482 nodeId,
7483 'canvas-' + Date.now(), // session id
7484 function (multiplexor) {
7485 console.log('[CANVAS] Multiplexor creation callback called');
7486 if (multiplexor != null) {
7487 console.log('[CANVAS] Desktop multiplexor created successfully');
7488 peer.deskMultiplexor = multiplexor;
7489 obj.desktoprelays[nodeId] = multiplexor;
7490
7491 // Add ourselves as a viewer
7492 multiplexor.addPeer(peer);
7493 console.log('[CANVAS] Added as peer to desktop multiplexor');
7494
7495 // Resume socket traffic
7496 if (ws._socket && ws._socket.resume) {
7497 ws._socket.resume();
7498 }
7499
7500 // Send ready message
7501 ws.send(JSON.stringify({ type: 'ready', message: 'Desktop streaming active' }));
7502 } else {
7503 console.error('[CANVAS] Failed to create desktop multiplexor - callback returned null');
7504 delete obj.desktoprelays[nodeId];
7505 ws.send(JSON.stringify({ type: 'error', message: 'Failed to establish desktop session' }));
7506 ws.close();
7507 }
7508 }
7509 );
7510 } else {
7511 console.log('[CANVAS] Using existing desktop multiplexor for node:', nodeId);
7512 peer.deskMultiplexor = deskMultiplexor;
7513
7514 // Add ourselves as a viewer to existing multiplexor
7515 deskMultiplexor.addPeer(peer);
7516 console.log('[CANVAS] Added as peer to existing desktop multiplexor');
7517
7518 // Resume socket traffic
7519 if (ws._socket && ws._socket.resume) {
7520 ws._socket.resume();
7521 }
7522
7523 // Send ready message
7524 ws.send(JSON.stringify({ type: 'ready', message: 'Desktop streaming active' }));
7525 }
7526
7527 console.log(`[CANVAS] Phase 2 - Complete setup for user ${userId} to node ${nodeId}`);
7528 } catch (err) {
7529 console.error('[CANVAS] ERROR in Phase 2 setup:', err.message);
7530 console.error('[CANVAS] Stack:', err.stack);
7531 ws.send(JSON.stringify({ type: 'error', message: 'Internal server error: ' + err.message }));
7532 }
7533 } // end handleAuthenticatedConnection
7534 });
7535 // End of Custom Canvas Desktop Endpoint
7536 // ========================================================================
7537
7538 obj.app.get(url + 'invite', handleInviteRequest);
7539 obj.app.post(url + 'invite', obj.bodyParser.urlencoded({ extended: false }), handleInviteRequest);
7540
7541 if (parent.pluginHandler != null) {
7542 obj.app.get(url + 'pluginadmin.ashx', obj.handlePluginAdminReq);
7543 obj.app.post(url + 'pluginadmin.ashx', obj.bodyParser.urlencoded({ extended: false }), obj.handlePluginAdminPostReq);
7544 obj.app.get(url + 'pluginHandler.js', obj.handlePluginJS);
7545 }
7546
7547 // New account CAPTCHA request
7548 if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
7549 obj.app.get(url + 'newAccountCaptcha.ashx', handleNewAccountCaptchaRequest);
7550 }
7551
7552 // Check CrowdSec Bounser if configured
7553 if (parent.crowdSecBounser != null) {
7554 obj.app.get(url + 'captcha.ashx', handleCaptchaGetRequest);
7555 obj.app.post(url + 'captcha.ashx', obj.bodyParser.urlencoded({ extended: false }), handleCaptchaPostRequest);
7556 }
7557
7558 // Setup IP-KVM relay if supported
7559 if (domain.ipkvm) {
7560 obj.app.ws(url + 'ipkvm.ashx/*', function (ws, req) {
7561 const domain = getDomain(req);
7562 if (domain == null) { parent.debug('web', 'ipkvm: failed domain checks.'); try { ws.close(); } catch (ex) { } return; }
7563 parent.ipKvmManager.handleIpKvmWebSocket(domain, ws, req);
7564 });
7565 obj.app.get(url + 'ipkvm.ashx/*', function (req, res, next) {
7566 const domain = getDomain(req);
7567 if (domain == null) return;
7568 parent.ipKvmManager.handleIpKvmGet(domain, req, res, next);
7569 });
7570 }
7571
7572 // Setup RDP unless indicated as disabled
7573 if (domain.mstsc !== false) {
7574 obj.app.get(url + 'mstsc.html', function (req, res) { handleMSTSCRequest(req, res, 'mstsc'); });
7575 obj.app.ws(url + 'mstscrelay.ashx', function (ws, req) {
7576 const domain = getDomain(req);
7577 if (domain == null) { parent.debug('web', 'mstsc: failed checks.'); try { ws.close(); } catch (e) { } return; }
7578 // If no user is logged in and we have a default user, set it now.
7579 if ((req.session.userid == null) && (typeof obj.args.user == 'string') && (obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()])) { req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase(); }
7580 try { require('./apprelays.js').CreateMstscRelay(obj, obj.db, ws, req, obj.args, domain); } catch (ex) { console.log(ex); }
7581 });
7582 }
7583
7584 // Setup SSH if needed
7585 if (domain.ssh === true) {
7586 obj.app.get(url + 'ssh.html', function (req, res) { handleMSTSCRequest(req, res, 'ssh'); });
7587 obj.app.ws(url + 'sshrelay.ashx', function (ws, req) {
7588 const domain = getDomain(req);
7589 if (domain == null) { parent.debug('web', 'ssh: failed checks.'); try { ws.close(); } catch (e) { } return; }
7590 // If no user is logged in and we have a default user, set it now.
7591 if ((req.session.userid == null) && (typeof obj.args.user == 'string') && (obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()])) { req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase(); }
7592 try { require('./apprelays.js').CreateSshRelay(obj, obj.db, ws, req, obj.args, domain); } catch (ex) { console.log(ex); }
7593 });
7594 obj.app.ws(url + 'sshterminalrelay.ashx', function (ws, req) {
7595 PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
7596 require('./apprelays.js').CreateSshTerminalRelay(obj, obj.db, ws1, req1, domain, user, cookie, obj.args);
7597 });
7598 });
7599 obj.app.ws(url + 'sshfilesrelay.ashx', function (ws, req) {
7600 PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
7601 require('./apprelays.js').CreateSshFilesRelay(obj, obj.db, ws1, req1, domain, user, cookie, obj.args);
7602 });
7603 });
7604 }
7605
7606 // Setup firebase push only server
7607 if ((obj.parent.firebase != null) && (obj.parent.config.firebase)) {
7608 if (obj.parent.config.firebase.pushrelayserver) { parent.debug('email', 'Firebase-pushrelay-handler'); obj.app.post(url + 'firebaserelay.aspx', obj.bodyParser.urlencoded({ extended: false }), handleFirebasePushOnlyRelayRequest); }
7609 if (obj.parent.config.firebase.relayserver) { parent.debug('email', 'Firebase-relay-handler'); obj.app.ws(url + 'firebaserelay.aspx', handleFirebaseRelayRequest); }
7610 }
7611
7612 // Setup auth strategies using passport if needed
7613 if (typeof domain.authstrategies == 'object') {
7614 parent.authLog('setupHTTPHandlers', `Setting up authentication strategies login and callback URLs for ${domain.id == '' ? 'root' : '"' + domain.id + '"'} domain.`);
7615 // Twitter
7616 if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.twitter) != 0) {
7617 obj.app.get(url + 'auth-twitter', function (req, res, next) {
7618 var domain = getDomain(req);
7619 if (domain.passport == null) { next(); return; }
7620 domain.passport.authenticate('twitter-' + domain.id)(req, res, function (err) { console.log('c1', err, req.session); next(); });
7621 });
7622 obj.app.get(url + 'auth-twitter-callback', function (req, res, next) {
7623 var domain = getDomain(req);
7624 if (domain.passport == null) { next(); return; }
7625 if ((Object.keys(req.session).length == 0) && (req.query.nmr == null)) {
7626 // This is an empty session likely due to the 302 redirection, redirect again (this is a bit of a hack).
7627 var url = req.url;
7628 if (url.indexOf('?') >= 0) { url += '&nmr=1'; } else { url += '?nmr=1'; } // Add this to the URL to prevent redirect loop.
7629 res.set('Content-Type', 'text/html');
7630 res.end('<html><head><meta http-equiv="refresh" content=0;url="' + encodeURIComponent(url) + '"></head><body></body></html>');
7631 } else {
7632 domain.passport.authenticate('twitter-' + domain.id, { failureRedirect: domain.url })(req, res, function (err) { if (err != null) { console.log(err); } next(); });
7633 }
7634 }, handleStrategyLogin);
7635 }
7636
7637 // Google
7638 if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.google) != 0) {
7639 obj.app.get(url + 'auth-google', function (req, res, next) {
7640 var domain = getDomain(req);
7641 if (domain.passport == null) { next(); return; }
7642 domain.passport.authenticate('google-' + domain.id, { scope: ['profile', 'email'] })(req, res, next);
7643 });
7644 obj.app.get(url + 'auth-google-callback', function (req, res, next) {
7645 var domain = getDomain(req);
7646 if (domain.passport == null) { next(); return; }
7647 domain.passport.authenticate('google-' + domain.id, { failureRedirect: domain.url })(req, res, function (err) { if (err != null) { console.log(err); } next(); });
7648 }, handleStrategyLogin);
7649 }
7650
7651 // GitHub
7652 if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.github) != 0) {
7653 obj.app.get(url + 'auth-github', function (req, res, next) {
7654 var domain = getDomain(req);
7655 if (domain.passport == null) { next(); return; }
7656 domain.passport.authenticate('github-' + domain.id, { scope: ['user:email'] })(req, res, next);
7657 });
7658 obj.app.get(url + 'auth-github-callback', function (req, res, next) {
7659 var domain = getDomain(req);
7660 if (domain.passport == null) { next(); return; }
7661 domain.passport.authenticate('github-' + domain.id, { failureRedirect: domain.url })(req, res, next);
7662 }, handleStrategyLogin);
7663 }
7664
7665 // Azure
7666 if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.azure) != 0) {
7667 obj.app.get(url + 'auth-azure', function (req, res, next) {
7668 var domain = getDomain(req);
7669 if (domain.passport == null) { next(); return; }
7670 domain.passport.authenticate('azure-' + domain.id, { state: obj.parent.encodeCookie({ 'p': 'azure' }, obj.parent.loginCookieEncryptionKey) })(req, res, next);
7671 });
7672 obj.app.get(url + 'auth-azure-callback', function (req, res, next) {
7673 var domain = getDomain(req);
7674 if (domain.passport == null) { next(); return; }
7675 if ((Object.keys(req.session).length == 0) && (req.query.nmr == null)) {
7676 // This is an empty session likely due to the 302 redirection, redirect again (this is a bit of a hack).
7677 var url = req.url;
7678 if (url.indexOf('?') >= 0) { url += '&nmr=1'; } else { url += '?nmr=1'; } // Add this to the URL to prevent redirect loop.
7679 res.set('Content-Type', 'text/html');
7680 res.end('<html><head><meta http-equiv="refresh" content=0;url="' + encodeURIComponent(url) + '"></head><body></body></html>');
7681 } else {
7682 if (req.query.state != null) {
7683 var c = obj.parent.decodeCookie(req.query.state, obj.parent.loginCookieEncryptionKey, 10); // 10 minute timeout
7684 if ((c != null) && (c.p == 'azure')) { domain.passport.authenticate('azure-' + domain.id, { failureRedirect: domain.url })(req, res, next); return; }
7685 }
7686 next();
7687 }
7688 }, handleStrategyLogin);
7689 }
7690
7691 // Setup OpenID Connect URLs
7692 if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.oidc) != 0) {
7693 let authURL = url + 'auth-oidc'
7694 parent.authLog('setupHTTPHandlers', `OIDC: Authorization URL: ${authURL}`);
7695 obj.app.get(authURL, function (req, res, next) {
7696 var domain = getDomain(req);
7697 if (domain.passport == null) { next(); return; }
7698 domain.passport.authenticate(`oidc-${domain.id}`, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
7699 });
7700 let redirectPath;
7701 if (typeof domain.authstrategies.oidc.client.redirect_uri == 'string') {
7702 redirectPath = (new URL(domain.authstrategies.oidc.client.redirect_uri)).pathname;
7703 } else if (Array.isArray(domain.authstrategies.oidc.client.redirect_uris)) {
7704 redirectPath = (new URL(domain.authstrategies.oidc.client.redirect_uris[0])).pathname;
7705 } else {
7706 redirectPath = url + 'auth-oidc-callback';
7707 }
7708 parent.authLog('setupHTTPHandlers', `OIDC: Callback URL: ${redirectPath}`);
7709 obj.app.get(redirectPath, obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
7710 var domain = getDomain(req);
7711 if (domain.passport == null) { next(); return; }
7712 if (req.session && req.session.userid) { next(); return; } // already logged in so dont authenticate just carry on
7713 if (req.session && req.session['oidc-' + domain.id]) { // we have a request to login so do authenticate
7714 domain.passport.authenticate(`oidc-${domain.id}`, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
7715 } else { // no idea so carry on
7716 next(); return;
7717 }
7718 }, handleStrategyLogin);
7719 }
7720
7721 // Generic SAML
7722 if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.saml) != 0) {
7723 obj.app.get(url + 'auth-saml', function (req, res, next) {
7724 var domain = getDomain(req);
7725 if (domain.passport == null) { next(); return; }
7726 //set RelayState when queries are passed
7727 if (Object.keys(req.query).length != 0){
7728 req.query.RelayState = encodeURIComponent(`${req.protocol}://${req.hostname}${req.originalUrl}`.replace('auth-saml/',''))
7729 }
7730 domain.passport.authenticate('saml-' + domain.id, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
7731 });
7732 obj.app.post(url + 'auth-saml-callback', obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
7733 var domain = getDomain(req);
7734 if (domain.passport == null) { next(); return; }
7735 domain.passport.authenticate('saml-' + domain.id, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
7736 }, handleStrategyLogin);
7737 }
7738
7739 // Intel SAML
7740 if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.intelSaml) != 0) {
7741 obj.app.get(url + 'auth-intel', function (req, res, next) {
7742 var domain = getDomain(req);
7743 if (domain.passport == null) { next(); return; }
7744 domain.passport.authenticate('isaml-' + domain.id, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
7745 });
7746 obj.app.post(url + 'auth-intel-callback', obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
7747 var domain = getDomain(req);
7748 if (domain.passport == null) { next(); return; }
7749 domain.passport.authenticate('isaml-' + domain.id, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
7750 }, handleStrategyLogin);
7751 }
7752
7753 // JumpCloud SAML
7754 if ((domain.authstrategies.authStrategyFlags & domainAuthStrategyConsts.jumpCloudSaml) != 0) {
7755 obj.app.get(url + 'auth-jumpcloud', function (req, res, next) {
7756 var domain = getDomain(req);
7757 if (domain.passport == null) { next(); return; }
7758 domain.passport.authenticate('jumpcloud-' + domain.id, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
7759 });
7760 obj.app.post(url + 'auth-jumpcloud-callback', obj.bodyParser.urlencoded({ extended: false }), function (req, res, next) {
7761 var domain = getDomain(req);
7762 if (domain.passport == null) { next(); return; }
7763 domain.passport.authenticate('jumpcloud-' + domain.id, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
7764 }, handleStrategyLogin);
7765 }
7766 }
7767
7768 // Setup Duo HTTP handlers if supported
7769 if ((typeof domain.duo2factor == 'object') && (typeof domain.duo2factor.integrationkey == 'string') && (typeof domain.duo2factor.secretkey == 'string') && (typeof domain.duo2factor.apihostname == 'string')) {
7770 // Duo authentication handler
7771 obj.app.get(url + 'auth-duo', function (req, res){
7772 var domain = getDomain(req);
7773 const sec = parent.decryptSessionData(req.session.e);
7774 if ((req.query.state !== sec.duostate) || (req.query.duo_code == null)) {
7775 // The state returned from Duo is not the same as what was in the session, so must fail
7776 parent.debug('web', 'handleRootRequest: Duo 2FA state failed.');
7777 req.session.loginmode = 1;
7778 req.session.messageid = 117; // Invalid security check
7779 res.redirect(domain.url + getQueryPortion(req)); // redirect back to main page
7780 return;
7781 } else {
7782 const duo = require('@duosecurity/duo_universal');
7783 const client = new duo.Client({
7784 clientId: domain.duo2factor.integrationkey,
7785 clientSecret: domain.duo2factor.secretkey,
7786 apiHost: domain.duo2factor.apihostname,
7787 redirectUrl: obj.generateBaseURL(domain, req) + 'auth-duo' + (domain.loginkey != null ? ('?key=' + domain.loginkey) : '')
7788 });
7789 if (sec.duoconfig == 1) {
7790 // Login data correct, now exchange authorization code for 2FA
7791 var userid = req.session.userid;
7792 client.exchangeAuthorizationCodeFor2FAResult(req.query.duo_code, userid.split('/')[2]).then(function (data) {
7793 // Duo 2FA exchange success
7794 parent.debug('web', 'handleRootRequest: Duo 2FA configuration success.');
7795 // Enable Duo for this user
7796 var user = obj.users[userid];
7797 if (user.otpduo == null) {
7798 user.otpduo = {};
7799 db.SetUser(user);
7800 // Notify change
7801 var targets = ['*', 'server-users', user._id];
7802 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
7803 var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msgid: 160, msg: "Enabled duo two-factor authentication.", domain: domain.id };
7804 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.
7805 parent.DispatchEvent(targets, obj, event);
7806 }
7807 // Clear the Duo state
7808 delete sec.duostate;
7809 delete sec.duoconfig;
7810 req.session.e = parent.encryptSessionData(sec);
7811 var url = req.session.duorurl;
7812 delete req.session.duorurl;
7813 res.redirect(url ? url : domain.url); // Redirect back to the user's original page
7814 }).catch(function (err) {
7815 const sec = parent.decryptSessionData(req.session.e);
7816 // Duo 2FA exchange success
7817 parent.debug('web', 'handleRootRequest: Duo 2FA configuration failed.');
7818 // Clear the Duo state
7819 delete sec.duostate;
7820 delete sec.duoconfig;
7821 req.session.e = parent.encryptSessionData(sec);
7822 var url = req.session.duorurl;
7823 delete req.session.duorurl;
7824 res.redirect(url ? url : domain.url); // Redirect back to the user's original page
7825 });
7826 } else {
7827 // User credentials are stored in session, just check again and get userid
7828 obj.authenticate(sec.tuser, sec.tpass, domain, function (err, userid, passhint, loginOptions) {
7829 if ((userid != null) && (err == null)) {
7830 var user = obj.users[userid]; // Get user object
7831 // Login data correct, now exchange authorization code for 2FA
7832 client.exchangeAuthorizationCodeFor2FAResult(req.query.duo_code, userid.split('/')[2]).then(function (data) {
7833 const sec = parent.decryptSessionData(req.session.e);
7834 // Duo 2FA exchange success
7835 parent.debug('web', 'handleRootRequest: Duo 2FA authorization success.');
7836 req.session.userid = userid;
7837 delete req.session.currentNode;
7838 req.session.ip = req.clientIp; // Bind this session to the IP address of the request
7839 setSessionRandom(req);
7840 // Clear the Duo state and user/pass
7841 delete sec.duostate;
7842 delete sec.tuser;
7843 delete sec.tpass;
7844 req.session.e = parent.encryptSessionData(sec);
7845 obj.parent.authLog('https', 'Accepted Duo authentication for ' + userid + ' from ' + req.clientIp + ':' + req.connection.remotePort, { useragent: req.headers['user-agent'], sessionid: req.session.x });
7846 // Notify account login
7847 var targets = ['*', 'server-users', user._id];
7848 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
7849 const ua = obj.getUserAgentInfo(req);
7850 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: 'duo' };
7851 obj.parent.DispatchEvent(targets, obj, loginEvent);
7852 res.redirect(domain.url + getQueryPortion(req));
7853 }).catch(function (err) {
7854 const sec = parent.decryptSessionData(req.session.e);
7855 // Duo 2FA exchange failed
7856 parent.debug('web', 'handleRootRequest: Duo 2FA authorization failed.');
7857 // Clear the Duo state
7858 delete sec.duostate;
7859 req.session.e = parent.encryptSessionData(sec);
7860 req.session.loginmode = 1;
7861 req.session.messageid = 117; // Invalid security check
7862 // Notify account 2fa failed login
7863 const ua = obj.getUserAgentInfo(req);
7864 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] });
7865 obj.setbad2Fa(req);
7866 res.redirect(domain.url + getQueryPortion(req));
7867 });
7868 } else {
7869 // Login failed
7870 parent.debug('web', 'handleRootRequest: login authorization failed when returning from Duo 2FA.');
7871 req.session.loginmode = 1;
7872 res.redirect(domain.url + getQueryPortion(req)); // redirect back to main page
7873 return;
7874 }
7875 });
7876 }
7877 }
7878 });
7879
7880 // Configure Duo handler
7881 obj.app.get(url + 'add-duo', function (req, res) {
7882 var domain = getDomain(req);
7883 if (req.session.userid == null) {
7884 res.sendStatus(404);
7885 } else {
7886 // Redirect to Duo here
7887 const duo = require('@duosecurity/duo_universal');
7888 const client = new duo.Client({
7889 clientId: domain.duo2factor.integrationkey,
7890 clientSecret: domain.duo2factor.secretkey,
7891 apiHost: domain.duo2factor.apihostname,
7892 redirectUrl: obj.generateBaseURL(domain, req) + 'auth-duo' + (domain.loginkey != null ? ('&key=' + domain.loginkey) : '')
7893 });
7894
7895 // Setup the Duo configuration
7896 if (req.query.rurl) { req.session.duorurl = req.query.rurl; } // Set Duo return URL
7897 const sec = parent.decryptSessionData(req.session.e);
7898 sec.duostate = client.generateState();
7899 sec.duoconfig = 1;
7900 req.session.e = parent.encryptSessionData(sec);
7901 parent.debug('web', 'Redirecting user ' + req.session.userid + ' to Duo for configuration');
7902 res.redirect(client.createAuthUrl(req.session.userid.split('/')[2], sec.duostate));
7903 }
7904 });
7905 }
7906
7907 // Server redirects
7908 if (parent.config.domains[i].redirects) { for (var j in parent.config.domains[i].redirects) { if (j[0] != '_') { obj.app.get(url + j, obj.handleDomainRedirect); } } }
7909
7910 // Server picture
7911 obj.app.get(url + 'serverpic.ashx', function (req, res) {
7912 // Check if we have "server.jpg" in the data folder, if so, use that.
7913 if ((parent.configurationFiles != null) && (parent.configurationFiles['server.png'] != null)) {
7914 res.set({ 'Content-Type': 'image/png' });
7915 res.send(parent.configurationFiles['server.png']);
7916 } else {
7917 // Check if we have "server.jpg" in the data folder, if so, use that.
7918 var p = obj.path.join(obj.parent.datapath, 'server.png');
7919 if (obj.fs.existsSync(p)) {
7920 // Use the data folder server picture
7921 try { res.sendFile(p); } catch (ex) { res.sendStatus(404); }
7922 } else {
7923 var domain = getDomain(req);
7924 if ((domain != null) && (domain.webpublicpath != null) && (obj.fs.existsSync(obj.path.join(domain.webpublicpath, 'images/server-256.png')))) {
7925 // Use the domain server picture
7926 try { res.sendFile(obj.path.join(domain.webpublicpath, 'images/server-256.png')); } catch (ex) { res.sendStatus(404); }
7927 } else if (parent.webPublicOverridePath && obj.fs.existsSync(obj.path.join(obj.parent.webPublicOverridePath, 'images/server-256.png'))) {
7928 // Use the override server picture
7929 try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, 'images/server-256.png')); } catch (ex) { res.sendStatus(404); }
7930 } else {
7931 // Use the default server picture
7932 try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'images/server-256.png')); } catch (ex) { res.sendStatus(404); }
7933 }
7934 }
7935 }
7936 });
7937
7938 // Receive mesh agent connections
7939 obj.app.ws(url + 'agent.ashx', function (ws, req) {
7940 var domain = checkAgentIpAddress(ws, req);
7941 if (domain == null) { parent.debug('web', 'Got agent connection with bad domain or blocked IP address ' + req.clientIp + ', holding.'); return; }
7942 if (domain.agentkey && ((req.query.key == null) || (domain.agentkey.indexOf(req.query.key) == -1))) { return; } // If agent key is required and not provided or not valid, just hold the websocket and do nothing.
7943 //console.log('Agent connect: ' + req.clientIp);
7944 try { obj.meshAgentHandler.CreateMeshAgent(obj, obj.db, ws, req, obj.args, domain); } catch (ex) { console.log(ex); }
7945 });
7946
7947 // Setup MQTT broker over websocket
7948 if (obj.parent.mqttbroker != null) {
7949 obj.app.ws(url + 'mqtt.ashx', function (ws, req) {
7950 var domain = checkAgentIpAddress(ws, req);
7951 if (domain == null) { parent.debug('web', 'Got agent connection with bad domain or blocked IP address ' + req.clientIp + ', holding.'); return; }
7952 var serialtunnel = SerialTunnel();
7953 serialtunnel.xtransport = 'ws';
7954 serialtunnel.xdomain = domain;
7955 serialtunnel.xip = req.clientIp;
7956 ws.on('message', function (b) { serialtunnel.updateBuffer(Buffer.from(b, 'binary')) });
7957 serialtunnel.forwardwrite = function (b) { ws.send(b, 'binary') }
7958 ws.on('close', function () { serialtunnel.emit('end'); });
7959 obj.parent.mqttbroker.handle(serialtunnel); // Pass socket wrapper to MQTT broker
7960 });
7961 }
7962
7963 // Setup any .well-known folders
7964 var p = obj.parent.path.join(obj.parent.datapath, '.well-known' + ((parent.config.domains[i].id == '') ? '' : ('-' + parent.config.domains[i].id)));
7965 if (obj.parent.fs.existsSync(p)) { obj.app.use(url + '.well-known', obj.express.static(p)); }
7966
7967 // Setup the alternative agent-only port
7968 if (obj.agentapp) {
7969 // Receive mesh agent connections on alternate port
7970 obj.agentapp.ws(url + 'agent.ashx', function (ws, req) {
7971 var domain = checkAgentIpAddress(ws, req);
7972 if (domain == null) { parent.debug('web', 'Got agent connection with bad domain or blocked IP address ' + req.clientIp + ', holding.'); return; }
7973 if (domain.agentkey && ((req.query.key == null) || (domain.agentkey.indexOf(req.query.key) == -1))) { return; } // If agent key is required and not provided or not valid, just hold the websocket and do nothing.
7974 try { obj.meshAgentHandler.CreateMeshAgent(obj, obj.db, ws, req, obj.args, domain); } catch (e) { console.log(e); }
7975 });
7976
7977 // Setup mesh relay on alternative agent-only port
7978 obj.agentapp.ws(url + 'meshrelay.ashx', function (ws, req) {
7979 PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie, authData) {
7980 if (((parent.config.settings.desktopmultiplex === true) || (domain.desktopmultiplex === true)) && (req.query.p == 2)) {
7981 obj.meshDesktopMultiplexHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Desktop multiplexor 1-to-n
7982 } else {
7983 obj.meshRelayHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Normal relay 1-to-1
7984 }
7985 });
7986 });
7987
7988 // Allows agents to transfer files
7989 obj.agentapp.ws(url + 'devicefile.ashx', function (ws, req) { obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, ws, null, req, domain); });
7990
7991 // Setup agent to/from server file transfer handler
7992 obj.agentapp.ws(url + 'agenttransfer.ashx', handleAgentFileTransfer); // Setup agent to/from server file transfer handler
7993
7994 // Setup agent downloads for meshcore updates
7995 obj.agentapp.get(url + 'meshagents', obj.handleMeshAgentRequest);
7996
7997 // Setup agent file downloads
7998 obj.agentapp.get(url + 'agentdownload.ashx', handleAgentDownloadFile);
7999
8000 // Setup APF.ashx for AMT communication
8001 if (obj.parent.mpsserver != null) {
8002 obj.agentapp.ws(url + 'apf.ashx', function (ws, req) { obj.parent.mpsserver.onWebSocketConnection(ws, req); })
8003 }
8004 }
8005
8006 // Setup web relay on this web server if needed
8007 // We set this up when a DNS name is used as a web relay instead of a port
8008 if (obj.args.relaydns != null) {
8009 obj.webRelayRouter = require('express').Router();
8010
8011 // This is the magic URL that will setup the relay session
8012 obj.webRelayRouter.get('/control-redirect.ashx', function (req, res, next) {
8013 if (obj.args.relaydns.indexOf(req.hostname) == -1) { res.sendStatus(404); return; }
8014 if ((req.session.userid == null) && obj.args.user && obj.users['user//' + obj.args.user.toLowerCase()]) { req.session.userid = 'user//' + obj.args.user.toLowerCase(); } // Use a default user if needed
8015 res.set({ 'Cache-Control': 'no-store' });
8016 parent.debug('web', 'webRelaySetup');
8017
8018 // Decode the relay cookie
8019 if (req.query.c == null) { res.sendStatus(404); return; }
8020
8021 // Decode and check if this relay cookie is valid
8022 var userid, domainid, domain, nodeid, addr, port, appid, webSessionId, expire, publicid;
8023 const urlCookie = obj.parent.decodeCookie(req.query.c, parent.loginCookieEncryptionKey, 32); // Allow cookies up to 32 minutes old. The web page will renew this cookie every 30 minutes.
8024 if (urlCookie == null) { res.sendStatus(404); return; }
8025
8026 // Decode the incoming cookie
8027 if ((urlCookie.ruserid != null) && (urlCookie.x != null)) {
8028 if (parent.webserver.destroyedSessions[urlCookie.ruserid + '/' + urlCookie.x] != null) { res.sendStatus(404); return; }
8029
8030 // This is a standard user, figure out what our web relay will be.
8031 if (req.session.x != urlCookie.x) { req.session.x = urlCookie.x; } // Set the sessionid if missing
8032 if (req.session.userid != urlCookie.ruserid) { req.session.userid = urlCookie.ruserid; } // Set the session userid if missing
8033 if (req.session.z) { delete req.session.z; } // Clear the web relay guest session
8034 userid = req.session.userid;
8035 domainid = userid.split('/')[1];
8036 domain = parent.config.domains[domainid];
8037 nodeid = ((req.query.relayid != null) ? req.query.relayid : req.query.n);
8038 addr = (req.query.addr != null) ? req.query.addr : '127.0.0.1';
8039 port = parseInt(req.query.p);
8040 appid = parseInt(req.query.appid);
8041 webSessionId = req.session.userid + '/' + req.session.x;
8042
8043 // Check that all the required arguments are present
8044 if ((req.session.userid == null) || (req.session.x == null) || (req.query.n == null) || (req.query.p == null) || (parent.webserver.destroyedSessions[webSessionId] != null) || ((req.query.appid != 1) && (req.query.appid != 2))) { res.redirect('/'); return; }
8045 } else if (urlCookie.r == 8) {
8046 // This is a guest user, figure out what our web relay will be.
8047 userid = urlCookie.userid;
8048 domainid = userid.split('/')[1];
8049 domain = parent.config.domains[domainid];
8050 nodeid = urlCookie.nid;
8051 addr = (urlCookie.addr != null) ? urlCookie.addr : '127.0.0.1';
8052 port = urlCookie.port;
8053 appid = (urlCookie.p == 16) ? 2 : 1; // appid: 1 = HTTP, 2 = HTTPS
8054 webSessionId = userid + '/' + urlCookie.pid;
8055 publicid = urlCookie.pid;
8056 if (req.session.x) { delete req.session.x; } // Clear the web relay sessionid
8057 if (req.session.userid) { delete req.session.userid; } // Clear the web relay userid
8058 if (req.session.z != webSessionId) { req.session.z = webSessionId; } // Set the web relay guest session
8059 expire = urlCookie.expire;
8060 if ((expire != null) && (expire <= Date.now())) { parent.debug('webrelay', 'expired link'); res.sendStatus(404); return; }
8061 }
8062
8063 // No session identifier was setup, exit now
8064 if (webSessionId == null) { res.sendStatus(404); return; }
8065
8066 // Check that we have an exact session on any of the relay DNS names
8067 var xrelaySessionId, xrelaySession, freeRelayHost, oldestRelayTime, oldestRelayHost;
8068 for (var hostIndex in obj.args.relaydns) {
8069 const host = obj.args.relaydns[hostIndex];
8070 xrelaySessionId = webSessionId + '/' + host;
8071 xrelaySession = webRelaySessions[xrelaySessionId];
8072 if (xrelaySession == null) {
8073 // We found an unused hostname, save this as it could be useful.
8074 if (freeRelayHost == null) { freeRelayHost = host; }
8075 } else {
8076 // Check if we already have a relay session that matches exactly what we want
8077 if ((xrelaySession.domain.id == domain.id) && (xrelaySession.userid == userid) && (xrelaySession.nodeid == nodeid) && (xrelaySession.addr == addr) && (xrelaySession.port == port) && (xrelaySession.appid == appid)) {
8078 // We found an exact match, we are all setup already, redirect to root of that DNS name
8079 if (host == req.hostname) {
8080 // Request was made on the same host, redirect to root.
8081 res.redirect('/');
8082 } else {
8083 // Request was made to a different host
8084 const httpport = ((args.aliasport != null) ? args.aliasport : args.port);
8085 res.redirect('https://' + host + ((httpport != 443) ? (':' + httpport) : '') + '/');
8086 }
8087 return;
8088 }
8089
8090 // Keep a record of the oldest web relay session, this could be useful.
8091 if (oldestRelayHost == null) {
8092 // Oldest host not set yet, set it
8093 oldestRelayHost = host;
8094 oldestRelayTime = xrelaySession.lastOperation;
8095 } else {
8096 // Check if this host is older then oldest so far
8097 if (oldestRelayTime > xrelaySession.lastOperation) {
8098 oldestRelayHost = host;
8099 oldestRelayTime = xrelaySession.lastOperation;
8100 }
8101 }
8102 }
8103 }
8104
8105 // Check that the user has rights to access this device
8106 parent.webserver.GetNodeWithRights(domain, userid, nodeid, function (node, rights, visible) {
8107 // If there is no remote control or relay rights, reject this web relay
8108 if ((rights & 0x00200008) == 0) { res.sendStatus(404); return; } // MESHRIGHT_REMOTECONTROL or MESHRIGHT_RELAY
8109
8110 // Check if there is a free relay DNS name we can use
8111 var selectedHost = null;
8112 if (freeRelayHost != null) {
8113 // There is a free one, use it.
8114 selectedHost = freeRelayHost;
8115 } else {
8116 // No free ones, close the oldest one
8117 selectedHost = oldestRelayHost;
8118 }
8119 xrelaySessionId = webSessionId + '/' + selectedHost;
8120
8121 if (selectedHost == req.hostname) {
8122 // If this web relay session id is not free, close it now
8123 xrelaySession = webRelaySessions[xrelaySessionId];
8124 if (xrelaySession != null) { xrelaySession.close(); delete webRelaySessions[xrelaySessionId]; }
8125
8126 // Create a web relay session
8127 const relaySession = require('./apprelays.js').CreateWebRelaySession(obj, db, req, args, domain, userid, nodeid, addr, port, appid, xrelaySessionId, expire, node.mtype);
8128 relaySession.xpublicid = publicid;
8129 relaySession.onclose = function (sessionId) {
8130 // Remove the relay session
8131 delete webRelaySessions[sessionId];
8132 // If there are not more relay sessions, clear the cleanup timer
8133 if ((Object.keys(webRelaySessions).length == 0) && (obj.cleanupTimer != null)) { clearInterval(webRelayCleanupTimer); obj.cleanupTimer = null; }
8134 }
8135
8136 // Set the multi-tunnel session
8137 webRelaySessions[xrelaySessionId] = relaySession;
8138
8139 // Setup the cleanup timer if needed
8140 if (obj.cleanupTimer == null) { webRelayCleanupTimer = setInterval(checkWebRelaySessionsTimeout, 10000); }
8141
8142 // Redirect to root.
8143 res.redirect('/');
8144 } else {
8145 if (req.query.noredirect != null) {
8146 // No redirects allowed, fail here. This is important to make sure there is no redirect cascades
8147 res.sendStatus(404);
8148 } else {
8149 // Request was made to a different host, redirect using the full URL so an HTTP cookie can be created on the other DNS name.
8150 const httpport = ((args.aliasport != null) ? args.aliasport : args.port);
8151 res.redirect('https://' + selectedHost + ((httpport != 443) ? (':' + httpport) : '') + req.url + '&noredirect=1');
8152 }
8153 }
8154 });
8155 });
8156
8157 // Handle all incoming requests as web relays
8158 obj.webRelayRouter.get('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
8159
8160 // Handle all incoming requests as web relays
8161 obj.webRelayRouter.post('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
8162
8163 // Handle all incoming requests as web relays
8164 obj.webRelayRouter.put('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
8165
8166 // Handle all incoming requests as web relays
8167 obj.webRelayRouter.delete('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
8168
8169 // Handle all incoming requests as web relays
8170 obj.webRelayRouter.options('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
8171
8172 // Handle all incoming requests as web relays
8173 obj.webRelayRouter.head('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
8174 }
8175
8176 // Theme Pack Override Middleware
8177 obj.app.use(url, function (req, res, next) {
8178 if (req.method !== 'GET') return next();
8179 var domain = getDomain(req);
8180 // Serve theme pack files if domain has a theme pack configured
8181 if (domain && domain.themepack) {
8182 var themeFilePath = obj.path.join(obj.parent.datapath, 'theme-pack', domain.themepack, 'public', req.path);
8183 // Prevent directory traversal
8184 if (themeFilePath.indexOf('..') >= 0) return next();
8185
8186 obj.fs.stat(themeFilePath, function (err, stats) {
8187 if (err || !stats.isFile()) return next();
8188 res.sendFile(themeFilePath);
8189 });
8190 } else {
8191 next();
8192 }
8193 });
8194
8195 // Indicates to ExpressJS that the override public folder should be used to serve static files.
8196 obj.app.use(url, function(req, res, next){
8197 var domain = getDomain(req);
8198 if (domain.webpublicpath != null) { // Use domain public path
8199 obj.express.static(domain.webpublicpath)(req, res, next);
8200 } else if (obj.parent.webPublicOverridePath != null) { // Use override path
8201 obj.express.static(obj.parent.webPublicOverridePath)(req, res, next);
8202 } else { // carry on and use default public path
8203 next();
8204 }
8205 });
8206 // Indicates to ExpressJS that the default public folder should be used to serve static files.
8207 obj.app.use(url, obj.express.static(obj.parent.webPublicPath));
8208
8209 // Start regular disconnection list flush every 2 minutes.
8210 obj.wsagentsDisconnectionsTimer = setInterval(function () { obj.wsagentsDisconnections = {}; }, 120000);
8211 }
8212 }
8213 function finalizeWebserver() {
8214 // Setup all HTTP handlers
8215 setupHTTPHandlers()
8216
8217 // Handle 404 error
8218 if (obj.args.nice404 !== false) {
8219 obj.app.use(function (req, res, next) {
8220 parent.debug('web', '404 Error ' + req.url);
8221 var domain = getDomain(req);
8222 if ((domain == null) || (domain.auth == 'sspi')) { res.sendStatus(404); return; }
8223 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL
8224 const cspNonce = obj.crypto.randomBytes(15).toString('base64');
8225 res.set({ 'Content-Security-Policy': "default-src 'none'; script-src 'self' 'nonce-" + cspNonce + "'; img-src 'self'; style-src 'self' 'nonce-" + cspNonce + "';" }); // This page supports very tight CSP policy
8226 res.status(404).render(getRenderPage((domain.sitestyle >= 2) ? 'error4042' : 'error404', req, domain), getRenderArgs({ cspNonce: cspNonce }, req, domain));
8227 });
8228 }
8229
8230 // Start server on a free port.
8231 CheckListenPort(obj.args.port, obj.args.portbind, StartWebServer);
8232
8233 // Start on a second agent-only alternative port if needed.
8234 if (obj.args.agentport) { CheckListenPort(obj.args.agentport, obj.args.agentportbind, StartAltWebServer); }
8235
8236 // We are done starting the web server.
8237 if (doneFunc) doneFunc();
8238 }
8239 }
8240
8241 function nice404(req, res) {
8242 parent.debug('web', '404 Error ' + req.url);
8243 var domain = getDomain(req);
8244 if ((domain == null) || (domain.auth == 'sspi')) { res.sendStatus(404); return; }
8245 if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL
8246 if (obj.args.nice404 == false) { res.sendStatus(404); return; }
8247 const cspNonce = obj.crypto.randomBytes(15).toString('base64');
8248 res.set({ 'Content-Security-Policy': "default-src 'none'; script-src 'self' 'nonce-" + cspNonce + "'; img-src 'self'; style-src 'self' 'nonce-" + cspNonce + "';" }); // This page supports very tight CSP policy
8249 res.status(404).render(getRenderPage((domain.sitestyle >= 2) ? 'error4042' : 'error404', req, domain), getRenderArgs({ cspNonce: cspNonce }, req, domain));
8250 }
8251
8252 // Auth strategy flags
8253 const domainAuthStrategyConsts = {
8254 twitter: 1,
8255 google: 2,
8256 github: 4,
8257 reddit: 8, // Deprecated
8258 azure: 16,
8259 oidc: 32,
8260 saml: 64,
8261 intelSaml: 128,
8262 jumpCloudSaml: 256
8263 }
8264
8265 // Setup auth strategies for a domain
8266 async function setupDomainAuthStrategy(domain) {
8267 // Return binary flags representing all auth strategies that have been setup
8268 let authStrategyFlags = 0;
8269
8270 // Setup auth strategies using passport if needed
8271 if (typeof domain.authstrategies != 'object') return authStrategyFlags;
8272
8273 const url = domain.url
8274 const passport = domain.passport = require('passport');
8275 passport.serializeUser(function (user, done) { done(null, user.sid); });
8276 passport.deserializeUser(function (sid, done) { done(null, { sid: sid }); });
8277 obj.app.use(passport.initialize());
8278 obj.app.use(require('connect-flash')());
8279
8280 // Twitter
8281 if ((typeof domain.authstrategies.twitter == 'object') && (typeof domain.authstrategies.twitter.clientid == 'string') && (typeof domain.authstrategies.twitter.clientsecret == 'string')) {
8282 const TwitterStrategy = require('passport-twitter');
8283 let options = { consumerKey: domain.authstrategies.twitter.clientid, consumerSecret: domain.authstrategies.twitter.clientsecret };
8284 if (typeof domain.authstrategies.twitter.callbackurl == 'string') { options.callbackURL = domain.authstrategies.twitter.callbackurl; } else { options.callbackURL = url + 'auth-twitter-callback'; }
8285 parent.authLog('setupDomainAuthStrategy', 'Adding Twitter SSO with options: ' + JSON.stringify(options));
8286 passport.use('twitter-' + domain.id, new TwitterStrategy(options,
8287 function (token, tokenSecret, profile, cb) {
8288 parent.authLog('setupDomainAuthStrategy', 'Twitter profile: ' + JSON.stringify(profile));
8289 var user = { sid: '~twitter:' + profile.id, name: profile.displayName, strategy: 'twitter' };
8290 if ((typeof profile.emails == 'object') && (profile.emails[0] != null) && (typeof profile.emails[0].value == 'string')) { user.email = profile.emails[0].value; }
8291 return cb(null, user);
8292 }
8293 ));
8294 authStrategyFlags |= domainAuthStrategyConsts.twitter;
8295 }
8296
8297 // Google
8298 if ((typeof domain.authstrategies.google == 'object') && (typeof domain.authstrategies.google.clientid == 'string') && (typeof domain.authstrategies.google.clientsecret == 'string')) {
8299 const GoogleStrategy = require('passport-google-oauth20');
8300 let options = { clientID: domain.authstrategies.google.clientid, clientSecret: domain.authstrategies.google.clientsecret };
8301 if (typeof domain.authstrategies.google.callbackurl == 'string') { options.callbackURL = domain.authstrategies.google.callbackurl; } else { options.callbackURL = url + 'auth-google-callback'; }
8302 parent.authLog('setupDomainAuthStrategy', 'Adding Google SSO with options: ' + JSON.stringify(options));
8303 passport.use('google-' + domain.id, new GoogleStrategy(options,
8304 function (token, tokenSecret, profile, cb) {
8305 parent.authLog('setupDomainAuthStrategy', 'Google profile: ' + JSON.stringify(profile));
8306 var user = { sid: '~google:' + profile.id, name: profile.displayName, strategy: 'google' };
8307 if ((typeof profile.emails == 'object') && (profile.emails[0] != null) && (typeof profile.emails[0].value == 'string') && (profile.emails[0].verified == true)) { user.email = profile.emails[0].value; }
8308 return cb(null, user);
8309 }
8310 ));
8311 authStrategyFlags |= domainAuthStrategyConsts.google;
8312 }
8313
8314 // Github
8315 if ((typeof domain.authstrategies.github == 'object') && (typeof domain.authstrategies.github.clientid == 'string') && (typeof domain.authstrategies.github.clientsecret == 'string')) {
8316 const GitHubStrategy = require('passport-github2');
8317 let options = { clientID: domain.authstrategies.github.clientid, clientSecret: domain.authstrategies.github.clientsecret };
8318 if (typeof domain.authstrategies.github.callbackurl == 'string') { options.callbackURL = domain.authstrategies.github.callbackurl; } else { options.callbackURL = url + 'auth-github-callback'; }
8319 //override passport-github2 defaults that point to github.com with urls specified by user
8320 if (typeof domain.authstrategies.github.authorizationurl == 'string') { options.authorizationURL = domain.authstrategies.github.authorizationurl; }
8321 if (typeof domain.authstrategies.github.tokenurl == 'string') { options.tokenURL = domain.authstrategies.github.tokenurl; }
8322 if (typeof domain.authstrategies.github.userprofileurl == 'string') { options.userProfileURL = domain.authstrategies.github.userprofileurl; }
8323 if (typeof domain.authstrategies.github.useremailurl == 'string') { options.userEmailURL = domain.authstrategies.github.useremailurl; }
8324 parent.authLog('setupDomainAuthStrategy', 'Adding Github SSO with options: ' + JSON.stringify(options));
8325 passport.use('github-' + domain.id, new GitHubStrategy(options,
8326 function (token, tokenSecret, profile, cb) {
8327 parent.authLog('setupDomainAuthStrategy', 'Github profile: ' + JSON.stringify(profile));
8328 var user = { sid: '~github:' + profile.id, name: profile.displayName, strategy: 'github' };
8329 if ((typeof profile.emails == 'object') && (profile.emails[0] != null) && (typeof profile.emails[0].value == 'string')) { user.email = profile.emails[0].value; }
8330 return cb(null, user);
8331 }
8332 ));
8333 authStrategyFlags |= domainAuthStrategyConsts.github;
8334 }
8335
8336 // Azure
8337 if ((typeof domain.authstrategies.azure == 'object') && (typeof domain.authstrategies.azure.clientid == 'string') && (typeof domain.authstrategies.azure.clientsecret == 'string')) {
8338 const AzureOAuth2Strategy = require('passport-azure-oauth2');
8339 let options = { clientID: domain.authstrategies.azure.clientid, clientSecret: domain.authstrategies.azure.clientsecret, tenant: domain.authstrategies.azure.tenantid };
8340 if (typeof domain.authstrategies.azure.callbackurl == 'string') { options.callbackURL = domain.authstrategies.azure.callbackurl; } else { options.callbackURL = url + 'auth-azure-callback'; }
8341 parent.authLog('setupDomainAuthStrategy', 'Adding Azure SSO with options: ' + JSON.stringify(options));
8342 passport.use('azure-' + domain.id, new AzureOAuth2Strategy(options,
8343 function (accessToken, refreshtoken, params, profile, done) {
8344 var userex = null;
8345 try { userex = require('jwt-simple').decode(params.id_token, '', true); } catch (ex) { }
8346 parent.authLog('setupDomainAuthStrategy', 'Azure profile: ' + JSON.stringify(userex));
8347 var user = null;
8348 if (userex != null) {
8349 var user = { sid: '~azure:' + userex.unique_name.toLowerCase(), name: userex.name, strategy: 'azure' };
8350 if (typeof userex.email == 'string') { user.email = userex.email.toLowerCase(); }
8351 }
8352 return done(null, user);
8353 }
8354 ));
8355 authStrategyFlags |= domainAuthStrategyConsts.azure;
8356 }
8357
8358 // Generic SAML
8359 if (typeof domain.authstrategies.saml == 'object') {
8360 if ((typeof domain.authstrategies.saml.cert != 'string') || (typeof domain.authstrategies.saml.idpurl != 'string')) {
8361 parent.debug('error', 'Missing SAML configuration.');
8362 } else {
8363 const certPath = obj.common.joinPath(obj.parent.datapath, domain.authstrategies.saml.cert);
8364 var cert = obj.fs.readFileSync(certPath);
8365 if (cert == null) {
8366 parent.debug('error', 'Unable to read SAML IdP certificate: ' + domain.authstrategies.saml.cert);
8367 } else {
8368 var options = { entryPoint: domain.authstrategies.saml.idpurl, issuer: 'meshcentral' };
8369 if (typeof domain.authstrategies.saml.callbackurl == 'string') { options.callbackUrl = domain.authstrategies.saml.callbackurl; } else { options.callbackUrl = url + 'auth-saml-callback'; }
8370 if (domain.authstrategies.saml.disablerequestedauthncontext != null) { options.disableRequestedAuthnContext = domain.authstrategies.saml.disablerequestedauthncontext; }
8371 if (typeof domain.authstrategies.saml.entityid == 'string') { options.issuer = domain.authstrategies.saml.entityid; }
8372 parent.authLog('setupDomainAuthStrategy', 'Adding SAML SSO with options: ' + JSON.stringify(options));
8373 options.cert = cert.toString().split('-----BEGIN CERTIFICATE-----').join('').split('-----END CERTIFICATE-----').join('');
8374 const SamlStrategy = require('passport-saml').Strategy;
8375 passport.use('saml-' + domain.id, new SamlStrategy(options,
8376 function (profile, done) {
8377 parent.authLog('setupDomainAuthStrategy', 'SAML profile: ' + JSON.stringify(profile));
8378 if (typeof profile.nameID != 'string') { return done(); }
8379 var user = { sid: '~saml:' + profile.nameID, name: profile.nameID, strategy: 'saml' };
8380 if (typeof profile.displayname == 'string') {
8381 user.name = profile.displayname;
8382 } else if ((typeof profile.firstname == 'string') && (typeof profile.lastname == 'string')) {
8383 user.name = profile.firstname + ' ' + profile.lastname;
8384 }
8385 if (typeof profile.email == 'string') { user.email = profile.email; }
8386 return done(null, user);
8387 }
8388 ));
8389 authStrategyFlags |= domainAuthStrategyConsts.saml
8390 }
8391 }
8392 }
8393
8394 // Intel SAML
8395 if (typeof domain.authstrategies.intel == 'object') {
8396 if ((typeof domain.authstrategies.intel.cert != 'string') || (typeof domain.authstrategies.intel.idpurl != 'string')) {
8397 parent.debug('error', 'Missing Intel SAML configuration.');
8398 } else {
8399 var cert = obj.fs.readFileSync(obj.common.joinPath(obj.parent.datapath, domain.authstrategies.intel.cert));
8400 if (cert == null) {
8401 parent.debug('error', 'Unable to read Intel SAML IdP certificate: ' + domain.authstrategies.intel.cert);
8402 } else {
8403 var options = { entryPoint: domain.authstrategies.intel.idpurl, issuer: 'meshcentral' };
8404 if (typeof domain.authstrategies.intel.callbackurl == 'string') { options.callbackUrl = domain.authstrategies.intel.callbackurl; } else { options.callbackUrl = url + 'auth-intel-callback'; }
8405 if (domain.authstrategies.intel.disablerequestedauthncontext != null) { options.disableRequestedAuthnContext = domain.authstrategies.intel.disablerequestedauthncontext; }
8406 if (typeof domain.authstrategies.intel.entityid == 'string') { options.issuer = domain.authstrategies.intel.entityid; }
8407 parent.authLog('setupDomainAuthStrategy', 'Adding Intel SSO with options: ' + JSON.stringify(options));
8408 options.cert = cert.toString().split('-----BEGIN CERTIFICATE-----').join('').split('-----END CERTIFICATE-----').join('');
8409 const SamlStrategy = require('passport-saml').Strategy;
8410 passport.use('isaml-' + domain.id, new SamlStrategy(options,
8411 function (profile, done) {
8412 parent.authLog('setupDomainAuthStrategy', 'Intel profile: ' + JSON.stringify(profile));
8413 if (typeof profile.nameID != 'string') { return done(); }
8414 var user = { sid: '~intel:' + profile.nameID, name: profile.nameID, strategy: 'intel' };
8415 if ((typeof profile.firstname == 'string') && (typeof profile.lastname == 'string')) { user.name = profile.firstname + ' ' + profile.lastname; }
8416 else if ((typeof profile.FirstName == 'string') && (typeof profile.LastName == 'string')) { user.name = profile.FirstName + ' ' + profile.LastName; }
8417 if (typeof profile.email == 'string') { user.email = profile.email; }
8418 else if (typeof profile.EmailAddress == 'string') { user.email = profile.EmailAddress; }
8419 return done(null, user);
8420 }
8421 ));
8422 authStrategyFlags |= domainAuthStrategyConsts.intelSaml
8423 }
8424 }
8425 }
8426
8427 // JumpCloud SAML
8428 if (typeof domain.authstrategies.jumpcloud == 'object') {
8429 if ((typeof domain.authstrategies.jumpcloud.cert != 'string') || (typeof domain.authstrategies.jumpcloud.idpurl != 'string')) {
8430 parent.debug('error', 'Missing JumpCloud SAML configuration.');
8431 } else {
8432 var cert = obj.fs.readFileSync(obj.common.joinPath(obj.parent.datapath, domain.authstrategies.jumpcloud.cert));
8433 if (cert == null) {
8434 parent.debug('error', 'Unable to read JumpCloud IdP certificate: ' + domain.authstrategies.jumpcloud.cert);
8435 } else {
8436 var options = { entryPoint: domain.authstrategies.jumpcloud.idpurl, issuer: 'meshcentral' };
8437 if (typeof domain.authstrategies.jumpcloud.callbackurl == 'string') { options.callbackUrl = domain.authstrategies.jumpcloud.callbackurl; } else { options.callbackUrl = url + 'auth-jumpcloud-callback'; }
8438 if (typeof domain.authstrategies.jumpcloud.entityid == 'string') { options.issuer = domain.authstrategies.jumpcloud.entityid; }
8439 parent.authLog('setupDomainAuthStrategy', 'Adding JumpCloud SSO with options: ' + JSON.stringify(options));
8440 options.cert = cert.toString().split('-----BEGIN CERTIFICATE-----').join('').split('-----END CERTIFICATE-----').join('');
8441 const SamlStrategy = require('passport-saml').Strategy;
8442 passport.use('jumpcloud-' + domain.id, new SamlStrategy(options,
8443 function (profile, done) {
8444 parent.authLog('setupDomainAuthStrategy', 'JumpCloud profile: ' + JSON.stringify(profile));
8445 if (typeof profile.nameID != 'string') { return done(); }
8446 var user = { sid: '~jumpcloud:' + profile.nameID, name: profile.nameID, strategy: 'jumpcloud' };
8447 if ((typeof profile.firstname == 'string') && (typeof profile.lastname == 'string')) { user.name = profile.firstname + ' ' + profile.lastname; }
8448 if (typeof profile.email == 'string') { user.email = profile.email; }
8449 return done(null, user);
8450 }
8451 ));
8452 authStrategyFlags |= domainAuthStrategyConsts.jumpCloudSaml
8453 }
8454 }
8455 }
8456
8457 // Setup OpenID Connect Authentication Strategy
8458 if (obj.common.validateObject(domain.authstrategies.oidc)) {
8459 parent.authLog('setupDomainAuthStrategy', `OIDC: Setting up strategy for domain: ${domain.id == null ? 'default' : domain.id}`);
8460 // Ensure required objects exist
8461 let initStrategy = domain.authstrategies.oidc
8462 if (typeof initStrategy.issuer == 'string') { initStrategy.issuer = { 'issuer': initStrategy.issuer } }
8463 let strategy = migrateOldConfigs(Object.assign({ 'client': {}, 'issuer': {}, 'options': {}, 'custom': {}, 'obj': { 'openidClient': require('openid-client') } }, initStrategy))
8464 let preset = obj.common.validateString(strategy.custom.preset) ? strategy.custom.preset : null
8465 if (!preset) {
8466 if (typeof strategy.custom.tenant_id == 'string') { strategy.custom.preset = preset = 'azure' }
8467 if (strategy.custom.customer_id || strategy.custom.identitysource || strategy.client.client_id.split('.')[2] == 'googleusercontent') { strategy.custom.preset = preset = 'google' }
8468 }
8469
8470 // Check issuer url
8471 let presetIssuer
8472 if (preset == 'azure') { presetIssuer = 'https://login.microsoftonline.com/' + strategy.custom.tenant_id + '/v2.0'; }
8473 if (preset == 'google') { presetIssuer = 'https://accounts.google.com'; }
8474 if (!obj.common.validateString(strategy.issuer.issuer)) {
8475 if (!preset) {
8476 let error = new Error('OIDC: Missing issuer URI.');
8477 parent.authLog('error', `${error.message} STRATEGY: ${JSON.stringify(strategy)}`);
8478 throw error;
8479 } else {
8480 strategy.issuer.issuer = presetIssuer
8481 parent.authLog('setupDomainAuthStrategy', `OIDC: PRESET: ${preset.toUpperCase()}: Using preset issuer: ${presetIssuer}`);
8482 }
8483 } else if ((typeof strategy.issuer.issuer == 'string') && (typeof strategy.custom.preset == 'string')) {
8484 let error = new Error(`OIDC: PRESET: ${strategy.custom.preset.toUpperCase()}: PRESET OVERRIDDEN: CONFIG ISSUER: ${strategy.issuer.issuer} PRESET ISSUER: ${presetIssuer}`);
8485 parent.authLog('setupDomainAuthStrategy', error.message);
8486 console.warn(error)
8487 }
8488
8489 // Setup Strategy Options
8490 strategy.custom.scope = obj.common.convertStrArray(strategy.custom.scope, ' ')
8491 if (strategy.custom.scope.length > 0) {
8492 strategy.options.params = Object.assign(strategy.options.params || {}, { 'scope': strategy.custom.scope });
8493 } else {
8494 strategy.options.params = Object.assign(strategy.options.params || {}, { 'scope': ['openid', 'profile', 'email'] });
8495 }
8496 if (typeof strategy.groups == 'object') {
8497 let groupScope = strategy.groups.scope || null
8498 if (groupScope == null) {
8499 if (preset == 'azure') { groupScope = 'Group.Read.All' }
8500 if (preset == 'google') { groupScope = 'https://www.googleapis.com/auth/cloud-identity.groups.readonly' }
8501 if (typeof preset != 'string') { groupScope = 'groups' }
8502 }
8503 strategy.options.params.scope.push(groupScope)
8504 }
8505 strategy.options.params.scope = strategy.options.params.scope.join(' ')
8506
8507 if (obj.httpsProxyAgent) {
8508 // process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // add using environment variables if needs be not here
8509 strategy.obj.openidClient.custom.setHttpOptionsDefaults({ agent: obj.httpsProxyAgent });
8510 }
8511 // Discover additional information if available, use endpoints from config if present
8512 let issuer
8513 try {
8514 parent.authLog('setupDomainAuthStrategy', `OIDC: Discovering Issuer Endpoints: ${strategy.issuer.issuer}`);
8515 issuer = await strategy.obj.openidClient.Issuer.discover(strategy.issuer.issuer);
8516 } catch (err) {
8517 let error = new Error('OIDC: Discovery failed.', { cause: err });
8518 parent.authLog('setupDomainAuthStrategy', `ERROR: ${JSON.stringify(error)} ISSUER_URI: ${strategy.issuer.issuer}`);
8519 throw error
8520 }
8521 if (Object.keys(strategy.issuer).length > 1) {
8522 parent.authLog('setupDomainAuthStrategy', `OIDC: Adding Issuer Metadata: ${JSON.stringify(strategy.issuer)}`);
8523 issuer = new strategy.obj.openidClient.Issuer(Object.assign(issuer?.metadata, strategy.issuer));
8524 }
8525 strategy.issuer = issuer?.metadata;
8526 strategy.obj.issuer = issuer;
8527
8528 var httpport = ((args.aliasport != null) ? args.aliasport : args.port);
8529 var origin = 'https://' + (domain.dns ? domain.dns : parent.certificates.CommonName);
8530 if (httpport != 443) { origin += ':' + httpport; }
8531
8532 // Make sure redirect_uri and post_logout_redirect_uri exist before continuing
8533 if (!strategy.client.redirect_uri) {
8534 strategy.client.redirect_uri = origin + url + 'auth-oidc-callback';
8535 }
8536 if (!strategy.client.post_logout_redirect_uri && strategy.client.post_logout_redirect_uri !== false) {
8537 strategy.client.post_logout_redirect_uri = origin + url + 'login';
8538 }
8539
8540 // Create client and overwrite in options
8541 let client = new issuer.Client(strategy.client)
8542 strategy.options = Object.assign(strategy.options, { 'client': client, sessionKey: 'oidc-' + domain.id });
8543 strategy.client = client.metadata
8544 strategy.obj.client = client
8545
8546 // Setup strategy and save configs for later
8547 passport.use('oidc-' + domain.id, new strategy.obj.openidClient.Strategy(strategy.options, oidcCallback));
8548 parent.config.domains[domain.id].authstrategies.oidc = strategy;
8549 parent.debug('verbose', 'OIDC: Saved Configuration: ' + JSON.stringify(strategy));
8550 if (preset) { parent.authLog('setupDomainAuthStrategy', 'OIDC: ' + preset.toUpperCase() + ': Setup Complete'); }
8551 else { parent.authLog('setupDomainAuthStrategy', 'OIDC: Setup Complete'); }
8552
8553 authStrategyFlags |= domainAuthStrategyConsts.oidc
8554
8555 function migrateOldConfigs(strategy) {
8556 let oldConfigs = {
8557 'client': {
8558 'clientid': 'client_id',
8559 'clientsecret': 'client_secret',
8560 'callbackurl': 'redirect_uri'
8561 },
8562 'issuer': {
8563 'authorizationurl': 'authorization_endpoint',
8564 'tokenurl': 'token_endpoint',
8565 'userinfourl': 'userinfo_endpoint'
8566 },
8567 'custom': {
8568 'tenantid': 'tenant_id',
8569 'customerid': 'customer_id'
8570 }
8571 }
8572 for (var type in oldConfigs) {
8573 for (const [key, value] of Object.entries(oldConfigs[type])) {
8574 if (Object.hasOwn(strategy, key)) {
8575 if (strategy[type][value] && obj.common.validateString(strategy[type][value])) {
8576 let error = new Error('OIDC: OLD CONFIG: Config conflict, new config overrides old config');
8577 parent.authLog('migrateOldConfigs', `${JSON.stringify(error)} OLD CONFIG: ${key}: ${strategy[key]} NEW CONFIG: ${value}:${strategy[type][value]}`);
8578 } else {
8579 parent.authLog('migrateOldConfigs', `OIDC: OLD CONFIG: Moving old config to new location. strategy.${key} => strategy.${type}.${value}`);
8580 strategy[type][value] = strategy[key];
8581 }
8582 delete strategy[key]
8583 }
8584 }
8585 }
8586 if (typeof strategy.scope == 'string') {
8587 if (!strategy.custom.scope) {
8588 strategy.custom.scope = strategy.scope;
8589 strategy.options.params = { 'scope': strategy.scope };
8590 parent.authLog('migrateOldConfigs', `OIDC: OLD CONFIG: Moving old config to new location. strategy.scope => strategy.custom.scope`);
8591 } else {
8592 let error = new Error('OIDC: OLD CONFIG: Config conflict, using new config values.');
8593 parent.authLog('migrateOldConfigs', `${error.message} OLD CONFIG: strategy.scope: ${strategy.scope} NEW CONFIG: strategy.custom.scope:${strategy.custom.scope}`);
8594 parent.debug('warning', error.message)
8595 }
8596 delete strategy.scope
8597 }
8598 if (strategy.groups && strategy.groups.sync && strategy.groups.sync.enabled && strategy.groups.sync.enabled === true) {
8599 if (strategy.groups.sync.filter) {
8600 delete strategy.groups.sync.enabled;
8601 } else {
8602 strategy.groups.sync = true;
8603 }
8604 parent.authLog('migrateOldConfigs', `OIDC: OLD CONFIG: Moving old config to new location. strategy.groups.sync.enabled => strategy.groups.sync`);
8605 }
8606 return strategy
8607 }
8608
8609 // Callback function must be able to grab info from API's using the access token, would prefer to use the token here.
8610 function oidcCallback(tokenset, profile, done) {
8611 // Handle case where done might not be the third parameter
8612 if (typeof done !== 'function') {
8613 // OpenID Connect strategy calls with (tokenset, done) instead of (tokenset, profile, done)
8614 if (typeof profile === 'function') {
8615 done = profile;
8616 profile = null;
8617 } else {
8618 parent.debug('error', 'OIDC: Unable to find callback function in parameters');
8619 return;
8620 }
8621 }
8622
8623 // If profile is null/undefined, extract user info from the tokenset
8624 if (!profile && tokenset && tokenset.id_token) {
8625 try {
8626 // Simple JWT decoder to extract user claims from id_token
8627 const parts = tokenset.id_token.split('.');
8628 if (parts.length === 3) {
8629 const payload = parts[1];
8630 const paddedPayload = payload + '='.repeat((4 - payload.length % 4) % 4);
8631 const decoded = JSON.parse(Buffer.from(paddedPayload, 'base64').toString());
8632 if (decoded) {
8633 profile = decoded;
8634 }
8635 }
8636 } catch (err) {
8637 parent.debug('error', `OIDC: Failed to decode id_token: ${err.message}`);
8638 }
8639 }
8640
8641 // Initialize user object
8642 let user = { 'strategy': 'oidc' }
8643 let claims = obj.common.validateObject(strategy.custom.claims) ? strategy.custom.claims : null;
8644
8645 user.sid = null;
8646 if (profile && obj.common.validateString(profile.sub)) {
8647 user.sid = '~oidc:' + profile.sub;
8648 } else if (profile && obj.common.validateString(profile.oid)) {
8649 user.sid = '~oidc:' + profile.oid;
8650 } else if (profile && obj.common.validateString(profile.email)) {
8651 user.sid = '~oidc:' + profile.email;
8652 } else if (profile && obj.common.validateString(profile.upn)) {
8653 user.sid = '~oidc:' + profile.upn;
8654 }
8655
8656 user.name = profile && obj.common.validateString(profile.name) ? profile.name : null;
8657 user.email = profile && obj.common.validateString(profile.email) ? profile.email : null;
8658 if (claims != null) {
8659 user.sid = obj.common.validateString(profile[claims.uuid]) ? '~oidc:' + profile[claims.uuid] : user.sid;
8660 user.name = obj.common.validateString(profile[claims.name]) ? profile[claims.name] : user.name;
8661 user.email = obj.common.validateString(profile[claims.email]) ? profile[claims.email] : user.email;
8662 }
8663
8664 // Ensure we have a valid sid before proceeding
8665 if (!user.sid) {
8666 parent.debug('error', `OIDC: No valid user identifier found in profile`);
8667 return done(new Error('OIDC: No valid user identifier found in profile'));
8668 }
8669
8670 user.emailVerified = profile && profile.email_verified ? profile.email_verified : obj.common.validateEmail(user.email);
8671 user.groups = profile && obj.common.validateStrArray(profile.groups, 1) ? profile.groups : null;
8672 user.preset = obj.common.validateString(strategy.custom.preset) ? strategy.custom.preset : null;
8673 if (strategy.groups && obj.common.validateString(strategy.groups.claim)) {
8674 user.groups = profile && obj.common.validateStrArray(profile[strategy.groups.claim], 1) ? profile[strategy.groups.claim] : null
8675 }
8676
8677 // Setup end session endpoint
8678 try {
8679 strategy.issuer.end_session_endpoint = strategy.obj.client.endSessionUrl({ 'id_token_hint': tokenset })
8680 parent.authLog('oidcCallback', `OIDC: Discovered end_session_endpoint: ${strategy.issuer.end_session_endpoint}`);
8681 } catch (err) {
8682 let error = new Error('OIDC: Discovering end_session_endpoint failed. Using Default.', { cause: err });
8683 strategy.issuer.end_session_endpoint = strategy.issuer.issuer + '/logout';
8684 parent.debug('error', `${error.message} end_session_endpoint: ${strategy.issuer.end_session_endpoint} post_logout_redirect_uri: ${strategy.client.post_logout_redirect_uri} TOKENSET: ${JSON.stringify(tokenset)}`);
8685 parent.authLog('oidcCallback', error.message);
8686 }
8687
8688 // Setup presets and groups, get groups from API if needed then return
8689 if (strategy.groups && typeof user.preset == 'string') {
8690 getGroups(user.preset, tokenset).then((groups) => {
8691 user = Object.assign(user, { 'groups': groups });
8692 done(null, user);
8693 }).catch((err) => {
8694 let error = new Error('OIDC: GROUPS: No groups found due to error:', { cause: err });
8695 parent.debug('error', `${JSON.stringify(error)}`);
8696 parent.authLog('oidcCallback', error.message);
8697 user.groups = [];
8698 done(null, user);
8699 });
8700 } else {
8701 done(null, user);
8702 }
8703
8704 async function getGroups(preset, tokenset) {
8705 let url = '';
8706 if (preset == 'azure') { url = strategy.groups.recursive == true ? 'https://graph.microsoft.com/v1.0/me/transitiveMemberOf?$top=999' : 'https://graph.microsoft.com/v1.0/me/memberOf?$top=999'; }
8707 if (preset == 'google') { url = strategy.custom.customer_id ? 'https://cloudidentity.googleapis.com/v1/groups?parent=customers/' + strategy.custom.customer_id : strategy.custom.identitysource ? 'https://cloudidentity.googleapis.com/v1/groups?parent=identitysources/' + strategy.custom.identitysource : null; }
8708 return new Promise((resolve, reject) => {
8709 const options = {
8710 'headers': { authorization: 'Bearer ' + tokenset.access_token }
8711 }
8712 if (obj.httpsProxyAgent) { options.agent = obj.httpsProxyAgent; }
8713 const req = require('https').get(url, options, (res) => {
8714 let data = []
8715 res.on('data', (chunk) => {
8716 data.push(chunk);
8717 });
8718 res.on('end', () => {
8719 if (res.statusCode < 200 || res.statusCode >= 300) {
8720 let error = new Error('OIDC: GROUPS: Bad response code from API, statusCode: ' + res.statusCode);
8721 parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
8722 console.error(error);
8723 reject(error);
8724 }
8725 if (data.length == 0) {
8726 let error = new Error('OIDC: GROUPS: Getting groups from API failed, request returned no data in response.');
8727 parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
8728 console.error(error);
8729 reject(error);
8730 }
8731 try {
8732 if (Buffer.isBuffer(data[0])) {
8733 data = Buffer.concat(data);
8734 data = data.toString();
8735 } else { // else if (typeof data[0] == 'string')
8736 data = data.join();
8737 }
8738 } catch (err) {
8739 let error = new Error('OIDC: GROUPS: Getting groups from API failed. Error joining response data.', { cause: err });
8740 parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
8741 console.error(error);
8742 reject(error);
8743 }
8744 if (preset == 'azure') {
8745 data = JSON.parse(data);
8746 if (data.error) {
8747 let error = new Error('OIDC: GROUPS: Getting groups from API failed. Error joining response data.', { cause: data.error });
8748 parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
8749 console.error(error);
8750 reject(error);
8751 }
8752 data = data.value;
8753 }
8754 if (preset == 'google') {
8755 data = data.split('\n');
8756 data = data.join('');
8757 data = JSON.parse(data);
8758 data = data.groups;
8759 }
8760 let groups = []
8761 for (var i in data) {
8762 if (typeof data[i].displayName == 'string') {
8763 groups.push(data[i].displayName);
8764 }
8765 }
8766 if (groups.length == 0) {
8767 let warn = new Error('OIDC: GROUPS: No groups returned from API.');
8768 parent.authLog('getGroups', `WARN: ${warn.message} DATA: ${data}`);
8769 console.warn(warn);
8770 resolve(groups);
8771 } else {
8772 resolve(groups);
8773 }
8774 });
8775 });
8776 req.on('error', (err) => {
8777 let error = new Error('OIDC: GROUPS: Request error.', { cause: err });
8778 parent.authLog('getGroups', `ERROR: ${error.message} URL: ${url} OPTIONS: ${JSON.stringify(options)}`);
8779 console.error(error);
8780 reject(error);
8781 });
8782 req.end();
8783 });
8784 }
8785 }
8786 }
8787 return authStrategyFlags;
8788 }
8789
8790 // Handle an incoming request as a web relay
8791 function handleWebRelayRequest(req, res) {
8792 var webRelaySessionId = null;
8793 if ((req.session.userid != null) && (req.session.x != null)) { webRelaySessionId = req.session.userid + '/' + req.session.x; }
8794 else if (req.session.z != null) { webRelaySessionId = req.session.z; }
8795 if ((webRelaySessionId != null) && (obj.destroyedSessions[webRelaySessionId] == null)) {
8796 var relaySession = webRelaySessions[webRelaySessionId + '/' + req.hostname];
8797 if (relaySession != null) {
8798 // The web relay session is valid, use it
8799 relaySession.handleRequest(req, res);
8800 } else {
8801 // No web relay session with this relay identifier, close the HTTP request.
8802 res.sendStatus(404);
8803 }
8804 } else {
8805 // The user is not logged in or does not have a relay identifier, close the HTTP request.
8806 res.sendStatus(404);
8807 }
8808 }
8809
8810 // Handle an incoming websocket connection as a web relay
8811 function handleWebRelayWebSocket(ws, req) {
8812 var webRelaySessionId = null;
8813 if ((req.session.userid != null) && (req.session.x != null)) { webRelaySessionId = req.session.userid + '/' + req.session.x; }
8814 else if (req.session.z != null) { webRelaySessionId = req.session.z; }
8815 if ((webRelaySessionId != null) && (obj.destroyedSessions[webRelaySessionId] == null)) {
8816 var relaySession = webRelaySessions[webRelaySessionId + '/' + req.hostname];
8817 if (relaySession != null) {
8818 // The multi-tunnel session is valid, use it
8819 relaySession.handleWebSocket(ws, req);
8820 } else {
8821 // No multi-tunnel session with this relay identifier, close the websocket.
8822 ws.close();
8823 }
8824 } else {
8825 // The user is not logged in or does not have a relay identifier, close the websocket.
8826 ws.close();
8827 }
8828 }
8829
8830 // Perform server inner authentication
8831 // This is a type of server authentication where the client will open the socket regardless of the TLS certificate and request that the server
8832 // sign a client nonce with the server agent cert and return the response. Only after that will the client send the client authentication username
8833 // and password or authentication cookie.
8834 function PerformWSSessionInnerAuth(ws, req, domain, func) {
8835 // When data is received from the web socket
8836 ws.on('message', function (data) {
8837 var command;
8838 try { command = JSON.parse(data.toString('utf8')); } catch (e) { return; }
8839 if (obj.common.validateString(command.action, 3, 32) == false) return; // Action must be a string between 3 and 32 chars
8840
8841 switch (command.action) {
8842 case 'serverAuth': { // This command is used to perform server "inner" authentication.
8843 // Check the client nonce and TLS hash
8844 if ((obj.common.validateString(command.cnonce, 1, 256) == false) || (obj.common.validateString(command.tlshash, 1, 512) == false)) {
8845 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'badargs' })); } catch (ex) { }
8846 try { ws.close(); } catch (ex) { }
8847 break;
8848 }
8849
8850 // Check that the TLS hash is an acceptable one.
8851 var h = Buffer.from(command.tlshash, 'hex').toString('binary');
8852 if ((obj.webCertificateHashs[domain.id] != h) && (obj.webCertificateFullHashs[domain.id] != h) && (obj.defaultWebCertificateHash != h) && (obj.defaultWebCertificateFullHash != h)) {
8853 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'badtlscert' })); } catch (ex) { }
8854 try { ws.close(); } catch (ex) { }
8855 return;
8856 }
8857
8858 // TLS hash check is a success, sign the request.
8859 // Perform the hash signature using the server agent certificate
8860 var nonce = obj.crypto.randomBytes(48);
8861 var signData = Buffer.from(command.cnonce, 'base64').toString('binary') + h + nonce.toString('binary'); // Client Nonce + TLS Hash + Server Nonce
8862 parent.certificateOperations.acceleratorPerformSignature(0, signData, null, function (tag, signature) {
8863 // Send back our certificate + nonce + signature
8864 ws.send(JSON.stringify({ 'action': 'serverAuth', 'cert': Buffer.from(obj.agentCertificateAsn1, 'binary').toString('base64'), 'nonce': nonce.toString('base64'), 'signature': Buffer.from(signature, 'binary').toString('base64') }));
8865 });
8866 break;
8867 }
8868 case 'userAuth': { // This command is used to perform user authentication.
8869 // Check username and password authentication
8870 if ((typeof command.username == 'string') && (typeof command.password == 'string')) {
8871 obj.authenticate(Buffer.from(command.username, 'base64').toString(), Buffer.from(command.password, 'base64').toString(), domain, function (err, userid, passhint, loginOptions) {
8872 if ((err != null) || (userid == null)) {
8873 // Invalid authentication
8874 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2c' })); } catch (ex) { }
8875 try { ws.close(); } catch (ex) { }
8876 } else {
8877 var user = obj.users[userid];
8878 if ((err == null) && (user)) {
8879 // Check if a 2nd factor is needed
8880 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'))
8881
8882 // See if we support two-factor trusted cookies
8883 var twoFactorCookieDays = 30;
8884 if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
8885
8886 // Check if two factor can be skipped
8887 const twoFactorSkip = checkUserOneTimePasswordSkip(domain, user, req, loginOptions);
8888
8889 if ((twoFactorSkip == null) && (checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true)) {
8890 // Figure out if email 2FA is allowed
8891 var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
8892 var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
8893 var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
8894 //var push2fa = ((parent.firebase != null) && (user.otpdev != null));
8895 if ((typeof command.token != 'string') || (command.token == '**email**') || (command.token == '**sms**')/* || (command.token == '**push**')*/) {
8896 if ((command.token == '**email**') && (email2fa == true)) {
8897 // Cause a token to be sent to the user's registered email
8898 user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
8899 obj.db.SetUser(user);
8900 parent.debug('web', 'Sending 2FA email to: ' + user.email);
8901 domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key);
8902 // Ask for a login token & confirm email was sent
8903 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, email2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
8904 } else if ((command.token == '**sms**') && (sms2fa == true)) {
8905 // Cause a token to be sent to the user's phone number
8906 user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
8907 obj.db.SetUser(user);
8908 parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
8909 parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
8910 // Ask for a login token & confirm sms was sent
8911 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, sms2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
8912 } else if ((command.token == '**msg**') && (msg2fa == true)) {
8913 // Cause a token to be sent to the user's messenger account
8914 user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
8915 obj.db.SetUser(user);
8916 parent.debug('web', 'Sending 2FA message to: ' + user.phone);
8917 parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req));
8918 // Ask for a login token & confirm sms was sent
8919 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, msg2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
8920 /*
8921 } else if ((command.token == '**push**') && (push2fa == true)) {
8922 // Cause push notification to device
8923 const code = Buffer.from(obj.common.zeroPad(getRandomSixDigitInteger(), 6)).toString('base64');
8924 const authCookie = parent.encodeCookie({ a: 'checkAuth', c: code, u: user._id, n: user.otpdev });
8925 var payload = { notification: { title: "MeshCentral", body: user.name + " authentication" }, data: { url: '2fa://auth?code=' + code + '&c=' + authCookie } };
8926 var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute
8927 parent.firebase.sendToDevice(user.otpdev, payload, options, function (id, err, errdesc) {
8928 if (err == null) { parent.debug('email', 'Successfully auth check send push message to device'); } else { parent.debug('email', 'Failed auth check push message to device, error: ' + errdesc); }
8929 });
8930 */
8931 } else {
8932 // Ask for a login token
8933 parent.debug('web', 'Asking for login token');
8934 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (ex) { console.log(ex); }
8935 }
8936 } else {
8937 checkUserOneTimePassword(req, domain, user, command.token, null, function (result, authData) {
8938 if (result == false) {
8939 // Failed, ask for a login token again
8940 parent.debug('web', 'Invalid login token, asking again');
8941 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
8942 } else {
8943 // We are authenticated with 2nd factor.
8944 // Check email verification
8945 if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
8946 parent.debug('web', 'Invalid login, asking for email validation');
8947 try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, email2fasent: true })); ws.close(); } catch (e) { }
8948 } else {
8949 // We are authenticated
8950 ws._socket.pause();
8951 ws.removeAllListeners(['message', 'close', 'error']);
8952 func(ws, req, domain, user, authData);
8953 }
8954 }
8955 });
8956 }
8957 } else {
8958 // Check email verification
8959 if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
8960 parent.debug('web', 'Invalid login, asking for email validation');
8961 var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
8962 var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
8963 var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
8964 try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, email2fasent: true })); ws.close(); } catch (e) { }
8965 } else {
8966 // We are authenticated
8967 ws._socket.pause();
8968 ws.removeAllListeners(['message', 'close', 'error']);
8969 func(ws, req, domain, user, twoFactorSkip);
8970 }
8971 }
8972 }
8973 }
8974 });
8975 } else {
8976 // Invalid authentication
8977 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2c' })); } catch (ex) { }
8978 try { ws.close(); } catch (ex) { }
8979 }
8980 break;
8981 }
8982 }
8983
8984 });
8985
8986 // If error, do nothing
8987 ws.on('error', function (err) { try { ws.close(); } catch (e) { console.log(e); } });
8988
8989 // If the web socket is closed
8990 ws.on('close', function (req) { try { ws.close(); } catch (e) { console.log(e); } });
8991
8992 // Resume the socket to perform inner authentication
8993 try { ws._socket.resume(); } catch (ex) { }
8994 }
8995
8996 // Authenticates a session and forwards
8997 function PerformWSSessionAuth(ws, req, noAuthOk, func) {
8998 // Check if the session expired
8999 if ((req.session != null) && (typeof req.session.expire == 'number') && (req.session.expire <= Date.now())) {
9000 parent.debug('web', 'WSERROR: Session expired.'); try { ws.send(JSON.stringify({ action: 'close', cause: 'expired', msg: 'expired-1' })); ws.close(); } catch (e) { } return;
9001 }
9002
9003 // Check if this is a banned ip address
9004 if (obj.checkAllowLogin(req) == false) { parent.debug('web', 'WSERROR: Banned connection.'); try { ws.send(JSON.stringify({ action: 'close', cause: 'banned', msg: 'banned-1' })); ws.close(); } catch (e) { } return; }
9005 try {
9006 // Hold this websocket until we are ready.
9007 ws._socket.pause();
9008
9009 // Check IP filtering and domain
9010 var domain = null;
9011 if (noAuthOk == true) {
9012 domain = getDomain(req);
9013 if (domain == null) { parent.debug('web', 'WSERROR: Got no domain, no auth ok.'); try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-1' })); ws.close(); return; } catch (e) { } return; }
9014 } else {
9015 // If authentication is required, enforce IP address filtering.
9016 domain = checkUserIpAddress(ws, req);
9017 if (domain == null) { parent.debug('web', 'WSERROR: Got no domain, user auth required.'); return; }
9018 }
9019
9020 // Check if inner authentication is requested
9021 if (req.headers['x-meshauth'] === '*') { func(ws, req, domain, null); return; }
9022
9023 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'))
9024
9025 // A web socket session can be authenticated in many ways (Default user, session, user/pass and cookie). Check authentication here.
9026 if ((req.query.user != null) && (req.query.pass != null)) {
9027 // A user/pass is provided in URL arguments
9028 obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid, passhint, loginOptions) {
9029 var user = obj.users[userid];
9030
9031 // Check if user as the "notools" site right. If so, deny this connection as tools are not allowed to connect.
9032 if ((user != null) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & SITERIGHT_NOMESHCMD)) {
9033 // No tools allowed, close the websocket connection
9034 parent.debug('web', 'ERR: Websocket no tools allowed');
9035 try { ws.send(JSON.stringify({ action: 'close', cause: 'notools', msg: 'notools' })); ws.close(); } catch (e) { }
9036 return;
9037 }
9038
9039 // See if we support two-factor trusted cookies
9040 var twoFactorCookieDays = 30;
9041 if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
9042
9043 if ((err == null) && (user)) {
9044 // Check if a 2nd factor is needed
9045 if (checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true) {
9046 // Figure out if email 2FA is allowed
9047 var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
9048 var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
9049 var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
9050 //var push2fa = ((parent.firebase != null) && (user.otpdev != null));
9051 if ((typeof req.query.token != 'string') || (req.query.token == '**email**') || (req.query.token == '**sms**')/* || (req.query.token == '**push**')*/) {
9052 if ((req.query.token == '**email**') && (email2fa == true)) {
9053 // Cause a token to be sent to the user's registered email
9054 user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
9055 obj.db.SetUser(user);
9056 parent.debug('web', 'Sending 2FA email to: ' + user.email);
9057 domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key);
9058 // Ask for a login token & confirm email was sent
9059 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, email2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
9060 } else if ((req.query.token == '**sms**') && (sms2fa == true)) {
9061 // Cause a token to be sent to the user's phone number
9062 user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
9063 obj.db.SetUser(user);
9064 parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
9065 parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
9066 // Ask for a login token & confirm sms was sent
9067 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, sms2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
9068 } else if ((req.query.token == '**msg**') && (msg2fa == true)) {
9069 // Cause a token to be sent to the user's messenger account
9070 user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
9071 obj.db.SetUser(user);
9072 parent.debug('web', 'Sending 2FA message to: ' + user.msghandle);
9073 parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req));
9074 // Ask for a login token & confirm message was sent
9075 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, msg2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
9076 /*
9077 } else if ((command.token == '**push**') && (push2fa == true)) {
9078 // Cause push notification to device
9079 const code = Buffer.from(obj.common.zeroPad(getRandomSixDigitInteger(), 6)).toString('base64');
9080 const authCookie = parent.encodeCookie({ a: 'checkAuth', c: code, u: user._id, n: user.otpdev });
9081 var payload = { notification: { title: "MeshCentral", body: user.name + " authentication" }, data: { url: '2fa://auth?code=' + code + '&c=' + authCookie } };
9082 var options = { priority: 'High', timeToLive: 60 }; // TTL: 1 minute
9083 parent.firebase.sendToDevice(user.otpdev, payload, options, function (id, err, errdesc) {
9084 if (err == null) { parent.debug('email', 'Successfully auth check send push message to device'); } else { parent.debug('email', 'Failed auth check push message to device, error: ' + errdesc); }
9085 });
9086 */
9087 } else {
9088 // Ask for a login token
9089 parent.debug('web', 'Asking for login token');
9090 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
9091 }
9092 } else {
9093 checkUserOneTimePassword(req, domain, user, req.query.token, null, function (result, authData) {
9094 if (result == false) {
9095 // Failed, ask for a login token again
9096 parent.debug('web', 'Invalid login token, asking again');
9097 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
9098 } else {
9099 // We are authenticated with 2nd factor.
9100 // Check email verification
9101 if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
9102 parent.debug('web', 'Invalid login, asking for email validation');
9103 try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, email2fasent: true })); ws.close(); } catch (e) { }
9104 } else {
9105 req.session.userid = user._id;
9106 req.session.ip = req.clientIp;
9107 setSessionRandom(req);
9108 func(ws, req, domain, user, null, authData);
9109 }
9110 }
9111 });
9112 }
9113 } else {
9114 // Check email verification
9115 if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
9116 parent.debug('web', 'Invalid login, asking for email validation');
9117 var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
9118 var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
9119 var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
9120 try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, email2fasent: true })); ws.close(); } catch (e) { }
9121 } else {
9122 // We are authenticated
9123 req.session.userid = user._id;
9124 req.session.ip = req.clientIp;
9125 setSessionRandom(req);
9126 func(ws, req, domain, user);
9127 }
9128 }
9129 } else {
9130 // Failed to authenticate, see if a default user is active
9131 if (obj.args.user && obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]) {
9132 // A default user is active
9133 func(ws, req, domain, obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]);
9134 } else {
9135 // If not authenticated, close the websocket connection
9136 parent.debug('web', 'ERR: Websocket bad user/pass auth');
9137 //obj.parent.DispatchEvent(['*', 'server-users', 'user/' + domain.id + '/' + obj.args.user.toLowerCase()], obj, { action: 'authfail', userid: 'user/' + domain.id + '/' + obj.args.user.toLowerCase(), username: obj.args.user, domain: domain.id, msg: 'Invalid user login attempt from ' + req.clientIp });
9138 //obj.setbadLogin(req);
9139 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2a' })); ws.close(); } catch (e) { }
9140 }
9141 }
9142 });
9143 return;
9144 }
9145
9146 if ((req.query.auth != null) && (req.query.auth != '')) {
9147 // This is a encrypted cookie authentication
9148 var cookie = obj.parent.decodeCookie(req.query.auth, obj.parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
9149 if ((cookie == null) && (obj.parent.multiServer != null)) { cookie = obj.parent.decodeCookie(req.query.auth, obj.parent.serverKey, 60); } // Try the server key
9150 if ((cookie != null) && (cookie.ip != null) && !checkCookieIp(cookie.ip, req.clientIp)) { // If the cookie if binded to an IP address, check here.
9151 parent.debug('web', 'ERR: Invalid cookie IP address, got \"' + cookie.ip + '\", expected \"' + cleanRemoteAddr(req.clientIp) + '\".');
9152 cookie = null;
9153 }
9154 if ((cookie != null) && (cookie.userid != null) && (obj.users[cookie.userid]) && (cookie.domainid == domain.id) && (cookie.userid.split('/')[1] == domain.id)) {
9155 // Valid cookie, we are authenticated. Cookie of format { userid: 'user//name', domain: '' }
9156 func(ws, req, domain, obj.users[cookie.userid], cookie);
9157 return;
9158 } else if ((cookie != null) && (cookie.a === 3) && (typeof cookie.u == 'string') && (obj.users[cookie.u]) && (cookie.u.split('/')[1] == domain.id)) {
9159 // Valid cookie, we are authenticated. Cookie of format { u: 'user//name', a: 3 }
9160 func(ws, req, domain, obj.users[cookie.u], cookie);
9161 return;
9162 } else if ((cookie != null) && (cookie.nouser === 1)) {
9163 // This is a valid cookie, but no user. This is used for agent self-sharing.
9164 func(ws, req, domain, null, cookie);
9165 return;
9166 } /*else {
9167 // This is a bad cookie, keep going anyway, maybe we have a active session that will save us.
9168 if ((cookie != null) && (cookie.domainid != domain.id)) { parent.debug('web', 'ERR: Invalid domain, got \"' + cookie.domainid + '\", expected \"' + domain.id + '\".'); }
9169 parent.debug('web', 'ERR: Websocket bad cookie auth (Cookie:' + (cookie != null) + '): ' + req.query.auth);
9170 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2b' })); ws.close(); } catch (e) { }
9171 return;
9172 }
9173 */
9174 }
9175
9176 if (req.headers['x-meshauth'] != null) {
9177 // This is authentication using a custom HTTP header
9178 var s = req.headers['x-meshauth'].split(',');
9179 for (var i in s) { s[i] = Buffer.from(s[i], 'base64').toString(); }
9180 if ((s.length < 2) || (s.length > 3)) { try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2c' })); ws.close(); } catch (e) { } return; }
9181 obj.authenticate(s[0], s[1], domain, function (err, userid, passhint, loginOptions) {
9182 var user = obj.users[userid];
9183 if ((err == null) && (user)) {
9184 // Check if user as the "notools" site right. If so, deny this connection as tools are not allowed to connect.
9185 if ((user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & SITERIGHT_NOMESHCMD)) {
9186 // No tools allowed, close the websocket connection
9187 parent.debug('web', 'ERR: Websocket no tools allowed');
9188 try { ws.send(JSON.stringify({ action: 'close', cause: 'notools', msg: 'notools' })); ws.close(); } catch (e) { }
9189 return;
9190 }
9191
9192 // Check if a 2nd factor is needed
9193 if (checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true) {
9194
9195 // See if we support two-factor trusted cookies
9196 var twoFactorCookieDays = 30;
9197 if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
9198
9199 // Figure out if email 2FA is allowed
9200 var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
9201 var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
9202 var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
9203 if (s.length != 3) {
9204 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
9205 } else {
9206 checkUserOneTimePassword(req, domain, user, s[2], null, function (result, authData) {
9207 if (result == false) {
9208 if ((s[2] == '**email**') && (email2fa == true)) {
9209 // Cause a token to be sent to the user's registered email
9210 user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
9211 obj.db.SetUser(user);
9212 parent.debug('web', 'Sending 2FA email to: ' + user.email);
9213 domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key);
9214 // Ask for a login token & confirm email was sent
9215 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, email2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
9216 } else if ((s[2] == '**sms**') && (sms2fa == true)) {
9217 // Cause a token to be sent to the user's phone number
9218 user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
9219 obj.db.SetUser(user);
9220 parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
9221 parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
9222 // Ask for a login token & confirm sms was sent
9223 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', sms2fa: sms2fa, sms2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
9224 } else if ((s[2] == '**msg**') && (msg2fa == true)) {
9225 // Cause a token to be sent to the user's phone number
9226 user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
9227 obj.db.SetUser(user);
9228 parent.debug('web', 'Sending 2FA message to: ' + user.msghandle);
9229 parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req));
9230 // Ask for a login token & confirm sms was sent
9231 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', msg2fa: msg2fa, msg2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
9232 } else {
9233 // Ask for a login token
9234 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, msg2fa: msg2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
9235 }
9236 } else {
9237 // We are authenticated with 2nd factor.
9238 // Check email verification
9239 if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
9240 parent.debug('web', 'Invalid login, asking for email validation');
9241 try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, email2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
9242 } else {
9243 func(ws, req, domain, user, null, authData);
9244 }
9245 }
9246 });
9247 }
9248 } else {
9249 // We are authenticated
9250 // Check email verification
9251 if (emailcheck && (user.email != null) && (!(user._id.split('/')[2].startsWith('~'))) && (user.emailVerified !== true)) {
9252 parent.debug('web', 'Invalid login, asking for email validation');
9253 try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, email2fasent: true })); ws.close(); } catch (e) { }
9254 } else {
9255 req.session.userid = user._id;
9256 req.session.ip = req.clientIp;
9257 setSessionRandom(req);
9258 func(ws, req, domain, user);
9259 }
9260 }
9261 } else {
9262 // Failed to authenticate, see if a default user is active
9263 if (obj.args.user && obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]) {
9264 // A default user is active
9265 func(ws, req, domain, obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]);
9266 } else {
9267 // If not authenticated, close the websocket connection
9268 parent.debug('web', 'ERR: Websocket bad user/pass auth');
9269 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2d' })); ws.close(); } catch (e) { }
9270 }
9271 }
9272 });
9273 return;
9274 }
9275
9276 if (obj.args.user && obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]) {
9277 // A default user is active
9278 func(ws, req, domain, obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]);
9279 return;
9280 }
9281
9282 if (req.session && (req.session.userid != null) && (req.session.userid.split('/')[1] == domain.id) && (obj.users[req.session.userid])) {
9283 // This user is logged in using the ExpressJS session
9284 func(ws, req, domain, obj.users[req.session.userid]);
9285 return;
9286 }
9287
9288 if (noAuthOk != true) {
9289 // If not authenticated, close the websocket connection
9290 parent.debug('web', 'ERR: Websocket no auth');
9291 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-4' })); ws.close(); } catch (e) { }
9292 } else {
9293 // Continue this session without user authentication,
9294 // this is expected if the agent is connecting for a tunnel.
9295 func(ws, req, domain, null);
9296 }
9297 } catch (e) { console.log(e); }
9298 }
9299
9300 // Find a free port starting with the specified one and going up.
9301 function CheckListenPort(port, addr, func) {
9302 var s = obj.net.createServer(function (socket) { });
9303 obj.tcpServer = s.listen(port, addr, function () { s.close(function () { if (func) { func(port, addr); } }); }).on('error', function (err) {
9304 if (args.exactports) { console.error('ERROR: MeshCentral HTTPS server port ' + port + ' not available.'); process.exit(); }
9305 else { if (port < 65535) { CheckListenPort(port + 1, addr, func); } else { if (func) { func(0); } } }
9306 });
9307 }
9308
9309 // Start the ExpressJS web server
9310 function StartWebServer(port, addr) {
9311 if ((port < 1) || (port > 65535)) return;
9312 obj.args.port = port;
9313 if (obj.tlsServer != null) {
9314 if (obj.args.lanonly == true) {
9315 obj.tcpServer = obj.tlsServer.listen(port, addr, function () { console.log('MeshCentral HTTPS server running on port ' + port + ((typeof args.aliasport == 'number') ? (', alias port ' + args.aliasport) : '') + '.'); });
9316 } else {
9317 obj.tcpServer = obj.tlsServer.listen(port, addr, function () {
9318 console.log('MeshCentral HTTPS server running on ' + certificates.CommonName + ':' + port + ((typeof args.aliasport == 'number') ? (', alias port ' + args.aliasport) : '') + '.');
9319 if (args.relaydns != null) { console.log('MeshCentral HTTPS relay server running on ' + args.relaydns[0] + ':' + port + ((typeof args.aliasport == 'number') ? (', alias port ' + args.aliasport) : '') + '.'); }
9320 });
9321 obj.parent.updateServerState('servername', certificates.CommonName);
9322 }
9323 obj.parent.debug('https', 'Server listening on ' + ((addr != null) ? addr : '0.0.0.0') + ' port ' + port + '.');
9324 obj.parent.updateServerState('https-port', port);
9325 if (args.aliasport != null) { obj.parent.updateServerState('https-aliasport', args.aliasport); }
9326 } else {
9327 obj.tcpServer = obj.app.listen(port, addr, function () {
9328 console.log('MeshCentral HTTP server running on port ' + port + ((typeof args.aliasport == 'number') ? (', alias port ' + args.aliasport) : '') + '.');
9329 if (args.relaydns != null) { console.log('MeshCentral HTTP relay server running on ' + args.relaydns[0] + ':' + port + ((typeof args.aliasport == 'number') ? (', alias port ' + args.aliasport) : '') + '.'); }
9330 });
9331 obj.parent.updateServerState('http-port', port);
9332 if (args.aliasport != null) { obj.parent.updateServerState('http-aliasport', args.aliasport); }
9333 }
9334
9335 // Check if there is a permissions problem with the ports.
9336 if (require('os').platform() != 'win32') {
9337 var expectedPort = obj.parent.config.settings.port ? obj.parent.config.settings.port : 443;
9338 if ((expectedPort != port) && (port >= 1024) && (port < 1034)) {
9339 console.log('');
9340 console.log('WARNING: MeshCentral is running without permissions to use ports below 1025.');
9341 console.log(' Use setcap to grant access to lower ports, or read installation guide.');
9342 console.log('');
9343 console.log(' sudo setcap \'cap_net_bind_service=+ep\' `which node` \r\n');
9344 obj.parent.addServerWarning('Server running without permissions to use ports below 1025.', false);
9345 }
9346 }
9347 }
9348
9349 // Start the ExpressJS web server on agent-only alternative port
9350 function StartAltWebServer(port, addr) {
9351 if ((port < 1) || (port > 65535)) return;
9352 var agentAliasPort = null;
9353 var agentAliasDns = null;
9354 if (args.agentaliasport != null) { agentAliasPort = args.agentaliasport; }
9355 if (args.agentaliasdns != null) { agentAliasDns = args.agentaliasdns; }
9356 if (obj.tlsAltServer != null) {
9357 if (obj.args.lanonly == true) {
9358 obj.tcpAltServer = obj.tlsAltServer.listen(port, addr, function () { console.log('MeshCentral HTTPS agent-only server running on port ' + port + ((agentAliasPort != null) ? (', alias port ' + agentAliasPort) : '') + '.'); });
9359 } else {
9360 obj.tcpAltServer = obj.tlsAltServer.listen(port, addr, function () { console.log('MeshCentral HTTPS agent-only server running on ' + ((agentAliasDns != null) ? agentAliasDns : certificates.CommonName) + ':' + port + ((agentAliasPort != null) ? (', alias port ' + agentAliasPort) : '') + '.'); });
9361 }
9362 obj.parent.debug('https', 'Server listening on 0.0.0.0 port ' + port + '.');
9363 obj.parent.updateServerState('https-agent-port', port);
9364 } else {
9365 obj.tcpAltServer = obj.agentapp.listen(port, addr, function () { console.log('MeshCentral HTTP agent-only server running on port ' + port + ((agentAliasPort != null) ? (', alias port ' + agentAliasPort) : '') + '.'); });
9366 obj.parent.updateServerState('http-agent-port', port);
9367 }
9368 }
9369
9370 // Force mesh agent disconnection
9371 obj.forceMeshAgentDisconnect = function (user, domain, nodeid, disconnectMode) {
9372 if (nodeid == null) return;
9373 var splitnode = nodeid.split('/');
9374 if ((splitnode.length != 3) || (splitnode[1] != domain.id)) return; // Check that nodeid is valid and part of our domain
9375 var agent = obj.wsagents[nodeid];
9376 if (agent == null) return;
9377
9378 // Check we have agent rights
9379 if (((obj.GetMeshRights(user, agent.dbMeshKey) & MESHRIGHT_AGENTCONSOLE) != 0) || (user.siteadmin == 0xFFFFFFFF)) { agent.close(disconnectMode); }
9380 };
9381
9382 // Send the core module to the mesh agent
9383 obj.sendMeshAgentCore = function (user, domain, nodeid, coretype, coredata) {
9384 if (nodeid == null) return;
9385 const splitnode = nodeid.split('/');
9386 if ((splitnode.length != 3) || (splitnode[1] != domain.id)) return; // Check that nodeid is valid and part of our domain
9387
9388 // TODO: This command only works if the agent is connected on the same server. Will not work with multi server peering.
9389 const agent = obj.wsagents[nodeid];
9390 if (agent == null) return;
9391
9392 // Check we have agent rights
9393 if (((obj.GetMeshRights(user, agent.dbMeshKey) & MESHRIGHT_AGENTCONSOLE) != 0) || (user.siteadmin == 0xFFFFFFFF)) {
9394 if (coretype == 'clear') {
9395 // Clear the mesh agent core
9396 agent.agentCoreCheck = 1000; // Tell the agent object we are using a custom core.
9397 agent.send(obj.common.ShortToStr(10) + obj.common.ShortToStr(0));
9398 } else if (coretype == 'default') {
9399 // Reset to default code
9400 agent.agentCoreCheck = 0; // Tell the agent object we are using a default code
9401 agent.send(obj.common.ShortToStr(11) + obj.common.ShortToStr(0)); // Command 11, ask for mesh core hash.
9402 } else if (coretype == 'recovery') {
9403 // Reset to recovery core
9404 agent.agentCoreCheck = 1001; // Tell the agent object we are using the recovery core.
9405 agent.send(obj.common.ShortToStr(11) + obj.common.ShortToStr(0)); // Command 11, ask for mesh core hash.
9406 } else if (coretype == 'tiny') {
9407 // Reset to tiny core
9408 agent.agentCoreCheck = 1011; // Tell the agent object we are using the tiny core.
9409 agent.send(obj.common.ShortToStr(11) + obj.common.ShortToStr(0)); // Command 11, ask for mesh core hash.
9410 } else if (coretype == 'custom') {
9411 agent.agentCoreCheck = 1000; // Tell the agent object we are using a custom core.
9412 var buf = Buffer.from(coredata, 'utf8');
9413 const hash = obj.crypto.createHash('sha384').update(buf).digest().toString('binary'); // Perform a SHA384 hash on the core module
9414 agent.sendBinary(obj.common.ShortToStr(10) + obj.common.ShortToStr(0) + hash + buf.toString('binary')); // Send the code module to the agent
9415 }
9416 }
9417 };
9418
9419 // Get the server path of a user or mesh object
9420 function getServerRootFilePath(obj) {
9421 if ((typeof obj != 'object') || (obj.domain == null) || (obj._id == null)) return null;
9422 var domainname = 'domain', splitname = obj._id.split('/');
9423 if (splitname.length != 3) return null;
9424 if (obj.domain !== '') domainname = 'domain-' + obj.domain;
9425 return obj.path.join(obj.filespath, domainname + "/" + splitname[0] + "-" + splitname[2]);
9426 }
9427
9428 // Return true is the input string looks like an email address
9429 function checkEmail(str) {
9430 var x = str.split('@');
9431 var ok = ((x.length == 2) && (x[0].length > 0) && (x[1].split('.').length > 1) && (x[1].length > 2));
9432 if (ok == true) { var y = x[1].split('.'); for (var i in y) { if (y[i].length == 0) { ok = false; } } }
9433 return ok;
9434 }
9435
9436 /*
9437 obj.wssessions = {}; // UserId --> Array Of Sessions
9438 obj.wssessions2 = {}; // "UserId + SessionRnd" --> Session (Note that the SessionId is the UserId + / + SessionRnd)
9439 obj.wsPeerSessions = {}; // ServerId --> Array Of "UserId + SessionRnd"
9440 obj.wsPeerSessions2 = {}; // "UserId + SessionRnd" --> ServerId
9441 obj.wsPeerSessions3 = {}; // ServerId --> UserId --> [ SessionId ]
9442 */
9443
9444 // Count sessions and event any changes
9445 obj.recountSessions = function (changedSessionId) {
9446 var userid, oldcount, newcount, x, serverid;
9447 if (changedSessionId == null) {
9448 // Recount all sessions
9449
9450 // Calculate the session count for all userid's
9451 var newSessionsCount = {};
9452 for (userid in obj.wssessions) { newSessionsCount[userid] = obj.wssessions[userid].length; }
9453 for (serverid in obj.wsPeerSessions3) {
9454 for (userid in obj.wsPeerSessions3[serverid]) {
9455 x = obj.wsPeerSessions3[serverid][userid].length;
9456 if (newSessionsCount[userid] == null) { newSessionsCount[userid] = x; } else { newSessionsCount[userid] += x; }
9457 }
9458 }
9459
9460 // See what session counts have changed, event any changes
9461 for (userid in newSessionsCount) {
9462 newcount = newSessionsCount[userid];
9463 oldcount = obj.sessionsCount[userid];
9464 if (oldcount == null) { oldcount = 0; } else { delete obj.sessionsCount[userid]; }
9465 if (newcount != oldcount) {
9466 x = userid.split('/');
9467 var u = obj.users[userid];
9468 if (u) {
9469 var targets = ['*', 'server-users'];
9470 if (u.groups) { for (var i in u.groups) { targets.push('server-users:' + i); } }
9471 obj.parent.DispatchEvent(targets, obj, { action: 'wssessioncount', userid: userid, username: x[2], count: newcount, domain: x[1], nolog: 1, nopeers: 1 });
9472 }
9473 }
9474 }
9475
9476 // If there are any counts left in the old counts, event to zero
9477 for (userid in obj.sessionsCount) {
9478 oldcount = obj.sessionsCount[userid];
9479 if ((oldcount != null) && (oldcount != 0)) {
9480 x = userid.split('/');
9481 var u = obj.users[userid];
9482 if (u) {
9483 var targets = ['*', 'server-users'];
9484 if (u.groups) { for (var i in u.groups) { targets.push('server-users:' + i); } }
9485 obj.parent.DispatchEvent(['*'], obj, { action: 'wssessioncount', userid: userid, username: x[2], count: 0, domain: x[1], nolog: 1, nopeers: 1 })
9486 }
9487 }
9488 }
9489
9490 // Set the new session counts
9491 obj.sessionsCount = newSessionsCount;
9492 } else {
9493 // Figure out the userid
9494 userid = changedSessionId.split('/').slice(0, 3).join('/');
9495
9496 // Recount only changedSessionId
9497 newcount = 0;
9498 if (obj.wssessions[userid] != null) { newcount = obj.wssessions[userid].length; }
9499 for (serverid in obj.wsPeerSessions3) { if (obj.wsPeerSessions3[serverid][userid] != null) { newcount += obj.wsPeerSessions3[serverid][userid].length; } }
9500 oldcount = obj.sessionsCount[userid];
9501 if (oldcount == null) { oldcount = 0; }
9502
9503 // If the count changed, update and event
9504 if (newcount != oldcount) {
9505 x = userid.split('/');
9506 var u = obj.users[userid];
9507 if (u) {
9508 var targets = ['*', 'server-users'];
9509 if (u.groups) { for (var i in u.groups) { targets.push('server-users:' + i); } }
9510 obj.parent.DispatchEvent(targets, obj, { action: 'wssessioncount', userid: userid, username: x[2], count: newcount, domain: x[1], nolog: 1, nopeers: 1 });
9511 obj.sessionsCount[userid] = newcount;
9512 }
9513 }
9514 }
9515 };
9516
9517 /* Access Control Functions */
9518
9519 // Remove user rights
9520 function removeUserRights(rights, user) {
9521 if (user.removeRights == null) return rights;
9522 var add = 0, substract = 0;
9523 if ((user.removeRights & 0x00000008) != 0) { substract += 0x00000008; } // No Remote Control
9524 if ((user.removeRights & 0x00010000) != 0) { add += 0x00010000; } // No Desktop
9525 if ((user.removeRights & 0x00000100) != 0) { add += 0x00000100; } // Desktop View Only
9526 if ((user.removeRights & 0x00000200) != 0) { add += 0x00000200; } // No Terminal
9527 if ((user.removeRights & 0x00000400) != 0) { add += 0x00000400; } // No Files
9528 if ((user.removeRights & 0x00000010) != 0) { substract += 0x00000010; } // No Console
9529 if ((user.removeRights & 0x00008000) != 0) { substract += 0x00008000; } // No Uninstall
9530 if ((user.removeRights & 0x00020000) != 0) { substract += 0x00020000; } // No Remote Command
9531 if ((user.removeRights & 0x00000040) != 0) { substract += 0x00000040; } // No Wake
9532 if ((user.removeRights & 0x00040000) != 0) { substract += 0x00040000; } // No Reset/Off
9533 if (rights != 0xFFFFFFFF) {
9534 // If not administrator, add and subsctract restrictions
9535 rights |= add;
9536 rights &= (0xFFFFFFFF - substract);
9537 } else {
9538 // If administrator for a device group, start with permissions and add and subsctract restrictions
9539 rights = 1 + 2 + 4 + 8 + 32 + 64 + 128 + 16384 + 32768 + 131072 + 262144 + 524288 + 1048576;
9540 rights |= add;
9541 rights &= (0xFFFFFFFF - substract);
9542 }
9543 return rights;
9544 }
9545
9546
9547 // Return the node and rights for a array of nodeids
9548 obj.GetNodesWithRights = function (domain, user, nodeids, func) {
9549 var rc = nodeids.length, r = {};
9550 for (var i in nodeids) {
9551 obj.GetNodeWithRights(domain, user, nodeids[i], function (node, rights, visible) {
9552 if ((node != null) && (visible == true)) { r[node._id] = { node: node, rights: rights }; if (--rc == 0) { func(r); } }
9553 });
9554 }
9555 }
9556
9557
9558 // Return the node and rights for a given nodeid
9559 obj.GetNodeWithRights = function (domain, user, nodeid, func) {
9560 // Perform user pre-validation
9561 if ((user == null) || (nodeid == null)) { func(null, 0, false); return; } // Invalid user
9562 if (typeof user == 'string') { user = obj.users[user]; }
9563 if (user == null) { func(null, 0, false); return; } // No rights
9564
9565 // Perform node pre-validation
9566 if (obj.common.validateString(nodeid, 0, 128) == false) { func(null, 0, false); return; } // Invalid nodeid
9567 const snode = nodeid.split('/');
9568 if ((snode.length != 3) || (snode[0] != 'node')) { func(null, 0, false); return; } // Invalid nodeid
9569 if ((domain != null) && (snode[1] != domain.id)) { func(null, 0, false); return; } // Invalid domain
9570
9571 // Check that we have permissions for this node.
9572 db.Get(nodeid, function (err, nodes) {
9573 if ((nodes == null) || (nodes.length != 1)) { func(null, 0, false); return; } // No such nodeid
9574
9575 // This is a super user that can see all device groups for a given domain
9576 if ((user.siteadmin == 0xFFFFFFFF) && ((parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0) || (user.links && Object.keys(user.links).some(key => parent.config.settings.managealldevicegroups.indexOf(key) >= 0))) && (nodes[0].domain == user.domain)) {
9577 func(nodes[0], removeUserRights(0xFFFFFFFF, user), true); return;
9578 }
9579
9580 // If no links, stop here.
9581 if (user.links == null) { func(null, 0, false); return; }
9582
9583 // Check device link
9584 var rights = 0, visible = false, r = user.links[nodeid];
9585 if (r != null) {
9586 if (r.rights == 0xFFFFFFFF) { func(nodes[0], removeUserRights(0xFFFFFFFF, user), true); return; } // User has full rights thru a device link, stop here.
9587 rights |= r.rights;
9588 visible = true;
9589 }
9590
9591 // Check device group link
9592 r = user.links[nodes[0].meshid];
9593 if (r != null) {
9594 if (r.rights == 0xFFFFFFFF) { func(nodes[0], removeUserRights(0xFFFFFFFF, user), true); return; } // User has full rights thru a device group link, stop here.
9595 rights |= r.rights;
9596 visible = true;
9597 }
9598
9599 // Check user group links
9600 for (var i in user.links) {
9601 if (i.startsWith('ugrp/')) {
9602 const g = obj.userGroups[i];
9603 if (g && (g.links != null)) {
9604 r = g.links[nodes[0].meshid];
9605 if (r != null) {
9606 if (r.rights == 0xFFFFFFFF) { func(nodes[0], removeUserRights(0xFFFFFFFF, user), true); return; } // User has full rights thru a user group link, stop here.
9607 rights |= r.rights; // TODO: Deal with reverse rights
9608 visible = true;
9609 }
9610 r = g.links[nodeid];
9611 if (r != null) {
9612 if (r.rights == 0xFFFFFFFF) { func(nodes[0], removeUserRights(0xFFFFFFFF, user), true); return; } // User has full rights thru a user group direct link, stop here.
9613 rights |= r.rights; // TODO: Deal with reverse rights
9614 visible = true;
9615 }
9616 }
9617 }
9618 }
9619
9620 // Remove any user rights
9621 rights = removeUserRights(rights, user);
9622
9623 // Return the rights we found
9624 func(nodes[0], rights, visible);
9625 });
9626 }
9627
9628 // Returns a list of all meshes that this user has some rights too
9629 obj.GetAllMeshWithRights = function (user, rights) {
9630 if (typeof user == 'string') { user = obj.users[user]; }
9631 if (user == null) { return []; }
9632
9633 var r = [];
9634 if ((user.siteadmin == 0xFFFFFFFF) && ((parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0) || (user.links && Object.keys(user.links).some(key => parent.config.settings.managealldevicegroups.indexOf(key) >= 0))) ) {
9635 // This is a super user that can see all device groups for a given domain
9636 var meshStartStr = 'mesh/' + user.domain + '/';
9637 for (var i in obj.meshes) { if ((obj.meshes[i]._id.startsWith(meshStartStr)) && (obj.meshes[i].deleted == null)) { r.push(obj.meshes[i]); } }
9638 return r;
9639 }
9640 if (user.links == null) { return []; }
9641 for (var i in user.links) {
9642 if (i.startsWith('mesh/')) {
9643 // Grant access to a device group thru a direct link
9644 const m = obj.meshes[i];
9645 if ((m) && (r.indexOf(m) == -1) && (m.deleted == null) && ((rights == null) || ((user.links[i].rights & rights) != 0))) { r.push(m); }
9646 } else if (i.startsWith('ugrp/')) {
9647 // Grant access to a device group thru a user group
9648 const g = obj.userGroups[i];
9649 for (var j in g.links) {
9650 if (j.startsWith('mesh/') && ((rights == null) || ((g.links[j].rights != null) && (g.links[j].rights & rights) != 0))) {
9651 const m = obj.meshes[j];
9652 if ((m) && (m.deleted == null) && (r.indexOf(m) == -1)) { r.push(m); }
9653 }
9654 }
9655 }
9656 }
9657 return r;
9658 }
9659
9660 // Returns a list of all mesh id's that this user has some rights too
9661 obj.GetAllMeshIdWithRights = function (user, rights) {
9662 if (typeof user == 'string') { user = obj.users[user]; }
9663 if (user == null) { return []; }
9664 var r = [];
9665 if ((user.siteadmin == 0xFFFFFFFF) && ((parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0) || (user.links && Object.keys(user.links).some(key => parent.config.settings.managealldevicegroups.indexOf(key) >= 0)))) {
9666 // This is a super user that can see all device groups for a given domain
9667 var meshStartStr = 'mesh/' + user.domain + '/';
9668 for (var i in obj.meshes) { if ((obj.meshes[i]._id.startsWith(meshStartStr)) && (obj.meshes[i].deleted == null)) { r.push(obj.meshes[i]._id); } }
9669 return r;
9670 }
9671 if (user.links == null) { return []; }
9672 for (var i in user.links) {
9673 if (i.startsWith('mesh/')) {
9674 // Grant access to a device group thru a direct link
9675 const m = obj.meshes[i];
9676 if ((m) && (m.deleted == null) && ((rights == null) || ((user.links[i].rights & rights) != 0))) {
9677 if (r.indexOf(m._id) == -1) { r.push(m._id); }
9678 }
9679 } else if (i.startsWith('ugrp/')) {
9680 // Grant access to a device group thru a user group
9681 const g = obj.userGroups[i];
9682 if (g && (g.links != null) && ((rights == null) || ((user.links[i].rights & rights) != 0))) {
9683 for (var j in g.links) {
9684 if (j.startsWith('mesh/')) {
9685 const m = obj.meshes[j];
9686 if ((m) && (m.deleted == null)) {
9687 if (r.indexOf(m._id) == -1) { r.push(m._id); }
9688 }
9689 }
9690 }
9691 }
9692 }
9693 }
9694 return r;
9695 }
9696
9697 // Get the rights of a user on a given device group
9698 obj.GetMeshRights = function (user, mesh) {
9699 if ((user == null) || (mesh == null)) { return 0; }
9700 if (typeof user == 'string') { user = obj.users[user]; }
9701 if (user == null) { return 0; }
9702 var r, meshid;
9703 if (typeof mesh == 'string') {
9704 meshid = mesh;
9705 } else if ((typeof mesh == 'object') && (typeof mesh._id == 'string')) {
9706 meshid = mesh._id;
9707 } else return 0;
9708
9709 // Check if this is a super user that can see all device groups for a given domain
9710 if ((user.siteadmin == 0xFFFFFFFF) && ((parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0) || (user.links && Object.keys(user.links).some(key => parent.config.settings.managealldevicegroups.indexOf(key) >= 0))) && (meshid.startsWith('mesh/' + user.domain + '/'))) { return removeUserRights(0xFFFFFFFF, user); }
9711
9712 // Check direct user to device group permissions
9713 if (user.links == null) return 0;
9714 var rights = 0;
9715 r = user.links[meshid];
9716 if (r != null) {
9717 var rights = r.rights;
9718 if (rights == 0xFFFFFFFF) { return removeUserRights(rights, user); } // If the user has full access thru direct link, stop here.
9719 }
9720
9721 // Check if we are part of any user groups that would give this user more access.
9722 for (var i in user.links) {
9723 if (i.startsWith('ugrp')) {
9724 const g = obj.userGroups[i];
9725 if (g) {
9726 r = g.links[meshid];
9727 if (r != null) {
9728 if (r.rights == 0xFFFFFFFF) {
9729 return removeUserRights(r.rights, user); // If the user hash full access thru a user group link, stop here.
9730 } else {
9731 rights |= r.rights; // Add to existing rights (TODO: Deal with reverse rights)
9732 }
9733 }
9734 }
9735
9736 }
9737 }
9738
9739 return removeUserRights(rights, user);
9740 }
9741
9742 // Returns true if the user can view the given device group
9743 obj.IsMeshViewable = function (user, mesh) {
9744 if ((user == null) || (mesh == null)) { return false; }
9745 if (typeof user == 'string') { user = obj.users[user]; }
9746 if (user == null) { return false; }
9747 var meshid;
9748 if (typeof mesh == 'string') {
9749 meshid = mesh;
9750 } else if ((typeof mesh == 'object') && (typeof mesh._id == 'string')) {
9751 meshid = mesh._id;
9752 } else return false;
9753
9754 // Check if this is a super user that can see all device groups for a given domain
9755 if ((user.siteadmin == 0xFFFFFFFF) && ((parent.config.settings.managealldevicegroups.indexOf(user._id) >= 0) || (user.links && Object.keys(user.links).some(key => parent.config.settings.managealldevicegroups.indexOf(key) >= 0))) && (meshid.startsWith('mesh/' + user.domain + '/'))) { return true; }
9756
9757 // Check direct user to device group permissions
9758 if (user.links == null) { return false; }
9759 if (user.links[meshid] != null) { return true; } // If the user has a direct link, stop here.
9760
9761 // Check if we are part of any user groups that would give this user visibility to this device group.
9762 for (var i in user.links) {
9763 if (i.startsWith('ugrp')) {
9764 const g = obj.userGroups[i];
9765 if (g && (g.links[meshid] != null)) { return true; } // If the user has a user group link, stop here.
9766 }
9767 }
9768
9769 return false;
9770 }
9771
9772 var GetNodeRightsCache = {};
9773 var GetNodeRightsCacheCount = 0;
9774
9775 // Return the user rights for a given node
9776 obj.GetNodeRights = function (user, mesh, nodeid) {
9777 if ((user == null) || (mesh == null) || (nodeid == null)) { return 0; }
9778 if (typeof user == 'string') { user = obj.users[user]; }
9779 if (user == null) { return 0; }
9780 var meshid;
9781 if (typeof mesh == 'string') { meshid = mesh; } else if ((typeof mesh == 'object') && (typeof mesh._id == 'string')) { meshid = mesh._id; } else return 0;
9782
9783 // Check if we have this in the cache
9784
9785 const cache = ((GetNodeRightsCache[user._id] || {})[meshid] || {})[nodeid];
9786 if (cache != null) { if (cache.t > Date.now()) { return cache.o; } else { GetNodeRightsCacheCount--; } } // Cache hit, or we need to update the cache
9787 if (GetNodeRightsCacheCount > 2000) { obj.FlushGetNodeRightsCache() } // From time to time, flush the cache
9788
9789 var r = obj.GetMeshRights(user, mesh);
9790 if (r == 0xFFFFFFFF) {
9791 const out = removeUserRights(r, user);
9792 GetNodeRightsCache[user._id] = GetNodeRightsCache[user._id] || {}
9793 GetNodeRightsCache[user._id][meshid] = GetNodeRightsCache[user._id][meshid] || {}
9794 GetNodeRightsCache[user._id][meshid][nodeid] = { t: Date.now() + 10000, o: out };
9795 GetNodeRightsCacheCount++;
9796 return out;
9797 }
9798
9799 // Check direct device rights using device data
9800 if ((user.links != null) && (user.links[nodeid] != null)) { r |= user.links[nodeid].rights; } // TODO: Deal with reverse permissions
9801 if (r == 0xFFFFFFFF) {
9802 const out = removeUserRights(r, user);
9803 GetNodeRightsCache[user._id] = GetNodeRightsCache[user._id] || {}
9804 GetNodeRightsCache[user._id][meshid] = GetNodeRightsCache[user._id][meshid] || {}
9805 GetNodeRightsCache[user._id][meshid][nodeid] = { t: Date.now() + 10000, o: out };
9806 GetNodeRightsCacheCount++;
9807 return out;
9808 }
9809
9810 // Check direct device rights thru a user group
9811 for (var i in user.links) {
9812 if (i.startsWith('ugrp')) {
9813 const g = obj.userGroups[i];
9814 if (g && (g.links[nodeid] != null)) { r |= g.links[nodeid].rights; }
9815 }
9816 }
9817
9818 const out = removeUserRights(r, user);
9819 GetNodeRightsCache[user._id] = GetNodeRightsCache[user._id] || {}
9820 GetNodeRightsCache[user._id][meshid] = GetNodeRightsCache[user._id][meshid] || {}
9821 GetNodeRightsCache[user._id][meshid][nodeid] = { t: Date.now() + 10000, o: out };
9822 GetNodeRightsCacheCount++;
9823 return out;
9824 }
9825
9826 obj.InvalidateNodeCache = function (user, mesh, nodeid) {
9827 if (user == null) { return; }
9828
9829 if (typeof user == 'string') { user = obj.users[user]; }
9830 if (user == null) { return 0; }
9831 var meshid;
9832 if (typeof mesh == 'string') { meshid = mesh; } else if ((typeof mesh == 'object') && (typeof mesh._id == 'string')) { meshid = mesh._id; };
9833
9834 if (mesh == null) {
9835 for (let [key, val] of Object.entries(GetNodeRightsCache[user._id] || {})) {
9836 GetNodeRightsCacheCount -= Object.keys(val).length
9837 }
9838 delete GetNodeRightsCache[user._id];
9839 return;
9840 }
9841 if (nodeid == null) {
9842 let cache_reduction = Object.keys((GetNodeRightsCache[user._id] || {})[meshid] || {}).length
9843 delete (GetNodeRightsCache[user._id] || {})[meshid]
9844 GetNodeRightsCacheCount -= cache_reduction;
9845 return;
9846 }
9847 if (((GetNodeRightsCache[user._id] || {})[meshid] || {})[nodeid]) {
9848 delete ((GetNodeRightsCache[user._id] || {})[meshid] || {})[nodeid]
9849 GetNodeRightsCacheCount--;
9850 }
9851 }
9852
9853 obj.FlushGetNodeRightsCache = function() {
9854 GetNodeRightsCache = {};
9855 GetNodeRightsCacheCount = 0;
9856 }
9857
9858 // Returns a list of displatch targets for a given mesh
9859 // We have to target the meshid and all user groups for this mesh, plus any added targets
9860 obj.CreateMeshDispatchTargets = function (mesh, addedTargets) {
9861 var targets = (addedTargets != null) ? addedTargets : [];
9862 if (targets.indexOf('*') == -1) { targets.push('*'); }
9863 if (typeof mesh == 'string') { mesh = obj.meshes[mesh]; }
9864 if (mesh != null) { targets.push(mesh._id); for (var i in mesh.links) { if (i.startsWith('ugrp/')) { targets.push(i); } } }
9865 return targets;
9866 }
9867
9868 // Returns a list of displatch targets for a given mesh
9869 // We have to target the meshid and all user groups for this mesh, plus any added targets
9870 obj.CreateNodeDispatchTargets = function (mesh, nodeid, addedTargets) {
9871 var targets = (addedTargets != null) ? addedTargets : [];
9872 targets.push(nodeid);
9873 if (targets.indexOf('*') == -1) { targets.push('*'); }
9874 if (typeof mesh == 'string') { mesh = obj.meshes[mesh]; }
9875 if (mesh != null) { targets.push(mesh._id); for (var i in mesh.links) { if (i.startsWith('ugrp/')) { targets.push(i); } } }
9876 for (var i in obj.userGroups) { const g = obj.userGroups[i]; if ((g != null) && (g.links != null) && (g.links[nodeid] != null)) { targets.push(i); } }
9877 return targets;
9878 }
9879
9880 // Clone a safe version of a user object, remove everything that is secret.
9881 obj.CloneSafeUser = function (user) {
9882 if (typeof user != 'object') { return user; }
9883 var user2 = Object.assign({}, user); // Shallow clone
9884 delete user2.hash;
9885 delete user2.passhint;
9886 delete user2.salt;
9887 delete user2.type;
9888 delete user2.domain;
9889 delete user2.subscriptions;
9890 delete user2.passtype;
9891 delete user2.otpsms;
9892 delete user2.otpmsg;
9893 if ((typeof user2.otpekey == 'object') && (user2.otpekey != null)) { user2.otpekey = 1; } // Indicates that email 2FA is enabled.
9894 if ((typeof user2.otpduo == 'object') && (user2.otpduo != null)) { user2.otpduo = 1; } // Indicates that duo 2FA is enabled.
9895 if ((typeof user2.otpsecret == 'string') && (user2.otpsecret != null)) { user2.otpsecret = 1; } // Indicates a time secret is present.
9896 if ((typeof user2.otpkeys == 'object') && (user2.otpkeys != null)) { user2.otpkeys = 0; if (user.otpkeys != null) { for (var i = 0; i < user.otpkeys.keys.length; i++) { if (user.otpkeys.keys[i].u == true) { user2.otpkeys = 1; } } } } // Indicates the number of one time backup codes that are active.
9897 if ((typeof user2.otphkeys == 'object') && (user2.otphkeys != null)) { user2.otphkeys = user2.otphkeys.length; } // Indicates the number of hardware keys setup
9898 if ((typeof user2.otpdev == 'string') && (user2.otpdev != null)) { user2.otpdev = 1; } // Indicates device for 2FA push notification
9899 if ((typeof user2.webpush == 'object') && (user2.webpush != null)) { user2.webpush = user2.webpush.length; } // Indicates the number of web push sessions we have
9900 return user2;
9901 }
9902
9903 // Clone a safe version of a node object, remove everything that is secret.
9904 obj.CloneSafeNode = function (node) {
9905 if (typeof node != 'object') { return node; }
9906 var r = node;
9907 if ((r.pmt != null) || (r.ssh != null) || (r.rdp != null) || ((r.intelamt != null) && ((r.intelamt.pass != null) || (r.intelamt.mpspass != null)))) {
9908 r = Object.assign({}, r); // Shallow clone
9909 if (r.pmt != null) { r.pmt = 1; }
9910 if (r.ssh != null) {
9911 var n = {};
9912 for (var i in r.ssh) {
9913 if (i.startsWith('user/')) {
9914 if (r.ssh[i].p) { n[i] = 1; } // Username and password
9915 else if (r.ssh[i].k && r.ssh[i].kp) { n[i] = 2; } // Username, key and password
9916 else if (r.ssh[i].k) { n[i] = 3; } // Username and key. No password.
9917 }
9918 }
9919 r.ssh = n;
9920 }
9921 if (r.rdp != null) { var n = {}; for (var i in r.rdp) { if (i.startsWith('user/')) { n[i] = 1; } } r.rdp = n; }
9922 if ((r.intelamt != null) && ((r.intelamt.pass != null) || (r.intelamt.mpspass != null))) {
9923 r.intelamt = Object.assign({}, r.intelamt); // Shallow clone
9924 if (r.intelamt.pass != null) { r.intelamt.pass = 1; }; // Remove the Intel AMT administrator password from the node
9925 if (r.intelamt.mpspass != null) { r.intelamt.mpspass = 1; }; // Remove the Intel AMT MPS password from the node
9926 }
9927 }
9928 return r;
9929 }
9930
9931 // Clone a safe version of a mesh object, remove everything that is secret.
9932 obj.CloneSafeMesh = function (mesh) {
9933 if (typeof mesh != 'object') { return mesh; }
9934 var r = mesh;
9935 if (((r.amt != null) && (r.amt.password != null)) || ((r.kvm != null) && (r.kvm.pass != null))) {
9936 r = Object.assign({}, r); // Shallow clone
9937 if ((r.amt != null) && (r.amt.password != null)) {
9938 r.amt = Object.assign({}, r.amt); // Shallow clone
9939 if ((r.amt.password != null) && (r.amt.password != '')) { r.amt.password = 1; } // Remove the Intel AMT password from the policy
9940 }
9941 if ((r.kvm != null) && (r.kvm.pass != null)) {
9942 r.kvm = Object.assign({}, r.kvm); // Shallow clone
9943 if ((r.kvm.pass != null) && (r.kvm.pass != '')) { r.kvm.pass = 1; } // Remove the IP KVM device password
9944 }
9945 }
9946 return r;
9947 }
9948
9949 // Filter the user web site and only output state that we need to keep
9950 const acceptableUserWebStateStrings = ['webPageStackMenu', 'notifications', 'deviceView', 'nightMode', 'webPageFullScreen', 'search', 'showRealNames', 'sort', 'deskAspectRatio', 'viewsize', 'DeskControl', 'uiMode', 'footerBar','loctag','theme','lastThemes','uiViewMode'];
9951 const acceptableUserWebStateDesktopStrings = [
9952 'encoding', 'showfocus', 'showmouse', 'quality', 'scaling', 'framerate', 'agentencoding', 'swapmouse',
9953 'rmw', 'remotekeymap', 'autoclipboard', 'autolock', 'localkeymap', 'kvmrmw', 'rdpsize', 'rdpsmb',
9954 'rdprmw', 'rdpautoclipboard', 'rdpflags'
9955 ];
9956 obj.filterUserWebState = function (state) {
9957 if (typeof state == 'string') { try { state = JSON.parse(state); } catch (ex) { return null; } }
9958 if ((state == null) || (typeof state != 'object')) { return null; }
9959 var out = {};
9960 for (var i in acceptableUserWebStateStrings) {
9961 var n = acceptableUserWebStateStrings[i];
9962 if ((state[n] != null) && ((typeof state[n] == 'number') || (typeof state[n] == 'boolean') || ((typeof state[n] == 'string') && (state[n].length < 64)))) { out[n] = state[n]; }
9963 }
9964 if ((typeof state.stars == 'string') && (state.stars.length < 2048)) { out.stars = state.stars; }
9965 if (typeof state.desktopsettings == 'string') { try { state.desktopsettings = JSON.parse(state.desktopsettings); } catch (ex) { delete state.desktopsettings; } }
9966 if (state.desktopsettings != null) {
9967 out.desktopsettings = {};
9968 for (var i in acceptableUserWebStateDesktopStrings) {
9969 var n = acceptableUserWebStateDesktopStrings[i];
9970 if ((state.desktopsettings[n] != null) && ((typeof state.desktopsettings[n] == 'number') || (typeof state.desktopsettings[n] == 'boolean') || ((typeof state.desktopsettings[n] == 'string') && (state.desktopsettings[n].length < 32)))) { out.desktopsettings[n] = state.desktopsettings[n]; }
9971 }
9972 out.desktopsettings = JSON.stringify(out.desktopsettings);
9973 }
9974 if ((typeof state.deskKeyShortcuts == 'string') && (state.deskKeyShortcuts.length < 2048)) { out.deskKeyShortcuts = state.deskKeyShortcuts; }
9975 if ((typeof state.deskStrings == 'string') && (state.deskStrings.length < 10000)) { out.deskStrings = state.deskStrings; }
9976 if ((typeof state.runopt == 'string') && (state.runopt.length < 30000)) { out.runopt = state.runopt; }
9977 return JSON.stringify(out);
9978 }
9979
9980 // Return the correct render page given mobile, minify and override path.
9981 function getRenderPage(pagename, req, domain) {
9982 var mobile = isMobileBrowser(req), minify = (domain.minify == true), p;
9983 if (req.query.mobile == '1') { mobile = true; } else if (req.query.mobile == '0') { mobile = false; }
9984 if (req.query.minify == '1') { minify = true; } else if (req.query.minify == '0') { minify = false; }
9985 if ((domain != null) && (domain.mobilesite === false)) { mobile = false; }
9986 if (mobile) {
9987 if ((domain != null) && (domain.webviewspath != null)) { // If the domain has a web views path, use that first
9988 if (minify) {
9989 p = obj.path.join(domain.webviewspath, pagename + '-mobile-min');
9990 if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Minify + Override document
9991 }
9992 p = obj.path.join(domain.webviewspath, pagename + '-mobile');
9993 if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Override document
9994 }
9995 if (obj.parent.webViewsOverridePath != null) {
9996 if (minify) {
9997 p = obj.path.join(obj.parent.webViewsOverridePath, pagename + '-mobile-min');
9998 if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Minify + Override document
9999 }
10000 p = obj.path.join(obj.parent.webViewsOverridePath, pagename + '-mobile');
10001 if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Override document
10002 }
10003 if (minify) {
10004 p = obj.path.join(obj.parent.webViewsPath, pagename + '-mobile-min');
10005 if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Minify document
10006 }
10007 p = obj.path.join(obj.parent.webViewsPath, pagename + '-mobile');
10008 if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile document
10009 }
10010 if ((domain != null) && (domain.webviewspath != null)) { // If the domain has a web views path, use that first
10011 if (minify) {
10012 p = obj.path.join(domain.webviewspath, pagename + '-min');
10013 if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Minify + Override document
10014 }
10015 p = obj.path.join(domain.webviewspath, pagename);
10016 if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Override document
10017 }
10018 if (obj.parent.webViewsOverridePath != null) {
10019 if (minify) {
10020 p = obj.path.join(obj.parent.webViewsOverridePath, pagename + '-min');
10021 if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Minify + Override document
10022 }
10023 p = obj.path.join(obj.parent.webViewsOverridePath, pagename);
10024 if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Override document
10025 }
10026 if (minify) {
10027 p = obj.path.join(obj.parent.webViewsPath, pagename + '-min');
10028 if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Minify document
10029 }
10030 p = obj.path.join(obj.parent.webViewsPath, pagename);
10031 if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Default document
10032 return null;
10033 }
10034
10035 function generateCustomCSSTags(customFilesObject, currentTemplate) {
10036 var cssTags = '';
10037
10038 cssTags += '<link keeplink=1 type="text/css" href="styles/custom.css" media="screen" rel="stylesheet" title="CSS" />\n ';
10039
10040
10041 if (customFilesObject) {
10042 if (Array.isArray(customFilesObject)) {
10043 // Legacy array support - convert to object format
10044 for (var i = 0; i < customFilesObject.length; i++) {
10045 var customFileConfig = customFilesObject[i];
10046 if (customFileConfig && customFileConfig.css && Array.isArray(customFileConfig.css)) {
10047 if ((customFileConfig.scope && customFileConfig.scope.indexOf('all') !== -1) ||
10048 (currentTemplate && customFileConfig.scope && customFileConfig.scope.indexOf(currentTemplate) !== -1)) {
10049 for (var j = 0; j < customFileConfig.css.length; j++) {
10050 cssTags += '<link keeplink=1 type="text/css" href="styles/' + customFileConfig.css[j] + '" media="screen" rel="stylesheet" title="CSS" />\n ';
10051 }
10052 }
10053 }
10054 }
10055 } else if (customFilesObject.css && Array.isArray(customFilesObject.css)) {
10056 // Legacy single object support
10057 for (var i = 0; i < customFilesObject.css.length; i++) {
10058 cssTags += '<link keeplink=1 type="text/css" href="styles/' + customFilesObject.css[i] + '" media="screen" rel="stylesheet" title="CSS" />\n ';
10059 }
10060 } else if (typeof customFilesObject === 'object') {
10061 // New object format - iterate through each custom file configuration
10062 for (var configName in customFilesObject) {
10063 var customFileConfig = customFilesObject[configName];
10064 if (customFileConfig && customFileConfig.css && Array.isArray(customFileConfig.css)) {
10065 if ((customFileConfig.scope && customFileConfig.scope.indexOf('all') !== -1) ||
10066 (currentTemplate && customFileConfig.scope && customFileConfig.scope.indexOf(currentTemplate) !== -1)) {
10067 for (var j = 0; j < customFileConfig.css.length; j++) {
10068 cssTags += '<link keeplink=1 type="text/css" href="styles/' + customFileConfig.css[j] + '" media="screen" rel="stylesheet" title="CSS" />\n ';
10069 }
10070 }
10071 }
10072 }
10073 }
10074 }
10075
10076 return cssTags.trim();
10077 }
10078
10079 function generateCustomJSTags(customFilesObject, currentTemplate) {
10080 var jsTags = '';
10081
10082 jsTags += '<script keeplink=1 type="text/javascript" src="scripts/custom.js"></script>\n ';
10083
10084 if (customFilesObject) {
10085 if (Array.isArray(customFilesObject)) {
10086 // Legacy array support - convert to object format
10087 for (var i = 0; i < customFilesObject.length; i++) {
10088 var customFileConfig = customFilesObject[i];
10089 if (customFileConfig && customFileConfig.js && Array.isArray(customFileConfig.js)) {
10090 if ((customFileConfig.scope && customFileConfig.scope.indexOf('all') !== -1) ||
10091 (currentTemplate && customFileConfig.scope && customFileConfig.scope.indexOf(currentTemplate) !== -1)) {
10092 for (var j = 0; j < customFileConfig.js.length; j++) {
10093 jsTags += '<script keeplink=1 type="text/javascript" src="scripts/' + customFileConfig.js[j] + '"></script>\n ';
10094 }
10095 }
10096 }
10097 }
10098 } else if (customFilesObject.js && Array.isArray(customFilesObject.js)) {
10099 // Legacy single object support
10100 for (var i = 0; i < customFilesObject.js.length; i++) {
10101 jsTags += '<script keeplink=1 type="text/javascript" src="scripts/' + customFilesObject.js[i] + '"></script>\n ';
10102 }
10103 } else if (typeof customFilesObject === 'object') {
10104 // New object format - iterate through each custom file configuration
10105 for (var configName in customFilesObject) {
10106 var customFileConfig = customFilesObject[configName];
10107 if (customFileConfig && customFileConfig.js && Array.isArray(customFileConfig.js)) {
10108 if ((customFileConfig.scope && customFileConfig.scope.indexOf('all') !== -1) ||
10109 (currentTemplate && customFileConfig.scope && customFileConfig.scope.indexOf(currentTemplate) !== -1)) {
10110 for (var j = 0; j < customFileConfig.js.length; j++) {
10111 jsTags += '<script keeplink=1 type="text/javascript" src="scripts/' + customFileConfig.js[j] + '"></script>\n ';
10112 }
10113 }
10114 }
10115 }
10116 }
10117 }
10118
10119 return jsTags.trim();
10120 }
10121
10122 function generateThemePackCSSTags(domain, currentTemplate) {
10123 var cssTags = '';
10124 // Load theme pack if domain has one configured AND (domain forces sitestyle 3 OR user is viewing sitestyle 3)
10125 var isModernUI = (currentTemplate === 'default3') || (domain.sitestyle === 3);
10126 if (domain && domain.themepack && isModernUI) {
10127 var themePath = obj.path.join(obj.parent.datapath, 'theme-pack', domain.themepack, 'public');
10128 if (obj.fs.existsSync(obj.path.join(themePath, 'styles', 'theme.css'))) {
10129 cssTags += '<link keeplink=1 type="text/css" href="styles/theme.css" media="screen" rel="stylesheet" title="CSS" />\n ';
10130 }
10131 }
10132 return cssTags;
10133 }
10134 function generateThemePackJSTags(domain, currentTemplate) {
10135 var jsTags = '';
10136 // Load theme pack if domain has one configured AND (domain forces sitestyle 3 OR user is viewing sitestyle 3)
10137 var isModernUI = (currentTemplate === 'default3') || (domain.sitestyle === 3);
10138 if (domain && domain.themepack && isModernUI) {
10139 var themePath = obj.path.join(obj.parent.datapath, 'theme-pack', domain.themepack, 'public');
10140 if (obj.fs.existsSync(obj.path.join(themePath, 'scripts', 'theme.js'))) {
10141 jsTags += '<script keeplink=1 type="text/javascript" src="scripts/theme.js"></script>\n ';
10142 }
10143 }
10144 return jsTags;
10145 }
10146
10147 // Return the correct render page arguments.
10148 function getRenderArgs(xargs, req, domain, page) {
10149 var minify = (domain.minify == true);
10150 if (req.query.minify == '1') { minify = true; } else if (req.query.minify == '0') { minify = false; }
10151 xargs.min = minify ? '-min' : '';
10152 xargs.titlehtml = domain.titlehtml;
10153 xargs.title = (domain.title != null) ? domain.title : 'MeshCentral';
10154 if (
10155 ((page == 'login2') && (domain.loginpicture == null) && (domain.titlehtml == null)) ||
10156 ((page != 'login2') && (domain.titlepicture == null) && (domain.titlehtml == null))
10157 ) {
10158 if (domain.title == null) {
10159 xargs.title1 = 'MeshCentral';
10160 xargs.title2 = '';
10161 } else {
10162 xargs.title1 = domain.title;
10163 xargs.title2 = domain.title2 ? domain.title2 : '';
10164 }
10165 } else {
10166 xargs.title1 = domain.title1 ? domain.title1 : '';
10167 xargs.title2 = (domain.title1 && domain.title2) ? domain.title2 : '';
10168 }
10169 xargs.title2 = obj.common.replacePlaceholders(xargs.title2, {
10170 'serverversion': obj.parent.currentVer,
10171 'servername': obj.getWebServerName(domain, req),
10172 'agentsessions': Object.keys(parent.webserver.wsagents).length,
10173 'connectedusers': Object.keys(parent.webserver.wssessions).length,
10174 'userssessions': Object.keys(parent.webserver.wssessions2).length,
10175 'relaysessions': parent.webserver.relaySessionCount,
10176 'relaycount': Object.keys(parent.webserver.wsrelays).length
10177 });
10178 xargs.extitle = encodeURIComponent(xargs.title).split('\'').join('\\\'');
10179 xargs.domainurl = domain.url;
10180 xargs.autocomplete = (domain.autocomplete === false) ? 'autocomplete=off x' : 'autocomplete'; // This option allows autocomplete to be turned off on the login page.
10181 if (typeof domain.hide == 'number') { xargs.hide = domain.hide; }
10182
10183 // To mitigate any possible BREACH attack, we generate a random 0 to 255 bytes length string here.
10184 xargs.randomlength = (args.webpagelengthrandomization !== false) ? parent.crypto.randomBytes(parent.crypto.randomBytes(1)[0]).toString('base64') : '';
10185
10186 // Generate custom CSS and JS tags
10187 if (xargs.customFiles) {
10188 try {
10189 var customFiles = JSON.parse(decodeURIComponent(xargs.customFiles));
10190 xargs.customCSSTags = generateCustomCSSTags(customFiles, page);
10191 xargs.customJSTags = generateCustomJSTags(customFiles, page);
10192 } catch (ex) {
10193 xargs.customCSSTags = generateCustomCSSTags(null, page);
10194 xargs.customJSTags = generateCustomJSTags(null, page);
10195 }
10196 } else {
10197 xargs.customCSSTags = generateCustomCSSTags(null, page);
10198 xargs.customJSTags = generateCustomJSTags(null, page);
10199 }
10200 xargs.customCSSTags += generateThemePackCSSTags(domain, page);
10201 xargs.customJSTags += generateThemePackJSTags(domain, page);
10202 return xargs;
10203 }
10204
10205 // Route a command from a agent. domainid, nodeid and meshid are the values of the source agent.
10206 obj.routeAgentCommand = function (command, domainid, nodeid, meshid) {
10207 // Route a message.
10208 // If this command has a sessionid, that is the target.
10209 if (command.sessionid != null) {
10210 if (typeof command.sessionid != 'string') return;
10211 var splitsessionid = command.sessionid.split('/');
10212 // Check that we are in the same domain and the user has rights over this node.
10213 if ((splitsessionid.length == 4) && (splitsessionid[0] == 'user') && (splitsessionid[1] == domainid)) {
10214 // Check if this user has rights to get this message
10215 if (obj.GetNodeRights(splitsessionid[0] + '/' + splitsessionid[1] + '/' + splitsessionid[2], meshid, nodeid) == 0) return; // TODO: Check if this is ok
10216
10217 // See if the session is connected. If so, go ahead and send this message to the target node
10218 var ws = obj.wssessions2[command.sessionid];
10219 if (ws != null) {
10220 command.nodeid = nodeid; // Set the nodeid, required for responses.
10221 delete command.sessionid; // Remove the sessionid, since we are sending to that sessionid, so it's implyed.
10222 try { ws.send(JSON.stringify(command)); } catch (ex) { }
10223 } else if (parent.multiServer != null) {
10224 // See if we can send this to a peer server
10225 var serverid = obj.wsPeerSessions2[command.sessionid];
10226 if (serverid != null) {
10227 command.fromNodeid = nodeid;
10228 parent.multiServer.DispatchMessageSingleServer(command, serverid);
10229 }
10230 }
10231 }
10232 } else if (command.userid != null) { // If this command has a userid, that is the target.
10233 if (typeof command.userid != 'string') return;
10234 var splituserid = command.userid.split('/');
10235 // Check that we are in the same domain and the user has rights over this node.
10236 if ((splituserid[0] == 'user') && (splituserid[1] == domainid)) {
10237 // Check if this user has rights to get this message
10238 if (obj.GetNodeRights(command.userid, meshid, nodeid) == 0) return; // TODO: Check if this is ok
10239
10240 // See if the session is connected
10241 var sessions = obj.wssessions[command.userid];
10242
10243 // Go ahead and send this message to the target node
10244 if (sessions != null) {
10245 command.nodeid = nodeid; // Set the nodeid, required for responses.
10246 delete command.userid; // Remove the userid, since we are sending to that userid, so it's implyed.
10247 for (i in sessions) { sessions[i].send(JSON.stringify(command)); }
10248 }
10249
10250 if (parent.multiServer != null) {
10251 // TODO: Add multi-server support
10252 }
10253 }
10254 } else { // Route this command to all users with MESHRIGHT_AGENTCONSOLE rights to this device group
10255 command.nodeid = nodeid;
10256 var cmdstr = JSON.stringify(command);
10257
10258 // Find all connected user sessions with access to this device
10259 for (var userid in obj.wssessions) {
10260 var xsessions = obj.wssessions[userid];
10261 if (obj.GetNodeRights(userid, meshid, nodeid) != 0) {
10262 // Send the message to all sessions for this user on this server
10263 for (i in xsessions) { try { xsessions[i].send(cmdstr); } catch (e) { } }
10264 }
10265 }
10266
10267 // Send the message to all users of other servers
10268 if (parent.multiServer != null) {
10269 delete command.nodeid;
10270 command.fromNodeid = nodeid;
10271 command.meshid = meshid;
10272 parent.multiServer.DispatchMessage(command);
10273 }
10274 }
10275 }
10276
10277 // Returns a list of acceptable languages in order
10278 obj.getLanguageCodes = function (req) {
10279 // If a user set a localization, use that
10280 if ((req.query.lang == null) && (req.session != null) && (req.session.userid)) {
10281 var user = obj.users[req.session.userid];
10282 if ((user != null) && (user.lang != null)) { req.query.lang = user.lang; }
10283 };
10284
10285 // Get a list of acceptable languages in order
10286 var acceptLanguages = [];
10287 if (req.query.lang != null) {
10288 acceptLanguages.push(req.query.lang.toLowerCase());
10289 } else {
10290 if (req.headers['accept-language'] != null) {
10291 var acceptLanguageSplit = req.headers['accept-language'].split(';');
10292 for (var i in acceptLanguageSplit) {
10293 var acceptLanguageSplitEx = acceptLanguageSplit[i].split(',');
10294 for (var j in acceptLanguageSplitEx) { if (acceptLanguageSplitEx[j].startsWith('q=') == false) { acceptLanguages.push(acceptLanguageSplitEx[j].toLowerCase()); } }
10295 }
10296 }
10297 }
10298
10299 return acceptLanguages;
10300 }
10301
10302 // Render a page using the proper language
10303 function render(req, res, filename, args, user) {
10304 if (obj.renderPages != null) {
10305 // Get the list of acceptable languages in order
10306 var acceptLanguages = obj.getLanguageCodes(req);
10307 var domain = getDomain(req);
10308 // Take a look at the options we have for this file
10309 var fileOptions = obj.renderPages[domain.id][obj.path.basename(filename)];
10310 if (fileOptions != null) {
10311 for (var i in acceptLanguages) {
10312 if (acceptLanguages[i] == 'zh-tw') { acceptLanguages[i] = 'zh-cht'; } // Change newer "zh-tw" to legacy "zh-cht" Chinese (Traditional) for now
10313 if (acceptLanguages[i] == 'zh-cn') { acceptLanguages[i] = 'zh-chs'; } // Change newer "zh-ch" to legacy "zh-chs" Chinese (Simplified) for now
10314 if ((acceptLanguages[i] == 'en') || (acceptLanguages[i].startsWith('en-'))) {
10315 // English requested
10316 args.lang = 'en';
10317 if (user && user.llang) { delete user.llang; obj.db.SetUser(user); } // Clear user 'last language' used if needed. Since English is the default, remove "last language".
10318 break;
10319 }
10320
10321 // See if a language (like "fr-ca") or short-language (like "fr") matches an available translation file.
10322 var foundLanguage = null;
10323 if (fileOptions[acceptLanguages[i]] != null) { foundLanguage = acceptLanguages[i]; } else {
10324 const ptr = acceptLanguages[i].indexOf('-');
10325 if (ptr >= 0) {
10326 const shortAcceptedLanguage = acceptLanguages[i].substring(0, ptr);
10327 if (fileOptions[shortAcceptedLanguage] != null) { foundLanguage = shortAcceptedLanguage; }
10328 }
10329 }
10330
10331 // If a language is found, render it.
10332 if (foundLanguage != null) {
10333 // Found a match. If the file no longer exists, default to English.
10334 obj.fs.exists(fileOptions[foundLanguage] + '.handlebars', function (exists) {
10335 if (exists) { args.lang = foundLanguage; res.render(fileOptions[foundLanguage], args); } else { args.lang = 'en'; res.render(filename, args); }
10336 });
10337 if (user && (user.llang != foundLanguage)) { user.llang = foundLanguage; obj.db.SetUser(user); } // Set user 'last language' used if needed.
10338 return;
10339 }
10340 }
10341 }
10342 }
10343
10344 // No matches found, render the default English page.
10345 res.render(filename, args);
10346 }
10347
10348 // Get the list of pages with different languages that can be rendered
10349 function getRenderList() {
10350 // Fetch default rendeing pages
10351 var translateFolder = null;
10352 if (obj.fs.existsSync('views/translations')) { translateFolder = 'views/translations'; }
10353 if (obj.fs.existsSync(obj.path.join(__dirname, 'views', 'translations'))) { translateFolder = obj.path.join(__dirname, 'views', 'translations'); }
10354
10355 if (translateFolder != null) {
10356 obj.renderPages = {};
10357 obj.renderLanguages = ['en'];
10358 for (var i in parent.config.domains) {
10359 if (obj.fs.existsSync('views/translations')) { translateFolder = 'views/translations'; }
10360 if (obj.fs.existsSync(obj.path.join(__dirname, 'views', 'translations'))) { translateFolder = obj.path.join(__dirname, 'views', 'translations'); }
10361 var files = obj.fs.readdirSync(translateFolder);
10362 var domain = parent.config.domains[i].id;
10363 obj.renderPages[domain] = {};
10364 for (var i in files) {
10365 var name = files[i];
10366 if (name.endsWith('.handlebars')) {
10367 name = name.substring(0, name.length - 11);
10368 var xname = name.split('_');
10369 if (xname.length == 2) {
10370 if (obj.renderPages[domain][xname[0]] == null) { obj.renderPages[domain][xname[0]] = {}; }
10371 obj.renderPages[domain][xname[0]][xname[1]] = obj.path.join(translateFolder, name);
10372 if (obj.renderLanguages.indexOf(xname[1]) == -1) { obj.renderLanguages.push(xname[1]); }
10373 }
10374 }
10375 }
10376 // See if there are any custom rending pages that will override the default ones
10377 if ((obj.parent.webViewsOverridePath != null) && (obj.fs.existsSync(obj.path.join(obj.parent.webViewsOverridePath, 'translations')))) {
10378 translateFolder = obj.path.join(obj.parent.webViewsOverridePath, 'translations');
10379 var files = obj.fs.readdirSync(translateFolder);
10380 for (var i in files) {
10381 var name = files[i];
10382 if (name.endsWith('.handlebars')) {
10383 name = name.substring(0, name.length - 11);
10384 var xname = name.split('_');
10385 if (xname.length == 2) {
10386 if (obj.renderPages[domain][xname[0]] == null) { obj.renderPages[domain][xname[0]] = {}; }
10387 obj.renderPages[domain][xname[0]][xname[1]] = obj.path.join(translateFolder, name);
10388 if (obj.renderLanguages.indexOf(xname[1]) == -1) { obj.renderLanguages.push(xname[1]); }
10389 }
10390 }
10391 }
10392 }
10393 // See if there is a custom meshcentral-web-domain folder as that will override the default ones
10394 if (obj.fs.existsSync(obj.path.join(__dirname, '..', 'meshcentral-web-' + domain, 'views', 'translations'))) {
10395 translateFolder = obj.path.join(__dirname, '..', 'meshcentral-web-' + domain, 'views', 'translations');
10396 var files = obj.fs.readdirSync(translateFolder);
10397 for (var i in files) {
10398 var name = files[i];
10399 if (name.endsWith('.handlebars')) {
10400 name = name.substring(0, name.length - 11);
10401 var xname = name.split('_');
10402 if (xname.length == 2) {
10403 if (obj.renderPages[domain][xname[0]] == null) { obj.renderPages[domain][xname[0]] = {}; }
10404 obj.renderPages[domain][xname[0]][xname[1]] = obj.path.join(translateFolder, name);
10405 if (obj.renderLanguages.indexOf(xname[1]) == -1) { obj.renderLanguages.push(xname[1]); }
10406 }
10407 }
10408 }
10409 }
10410 }
10411 }
10412 }
10413
10414 // Get the list of pages with different languages that can be rendered
10415 function getEmailLanguageList() {
10416 // Fetch default rendeing pages
10417 var translateFolder = null;
10418 if (obj.fs.existsSync('emails/translations')) { translateFolder = 'emails/translations'; }
10419 if (obj.fs.existsSync(obj.path.join(__dirname, 'emails', 'translations'))) { translateFolder = obj.path.join(__dirname, 'emails', 'translations'); }
10420
10421 if (translateFolder != null) {
10422 obj.emailLanguages = ['en'];
10423 var files = obj.fs.readdirSync(translateFolder);
10424 for (var i in files) {
10425 var name = files[i];
10426 if (name.endsWith('.html')) {
10427 name = name.substring(0, name.length - 5);
10428 var xname = name.split('_');
10429 if (xname.length == 2) {
10430 if (obj.emailLanguages.indexOf(xname[1]) == -1) { obj.emailLanguages.push(xname[1]); }
10431 }
10432 }
10433 }
10434
10435 // See if there are any custom rending pages that will override the default ones
10436 if ((obj.parent.webEmailsOverridePath != null) && (obj.fs.existsSync(obj.path.join(obj.parent.webEmailsOverridePath, 'translations')))) {
10437 translateFolder = obj.path.join(obj.parent.webEmailsOverridePath, 'translations');
10438 var files = obj.fs.readdirSync(translateFolder);
10439 for (var i in files) {
10440 var name = files[i];
10441 if (name.endsWith('.html')) {
10442 name = name.substring(0, name.length - 5);
10443 var xname = name.split('_');
10444 if (xname.length == 2) {
10445 if (obj.emailLanguages.indexOf(xname[1]) == -1) { obj.emailLanguages.push(xname[1]); }
10446 }
10447 }
10448 }
10449 }
10450 }
10451 }
10452
10453 // Perform a web push to a user
10454 // If any of the push fail, remove the subscription from the user's webpush subscription list.
10455 obj.performWebPush = function (domain, user, payload, options) {
10456 if ((parent.webpush == null) || (Array.isArray(user.webpush) == false) || (user.webpush.length == 0)) return;
10457
10458 var completionFunc = function pushCompletionFunc(sub, fail) {
10459 pushCompletionFunc.failCount += fail;
10460 if (--pushCompletionFunc.pushCount == 0) {
10461 if (pushCompletionFunc.failCount > 0) {
10462 var user = pushCompletionFunc.user, newwebpush = [];
10463 for (var i in user.webpush) { if (user.webpush[i].fail == null) { newwebpush.push(user.webpush[i]); } }
10464 user.webpush = newwebpush;
10465
10466 // Update the database
10467 obj.db.SetUser(user);
10468
10469 // Event the change
10470 var message = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', domain: domain.id, nolog: 1 };
10471 if (db.changeStream) { message.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come.
10472 var targets = ['*', 'server-users', user._id];
10473 if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
10474 parent.DispatchEvent(targets, obj, message);
10475 }
10476 }
10477 }
10478 completionFunc.pushCount = user.webpush.length;
10479 completionFunc.user = user;
10480 completionFunc.domain = domain;
10481 completionFunc.failCount = 0;
10482
10483 for (var i in user.webpush) {
10484 var errorFunc = function pushErrorFunc(error) { pushErrorFunc.sub.fail = 1; pushErrorFunc.call(pushErrorFunc.sub, 1); }
10485 errorFunc.sub = user.webpush[i];
10486 errorFunc.call = completionFunc;
10487 var successFunc = function pushSuccessFunc(value) { pushSuccessFunc.call(pushSuccessFunc.sub, 0); }
10488 successFunc.sub = user.webpush[i];
10489 successFunc.call = completionFunc;
10490 parent.webpush.sendNotification(user.webpush[i], JSON.stringify(payload), options).then(successFunc, errorFunc);
10491 }
10492
10493 }
10494
10495 // Ensure exclusivity of a push messaging token for Android device
10496 obj.removePmtFromAllOtherNodes = function (node) {
10497 if (typeof node.pmt != 'string') return;
10498 db.Get('pmt_' + node.pmt, function (err, docs) {
10499 if ((err == null) && (docs.length == 1)) {
10500 var oldNodeId = docs[0].nodeid;
10501 db.Get(oldNodeId, function (nerr, ndocs) {
10502 if ((nerr == null) && (ndocs.length == 1)) {
10503 var oldNode = ndocs[0];
10504 if (oldNode.pmt == node.pmt) {
10505 // Remove the push messaging token and save the node.
10506 delete oldNode.pmt;
10507 db.Set(oldNode);
10508
10509 // Event the node change
10510 var event = { etype: 'node', action: 'changenode', nodeid: oldNode._id, domain: oldNode.domain, node: obj.CloneSafeNode(oldNode) }
10511 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.
10512 parent.DispatchEvent(['*', oldNode.meshid, oldNode._id], obj, event);
10513 }
10514 }
10515 });
10516 }
10517 db.Set({ _id: 'pmt_' + node.pmt, type: 'pmt', domain: node.domain, time: Date.now(), nodeid: node._id })
10518 });
10519 }
10520
10521 // Return true if a mobile browser is detected.
10522 // This code comes from "http://detectmobilebrowsers.com/" and was modified, This is free and unencumbered software released into the public domain. For more information, please refer to the http://unlicense.org/
10523 function isMobileBrowser(req) {
10524 //var ua = req.headers['user-agent'].toLowerCase();
10525 //return (/(android|bb\d+|meego).+mobile|mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(ua) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(ua.substr(0, 4)));
10526 if (typeof req.headers['user-agent'] != 'string') return false;
10527 return (req.headers['user-agent'].toLowerCase().indexOf('mobile') >= 0);
10528 }
10529
10530 // Return decoded user agent information
10531 obj.getUserAgentInfo = function (req) {
10532 var browser = 'Unknown', os = 'Unknown';
10533 try {
10534 const ua = obj.uaparser((typeof req == 'string') ? req : req.headers['user-agent']);
10535 // Add client hints if available
10536 if((typeof req != 'string')){
10537 const ch = new obj.uaclienthints.UAClientHints().setValuesFromHeaders(req.headers);
10538 Object.assign(ua, ch);
10539 }
10540 if (ua.browser && ua.browser.name) { ua.browserStr = ua.browser.name; if (ua.browser.version) { ua.browserStr += '/' + ua.browser.version } }
10541 if (ua.os && ua.os.name) { ua.osStr = ua.os.name; if (ua.os.version) { ua.osStr += '/' + ua.os.version } }
10542 // If the platform is set, use that instead of the OS
10543 if (ua.platform) {
10544 ua.osStr = ua.platform;
10545 // Special case for Windows 11
10546 if (ua.platformVersion) {
10547 if (ua.platform == 'Windows' && parseInt(ua.platformVersion) >= 13) {
10548 ua.platformVersion = '11';
10549 }
10550 ua.osStr += '/' + ua.platformVersion
10551 }
10552 }
10553 return ua;
10554 } catch (ex) { return { browserStr: browser, osStr: os } }
10555 }
10556
10557 // Return the query string portion of the URL, the ? and anything after BUT remove secret keys from authentication providers
10558 function getQueryPortion(req) {
10559 var removeKeys = ['duo_code', 'state']; // Keys to remove
10560 var s = req.url.indexOf('?');
10561 if (s == -1) {
10562 if (req.body && req.body.urlargs) {
10563 return req.body.urlargs;
10564 }
10565 return '';
10566 }
10567 var queryString = req.url.substring(s + 1);
10568 var params = queryString.split('&');
10569 var filteredParams = [];
10570 for (var i = 0; i < params.length; i++) {
10571 var key = params[i].split('=')[0];
10572 if (removeKeys.indexOf(key) === -1) {
10573 filteredParams.push(params[i]);
10574 }
10575 }
10576 return (filteredParams.length > 0 ? ('?' + filteredParams.join('&')) : '');
10577 }
10578
10579 // Generate a random Intel AMT password
10580 function checkAmtPassword(p) { return (p.length > 7) && (/\d/.test(p)) && (/[a-z]/.test(p)) && (/[A-Z]/.test(p)) && (/\W/.test(p)); }
10581 function getRandomAmtPassword() { var p; do { p = Buffer.from(obj.crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); } while (checkAmtPassword(p) == false); return p; }
10582 function getRandomPassword() { return Buffer.from(obj.crypto.randomBytes(9), 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); }
10583 function getRandomLowerCase(len) { var r = '', random = obj.crypto.randomBytes(len); for (var i = 0; i < len; i++) { r += String.fromCharCode(97 + (random[i] % 26)); } return r; }
10584
10585 // Generate a 8 digit integer with even random probability for each value.
10586 function getRandomEightDigitInteger() { var bigInt; do { bigInt = parent.crypto.randomBytes(4).readUInt32BE(0); } while (bigInt >= 4200000000); return bigInt % 100000000; }
10587 function getRandomSixDigitInteger() { var bigInt; do { bigInt = parent.crypto.randomBytes(4).readUInt32BE(0); } while (bigInt >= 4200000000); return bigInt % 1000000; }
10588
10589 // Clean a IPv6 address that encodes a IPv4 address
10590 function cleanRemoteAddr(addr) { if (typeof addr != 'string') { return null; } if (addr.indexOf('::ffff:') == 0) { return addr.substring(7); } else { return addr; } }
10591
10592 // Set the content disposition header for a HTTP response.
10593 // Because the filename can't have any special characters in it, we need to be extra careful.
10594 function setContentDispositionHeader(res, type, name, size, altname) {
10595 var name = require('path').basename(name).split('\\').join('').split('/').join('').split(':').join('').split('*').join('').split('?').join('').split('"').join('').split('<').join('').split('>').join('').split('|').join('').split('\'').join('');
10596 try {
10597 var x = { 'Cache-Control': 'no-store', 'Content-Type': type, 'Content-Disposition': 'attachment; filename="' + encodeURIComponent(name) + '"' };
10598 if (typeof size == 'number') { x['Content-Length'] = size; }
10599 res.set(x);
10600 } catch (ex) {
10601 var x = { 'Cache-Control': 'no-store', 'Content-Type': type, 'Content-Disposition': 'attachment; filename="' + altname + '"' };
10602 if (typeof size == 'number') { x['Content-Length'] = size; }
10603 res.set(x);
10604 }
10605 }
10606
10607 // Perform a IP match against a list
10608 function isIPMatch(ip, matchList) {
10609 const ipcheck = require('ipcheck');
10610 for (var i in matchList) { if (ipcheck.match(ip, matchList[i]) == true) return true; }
10611 return false;
10612 }
10613
10614 // This is the invalid login throttling code
10615 obj.badLoginTable = {};
10616 obj.badLoginTableLastClean = 0;
10617 if (parent.config.settings == null) { parent.config.settings = {}; }
10618 if (parent.config.settings.maxinvalidlogin !== false) {
10619 if (typeof parent.config.settings.maxinvalidlogin != 'object') { parent.config.settings.maxinvalidlogin = { time: 10, count: 10 }; }
10620 if (typeof parent.config.settings.maxinvalidlogin.time != 'number') { parent.config.settings.maxinvalidlogin.time = 10; }
10621 if (typeof parent.config.settings.maxinvalidlogin.count != 'number') { parent.config.settings.maxinvalidlogin.count = 10; }
10622 if ((typeof parent.config.settings.maxinvalidlogin.coolofftime != 'number') || (parent.config.settings.maxinvalidlogin.coolofftime < 1)) { parent.config.settings.maxinvalidlogin.coolofftime = null; }
10623 }
10624 obj.setbadLogin = function (ip) { // Set an IP address that just did a bad login request
10625 if (parent.config.settings.maxinvalidlogin === false) return;
10626 if (typeof ip == 'object') { ip = ip.clientIp; }
10627 if (parent.config.settings.maxinvalidlogin != null) {
10628 if (typeof parent.config.settings.maxinvalidlogin.exclude == 'string') {
10629 const excludeSplit = parent.config.settings.maxinvalidlogin.exclude.split(',');
10630 for (var i in excludeSplit) { if (require('ipcheck').match(ip, excludeSplit[i])) return; }
10631 } else if (Array.isArray(parent.config.settings.maxinvalidlogin.exclude)) {
10632 for (var i in parent.config.settings.maxinvalidlogin.exclude) { if (require('ipcheck').match(ip, parent.config.settings.maxinvalidlogin.exclude[i])) return; }
10633 }
10634 }
10635 var splitip = ip.split('.');
10636 if (splitip.length == 4) { ip = (splitip[0] + '.' + splitip[1] + '.' + splitip[2] + '.*'); }
10637 if (++obj.badLoginTableLastClean > 100) { obj.cleanBadLoginTable(); }
10638 if (typeof obj.badLoginTable[ip] == 'number') { if (obj.badLoginTable[ip] < Date.now()) { delete obj.badLoginTable[ip]; } else { return; } } // Check cooloff period
10639 if (obj.badLoginTable[ip] == null) { obj.badLoginTable[ip] = [Date.now()]; } else { obj.badLoginTable[ip].push(Date.now()); }
10640 if ((obj.badLoginTable[ip].length >= parent.config.settings.maxinvalidlogin.count) && (parent.config.settings.maxinvalidlogin.coolofftime != null)) {
10641 obj.badLoginTable[ip] = Date.now() + (parent.config.settings.maxinvalidlogin.coolofftime * 60000); // Move to cooloff period
10642 }
10643 }
10644 obj.checkAllowLogin = function (ip) { // Check if an IP address is allowed to login
10645 if (parent.config.settings.maxinvalidlogin === false) return true;
10646 if (typeof ip == 'object') { ip = ip.clientIp; }
10647 var splitip = ip.split('.');
10648 if (splitip.length == 4) { ip = (splitip[0] + '.' + splitip[1] + '.' + splitip[2] + '.*'); } // If this is IPv4, keep only the 3 first
10649 var cutoffTime = Date.now() - (parent.config.settings.maxinvalidlogin.time * 60000); // Time in minutes
10650 var ipTable = obj.badLoginTable[ip];
10651 if (ipTable == null) return true;
10652 if (typeof ipTable == 'number') { if (obj.badLoginTable[ip] < Date.now()) { delete obj.badLoginTable[ip]; } else { return false; } } // Check cooloff period
10653 while ((ipTable.length > 0) && (ipTable[0] < cutoffTime)) { ipTable.shift(); }
10654 if (ipTable.length == 0) { delete obj.badLoginTable[ip]; return true; }
10655 return (ipTable.length < parent.config.settings.maxinvalidlogin.count); // No more than x bad logins in x minutes
10656 }
10657 obj.cleanBadLoginTable = function () { // Clean up the IP address login blockage table, we do this occasionaly.
10658 if (parent.config.settings.maxinvalidlogin === false) return;
10659 var cutoffTime = Date.now() - (parent.config.settings.maxinvalidlogin.time * 60000); // Time in minutes
10660 for (var ip in obj.badLoginTable) {
10661 var ipTable = obj.badLoginTable[ip];
10662 if (typeof ipTable == 'number') {
10663 if (obj.badLoginTable[ip] < Date.now()) { delete obj.badLoginTable[ip]; } // Check cooloff period
10664 } else {
10665 while ((ipTable.length > 0) && (ipTable[0] < cutoffTime)) { ipTable.shift(); }
10666 if (ipTable.length == 0) { delete obj.badLoginTable[ip]; }
10667 }
10668 }
10669 obj.badLoginTableLastClean = 0;
10670 }
10671
10672 // This is the invalid 2FA throttling code
10673 obj.bad2faTable = {};
10674 obj.bad2faTableLastClean = 0;
10675 if (parent.config.settings == null) { parent.config.settings = {}; }
10676 if (parent.config.settings.maxinvalid2fa !== false) {
10677 if (typeof parent.config.settings.maxinvalid2fa != 'object') { parent.config.settings.maxinvalid2fa = { time: 10, count: 10 }; }
10678 if (typeof parent.config.settings.maxinvalid2fa.time != 'number') { parent.config.settings.maxinvalid2fa.time = 10; }
10679 if (typeof parent.config.settings.maxinvalid2fa.count != 'number') { parent.config.settings.maxinvalid2fa.count = 10; }
10680 if ((typeof parent.config.settings.maxinvalid2fa.coolofftime != 'number') || (parent.config.settings.maxinvalid2fa.coolofftime < 1)) { parent.config.settings.maxinvalid2fa.coolofftime = null; }
10681 }
10682 obj.setbad2Fa = function (ip) { // Set an IP address that just did a bad 2FA request
10683 if (parent.config.settings.maxinvalid2fa === false) return;
10684 if (typeof ip == 'object') { ip = ip.clientIp; }
10685 if (parent.config.settings.maxinvalid2fa != null) {
10686 if (typeof parent.config.settings.maxinvalid2fa.exclude == 'string') {
10687 const excludeSplit = parent.config.settings.maxinvalid2fa.exclude.split(',');
10688 for (var i in excludeSplit) { if (require('ipcheck').match(ip, excludeSplit[i])) return; }
10689 } else if (Array.isArray(parent.config.settings.maxinvalid2fa.exclude)) {
10690 for (var i in parent.config.settings.maxinvalid2fa.exclude) { if (require('ipcheck').match(ip, parent.config.settings.maxinvalid2fa.exclude[i])) return; }
10691 }
10692 }
10693 var splitip = ip.split('.');
10694 if (splitip.length == 4) { ip = (splitip[0] + '.' + splitip[1] + '.' + splitip[2] + '.*'); }
10695 if (++obj.bad2faTableLastClean > 100) { obj.cleanBad2faTable(); }
10696 if (typeof obj.bad2faTable[ip] == 'number') { if (obj.bad2faTable[ip] < Date.now()) { delete obj.bad2faTable[ip]; } else { return; } } // Check cooloff period
10697 if (obj.bad2faTable[ip] == null) { obj.bad2faTable[ip] = [Date.now()]; } else { obj.bad2faTable[ip].push(Date.now()); }
10698 if ((obj.bad2faTable[ip].length >= parent.config.settings.maxinvalid2fa.count) && (parent.config.settings.maxinvalid2fa.coolofftime != null)) {
10699 obj.bad2faTable[ip] = Date.now() + (parent.config.settings.maxinvalid2fa.coolofftime * 60000); // Move to cooloff period
10700 }
10701 }
10702 obj.checkAllow2Fa = function (ip) { // Check if an IP address is allowed to perform 2FA
10703 if (parent.config.settings.maxinvalid2fa === false) return true;
10704 if (typeof ip == 'object') { ip = ip.clientIp; }
10705 var splitip = ip.split('.');
10706 if (splitip.length == 4) { ip = (splitip[0] + '.' + splitip[1] + '.' + splitip[2] + '.*'); } // If this is IPv4, keep only the 3 first
10707 var cutoffTime = Date.now() - (parent.config.settings.maxinvalid2fa.time * 60000); // Time in minutes
10708 var ipTable = obj.bad2faTable[ip];
10709 if (ipTable == null) return true;
10710 if (typeof ipTable == 'number') { if (obj.bad2faTable[ip] < Date.now()) { delete obj.bad2faTable[ip]; } else { return false; } } // Check cooloff period
10711 while ((ipTable.length > 0) && (ipTable[0] < cutoffTime)) { ipTable.shift(); }
10712 if (ipTable.length == 0) { delete obj.bad2faTable[ip]; return true; }
10713 return (ipTable.length < parent.config.settings.maxinvalid2fa.count); // No more than x bad 2FAs in x minutes
10714 }
10715 obj.cleanBad2faTable = function () { // Clean up the IP address 2FA blockage table, we do this occasionaly.
10716 if (parent.config.settings.maxinvalid2fa === false) return;
10717 var cutoffTime = Date.now() - (parent.config.settings.maxinvalid2fa.time * 60000); // Time in minutes
10718 for (var ip in obj.bad2faTable) {
10719 var ipTable = obj.bad2faTable[ip];
10720 if (typeof ipTable == 'number') {
10721 if (obj.bad2faTable[ip] < Date.now()) { delete obj.bad2faTable[ip]; } // Check cooloff period
10722 } else {
10723 while ((ipTable.length > 0) && (ipTable[0] < cutoffTime)) { ipTable.shift(); }
10724 if (ipTable.length == 0) { delete obj.bad2faTable[ip]; }
10725 }
10726 }
10727 obj.bad2faTableLastClean = 0;
10728 }
10729
10730 // Hold a websocket until additional arguments are provided within the socket.
10731 // This is a generic function that can be used for any websocket to avoid passing arguments in the URL.
10732 function getWebsocketArgs(ws, req, func) {
10733 if (req.query.moreargs != '1') {
10734 // No more arguments needed, pass the websocket thru
10735 func(ws, req);
10736 } else {
10737 // More arguments are needed
10738 delete req.query.moreargs;
10739 const xfunc = function getWebsocketArgsEx(msg) {
10740 var command = null;
10741 try { command = JSON.parse(msg.toString('utf8')); } catch (e) { return; }
10742 if ((command != null) && (command.action === 'urlargs') && (typeof command.args == 'object')) {
10743 for (var i in command.args) { getWebsocketArgsEx.req.query[i] = command.args[i]; }
10744 ws.removeEventListener('message', getWebsocketArgsEx);
10745 getWebsocketArgsEx.func(getWebsocketArgsEx.ws, getWebsocketArgsEx.req);
10746 }
10747 }
10748 xfunc.ws = ws;
10749 xfunc.req = req;
10750 xfunc.func = func;
10751 ws.on('message', xfunc);
10752 }
10753 }
10754
10755 // Set a random value to this session. Only works if the session has a userid.
10756 // This random value along with the userid is used to destroy the session when logging out.
10757 function setSessionRandom(req) {
10758 if ((req.session == null) || (req.session.userid == null) || (req.session.x != null)) return;
10759 var x = obj.crypto.randomBytes(6).toString('base64');
10760 while (obj.destroyedSessions[req.session.userid + '/' + x] != null) { x = obj.crypto.randomBytes(6).toString('base64'); }
10761 req.session.x = x;
10762 }
10763
10764 // Remove all destroyed sessions after 2 hours, these sessions would have timed out anyway.
10765 function clearDestroyedSessions() {
10766 var toRemove = [], t = Date.now() - (2 * 60 * 60 * 1000);
10767 for (var i in obj.destroyedSessions) { if (obj.destroyedSessions[i] < t) { toRemove.push(i); } }
10768 for (var i in toRemove) { delete obj.destroyedSessions[toRemove[i]]; }
10769 }
10770
10771 // Check and/or convert the agent color value into a correct string or return empty string.
10772 function checkAgentColorString(header, value) {
10773 if ((typeof header !== 'string') || (typeof value !== 'string')) return '';
10774 if (value.startsWith('#') && (value.length == 7)) {
10775 // Convert color in hex format
10776 value = parseInt(value.substring(1, 3), 16) + ',' + parseInt(value.substring(3, 5), 16) + ',' + parseInt(value.substring(5, 7), 16);
10777 } else {
10778 // Check color in decimal format
10779 const valueSplit = value.split(',');
10780 if (valueSplit.length != 3) return '';
10781 const r = parseInt(valueSplit[0]), g = parseInt(valueSplit[1]), b = parseInt(valueSplit[2]);
10782 if (isNaN(r) || (r < 0) || (r > 255) || isNaN(g) || (g < 0) || (g > 255) || isNaN(b) || (b < 0) || (b > 255)) return '';
10783 value = r + ',' + g + ',' + b;
10784 }
10785 return header + value + '\r\n';
10786 }
10787
10788 // Check that everything is cleaned up
10789 function checkWebRelaySessionsTimeout() {
10790 for (var i in webRelaySessions) { webRelaySessions[i].checkTimeout(); }
10791 }
10792
10793 // Return true if this is a private IP address
10794 function isPrivateAddress(ip_addr) {
10795 // If this is a loopback address, return true
10796 if ((ip_addr == '127.0.0.1') || (ip_addr == '::1')) return true;
10797
10798 // Check IPv4 private addresses
10799 const ipcheck = require('ipcheck');
10800 const IPv4PrivateRanges = ['0.0.0.0/8', '10.0.0.0/8', '100.64.0.0/10', '127.0.0.0/8', '169.254.0.0/16', '172.16.0.0/12', '192.0.0.0/24', '192.0.0.0/29', '192.0.0.8/32', '192.0.0.9/32', '192.0.0.10/32', '192.0.0.170/32', '192.0.0.171/32', '192.0.2.0/24', '192.31.196.0/24', '192.52.193.0/24', '192.88.99.0/24', '192.168.0.0/16', '192.175.48.0/24', '198.18.0.0/15', '198.51.100.0/24', '203.0.113.0/24', '240.0.0.0/4', '255.255.255.255/32']
10801 for (var i in IPv4PrivateRanges) { if (ipcheck.match(ip_addr, IPv4PrivateRanges[i])) return true; }
10802
10803 // Check IPv6 private addresses
10804 return /^::$/.test(ip_addr) ||
10805 /^::1$/.test(ip_addr) ||
10806 /^::f{4}:([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/.test(ip_addr) ||
10807 /^::f{4}:0.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/.test(ip_addr) ||
10808 /^64:ff9b::([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/.test(ip_addr) ||
10809 /^100::([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4})$/.test(ip_addr) ||
10810 /^2001::([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4})$/.test(ip_addr) ||
10811 /^2001:2[0-9a-fA-F]:([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4})$/.test(ip_addr) ||
10812 /^2001:db8:([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4})$/.test(ip_addr) ||
10813 /^2002:([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4}):?([0-9a-fA-F]{0,4})$/.test(ip_addr) ||
10814 /^f[c-d]([0-9a-fA-F]{2,2}):/i.test(ip_addr) ||
10815 /^fe[8-9a-bA-B][0-9a-fA-F]:/i.test(ip_addr) ||
10816 /^ff([0-9a-fA-F]{2,2}):/i.test(ip_addr)
10817 }
10818
10819 // Check that a cookie IP is within the correct range depending on the active policy
10820 function checkCookieIp(cookieip, ip) {
10821 if (obj.args.cookieipcheck == 'none') return true; // 'none' - No IP address checking
10822 if (obj.args.cookieipcheck == 'strict') return (cookieip == ip); // 'strict' - Strict IP address checking, this can cause issues with HTTP proxies or load-balancers.
10823 if (require('ipcheck').match(cookieip, ip + '/24')) return true; // 'lax' - IP address need to be in the some range
10824 return (isPrivateAddress(cookieip) && isPrivateAddress(ip)); // 'lax' - If both IP addresses are private or loopback, accept it. This is needed because sometimes browsers will resolve IP addresses oddly on private networks.
10825 }
10826
10827 // Takes a formating string like "this {{{a}}} is an {{{b}}} example" and fills the a and b with input o.a and o.b
10828 function assembleStringFromObject(format, o) {
10829 var r = '', i = format.indexOf('{{{');
10830 if (i > 0) { r = format.substring(0, i); format = format.substring(i); }
10831 const cmd = format.split('{{{');
10832 for (var j in cmd) { if (j == 0) continue; i = cmd[j].indexOf('}}}'); r += o[cmd[j].substring(0, i)] + cmd[j].substring(i + 3); }
10833 return r;
10834 }
10835
10836 // Sync an account with an external user group.
10837 // Return true if the user was changed
10838 function syncExternalUserGroups(domain, user, userMemberships, userMembershipType) {
10839 var userChanged = false;
10840 if (user.links == null) { user.links = {}; }
10841
10842 // Create a user of memberships for this user that type
10843 var existingUserMemberships = {};
10844 for (var i in user.links) {
10845 if (i.startsWith('ugrp/') && (obj.userGroups[i] != null) && (obj.userGroups[i].membershipType == userMembershipType)) { existingUserMemberships[i] = obj.userGroups[i]; }
10846 }
10847
10848 // Go thru the list user memberships and create and add to any user groups as needed
10849 for (var i in userMemberships) {
10850 const membership = userMemberships[i];
10851 var ugrpid = 'ugrp/' + domain.id + '/' + obj.crypto.createHash('sha384').update(membership).digest('base64').replace(/\+/g, '@').replace(/\//g, '$');
10852 var ugrp = obj.userGroups[ugrpid];
10853 if (ugrp == null) {
10854 // This user group does not exist, create it
10855 ugrp = { type: 'ugrp', _id: ugrpid, name: membership, domain: domain.id, membershipType: userMembershipType, links: {} };
10856
10857 // Save the new group
10858 db.Set(ugrp);
10859 if (db.changeStream == false) { obj.userGroups[ugrpid] = ugrp; }
10860
10861 // Event the user group creation
10862 var event = { etype: 'ugrp', ugrpid: ugrpid, name: ugrp.name, action: 'createusergroup', links: ugrp.links, msgid: 69, msgArgv: [ugrp.name], msg: 'User group created: ' + ugrp.name, ugrpdomain: domain.id };
10863 parent.DispatchEvent(['*', ugrpid, user._id], obj, event); // Even if DB change stream is active, this event must be acted upon.
10864
10865 // Log in the auth log
10866 parent.authLog('https', userMembershipType.toUpperCase() + ': Created user group ' + ugrp.name);
10867 }
10868
10869 if (existingUserMemberships[ugrpid] == null) {
10870 // This user is not part of the user group, add it.
10871 if (user.links == null) { user.links = {}; }
10872 user.links[ugrp._id] = { rights: 1 };
10873 userChanged = true;
10874 db.SetUser(user);
10875 parent.DispatchEvent([user._id], obj, 'resubscribe');
10876
10877 // Notify user change
10878 var targets = ['*', 'server-users', user._id];
10879 var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msgid: 67, msgArgs: [user.name], msg: 'User group membership changed: ' + user.name, domain: domain.id };
10880 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.
10881 parent.DispatchEvent(targets, obj, event);
10882
10883 // Add a user to the user group
10884 ugrp.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
10885 db.Set(ugrp);
10886
10887 // Notify user group change
10888 var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: ugrp._id, name: ugrp.name, desc: ugrp.desc, action: 'usergroupchange', links: ugrp.links, msgid: 71, msgArgs: [user.name, ugrp.name], msg: 'Added user(s) ' + user.name + ' to user group ' + ugrp.name, addUserDomain: domain.id };
10889 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.
10890 parent.DispatchEvent(['*', ugrp._id, user._id], obj, event);
10891
10892 // Log in the auth log
10893 parent.authLog('https', userMembershipType.toUpperCase() + ': Adding ' + user.name + ' to user group ' + userMemberships[i] + '.');
10894 } else {
10895 // User is already part of this user group
10896 delete existingUserMemberships[ugrpid];
10897 }
10898 }
10899
10900 // Remove the user from any memberships they don't belong to anymore
10901 for (var ugrpid in existingUserMemberships) {
10902 var ugrp = obj.userGroups[ugrpid];
10903 parent.authLog('https', userMembershipType.toUpperCase() + ': Removing ' + user.name + ' from user group ' + ugrp.name + '.');
10904 if ((user.links != null) && (user.links[ugrpid] != null)) {
10905 delete user.links[ugrpid];
10906
10907 // Notify user change
10908 var targets = ['*', 'server-users', user._id, user._id];
10909 var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msgid: 67, msgArgs: [user.name], msg: 'User group membership changed: ' + user.name, domain: domain.id };
10910 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.
10911 parent.DispatchEvent(targets, obj, event);
10912
10913 db.SetUser(user);
10914 parent.DispatchEvent([user._id], obj, 'resubscribe');
10915 }
10916
10917 if (ugrp != null) {
10918 // Remove the user from the group
10919 if ((ugrp.links != null) && (ugrp.links[user._id] != null)) {
10920 delete ugrp.links[user._id];
10921 db.Set(ugrp);
10922
10923 // Notify user group change
10924 var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: ugrp._id, name: ugrp.name, desc: ugrp.desc, action: 'usergroupchange', links: ugrp.links, msgid: 72, msgArgs: [user.name, ugrp.name], msg: 'Removed user ' + user.name + ' from user group ' + ugrp.name, domain: domain.id };
10925 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.
10926 parent.DispatchEvent(['*', ugrp._id, user._id], obj, event);
10927 }
10928 }
10929 }
10930
10931 return userChanged;
10932 }
10933
10934 return obj;
10935};