2* @description MeshCentral web server
3* @author Ylian Saint-Hilaire
4* @copyright Intel Corporation 2018-2022
11/*jshint strict:false */
13/*jshint esversion: 6 */
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()
26// ExpressJS login sample
27// https://github.com/expressjs/express/blob/master/examples/auth/index.js
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; }; }
33// Construct a HTTP server object
34module.exports.CreateWebServer = function (parent, db, args, certificates, doneFunc) {
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.
58 // Setup WebAuthn / FIDO2
59 obj.webauthn = require('./webauthn.js').CreateWebAuthnModule();
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']);
68 obj.filespath = parent.filespath;
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);
79 obj.app.disable('x-powered-by');
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;
93 obj.relaySessionCount = 0;
94 obj.relaySessionErrorCount = 0;
96 obj.blockedAgents = 0;
97 obj.renderPages = null;
98 obj.renderLanguages = [];
99 obj.destroyedSessions = {}; // userid/req.session.x --> destroyed session time
101 // Web relay sessions
102 var webRelayNextSessionId = 1;
103 var webRelaySessions = {} // UserId/SessionId/Host --> Web Relay Session
104 var webRelayCleanupTimer = null;
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(); }
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;
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;
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 }); } }
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;
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);
187 // Decode a RSA certificate and hash the public key.
188 obj.webCertificateHashs[i] = parent.certificateOperations.getPublicKeyHashBinary(obj.parent.config.domains[i].certs.cert);
190 // This may be a ECDSA certificate, hash the entire cert.
191 obj.webCertificateHashs[i] = obj.webCertificateFullHashs[i];
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[''];
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' });
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.
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; });
236 // Get non-english web pages and emails
238 getEmailLanguageList();
240 // Setup DNS domain TLS SNI credentials
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; }
247 function TlsSniCallback(name, cb) {
248 var c = obj.tlsSniCredentials[name];
252 cb(null, obj.tlsSniCredentials['']);
256 function EscapeHtml(x) { if (typeof x == 'string') return x.replace(/&/g, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/'/g, '''); if (typeof x == 'boolean') return x; if (typeof x == 'number') return x; }
257 //function EscapeHtmlBreaks(x) { if (typeof x == "string") return x.replace(/&/g, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/'/g, ''').replace(/\r/g, '<br />').replace(/\n/g, '').replace(/\t/g, ' '); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
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.');
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.
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);
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
292 obj.userGroups[docs[i]._id] = docs[i]; // Get all user groups
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 };
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
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
332 //if (Object.keys(user.links).length == 0) { delete user.links; }
336 // We loaded the users, device groups and user group state, start the server
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; }
356 // Return statistics about this web server
357 obj.getStats = function () {
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
383 createMeshAgentCount: 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,
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,
410 obj.getAgentStats = function () { return obj.agentStats; }
415 httpWebSocketCount: 0,
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 }
441 function calcDelta(oldData, newData) { // Recursive function that computes the difference of all numbers
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]; } }
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 = [];
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; }
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; }
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);
480 fn(new Error('invalid password'));
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; }
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) { });
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]; }
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']; }
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;
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 = []; }
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
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]; }
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)) {
547 for (var i in userMemberships) {
549 for (var j in domain.ldapsyncwithusergroups.filter) {
550 if (userMemberships[i].indexOf(domain.ldapsyncwithusergroups.filter[j]) >= 0) { match = true; }
552 if (match) { g.push(userMemberships[i]); }
557 // LDAP user memberships sync is disabled, sync the user with empty membership
558 userMemberships = [];
561 // Get the email address for this LDAP user
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.
567 // Get the real name for this LDAP user
569 if (typeof domain.ldapuserrealname == 'string') {
570 if (domain.ldapuserrealname.indexOf('{{{') >= 0) { realname = assembleStringFromObject(domain.ldapuserrealname, xxuser); } else { realname = xxuser[domain.ldapuserrealname]; }
572 else { if (typeof xxuser['name'] == 'string') { realname = xxuser['name']; } }
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']; } }
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'); }
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));
594 // If there is a testing userid, use that
595 if (ldapHandlerFunc.ldapShortName) {
596 shortname = ldapHandlerFunc.ldapShortName;
597 userid = 'user/' + domain.id + '/' + shortname;
600 // Save the user image
601 if (userimage != null) { parent.db.Set({ _id: 'im' + userid, image: userimage }); } else { db.Remove('im' + userid); }
603 // Close the LDAP object
604 if (ldapHandlerFunc.ldapobj) { try { ldapHandlerFunc.ldapobj.close(); } catch (ex) { console.log(ex); } }
606 // Check if the user already exists
607 var user = obj.users[userid];
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; }
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.
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 };
629 // Add user to the group
630 ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
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);
641 // Check the user real name
642 if (realname) { user.realname = realname; }
644 // Check the user phone number
645 if (phonenumber) { user.phone = phonenumber; }
647 // Indicate that this user has a image
648 if (userimage != null) { user.flags = 1; }
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;
656 // Sync the user with LDAP matching user groups
657 if (syncExternalUserGroups(domain, user, userMemberships, 'ldap') == true) { userChanged = true; }
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);
666 var userChanged = false;
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; }
672 // Check if user email has changed
673 if (user.email && !email) { // email unset in ldap => unset
675 delete user.emailVerified;
677 } else if (user.email != email) { // update email
678 user['email'] = email;
679 user['emailVerified'] = true;
683 // Check the user real name
684 if (realname != user.realname) { user.realname = realname; userChanged = true; }
686 // Check the user phone number
687 if (phonenumber != user.phone) { user.phone = phonenumber; userChanged = true; }
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; }
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;
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;
704 // Synd the user with LDAP matching user groups
705 if (syncExternalUserGroups(domain, user, userMemberships, 'ldap') == true) { userChanged = true; }
707 // If the user changed, save the changes to the database here
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);
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);
721 if (domain.ldapoptions.url == 'test') {
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));
730 // The test user information is in the config.json, use it.
731 ldapHandler(null, xxuser);
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);
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) {
751 // PostgreSQL authentication successful
752 console.log('[WebServer] PostgreSQL authentication successful for:', meshUser._id);
754 // Store or update user in MeshCentral's user cache
755 obj.users[meshUser._id] = meshUser;
757 // Check if user is locked
758 if ((meshUser.siteadmin) && (meshUser.siteadmin != 0xFFFFFFFF) && (meshUser.siteadmin & 32) != 0) {
763 // Return successful authentication
764 return fn(null, meshUser._id);
766 // PostgreSQL authentication failed, try MeshCentral local users
767 tryLocalAuthentication();
773 // No JWT auth or it's not available, try local authentication
774 tryLocalAuthentication();
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'));
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);
794 fn(new Error('invalid password'), null, user.passhint);
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);
804 fn(new Error('invalid password'), null, user.passhint);
813 obj.restrict = function (req, res, next) {
814 console.log('restrict', req.url);
815 var domain = getDomain(req);
816 if (req.session.userid) {
819 req.session.messageid = 111; // Access denied.
820 res.redirect(domain.url + 'login');
825 // Check if the source IP address is in the IP list, return false if not.
826 function checkIpAddressEx(req, res, ipList, closeIfThis, redirectUrl) {
828 if (req.connection) {
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); } }
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) { } }
837 } catch (e) { console.log(e); } // Should never happen
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; }
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; }
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[''];
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
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; } }
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];
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 });
895 if (req.session.x) { clearDestroyedSessions(); obj.destroyedSessions[req.session.userid + '/' + req.session.x] = Date.now(); } // Destroy this session
898 parent.debug('web', 'handleLogoutRequest: success.');
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;
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);
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'); }
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;
931 // If this login occurred using a login token, no 2FA needed.
932 if ((loginOptions != null) && (typeof loginOptions.tokenName === 'string')) { return { twoFactorType: 'tokenlogin' }; }
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' }; } }
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' }; }
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; }
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; }
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; }
974 // See if SMS 2FA is available
975 var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
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));
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))));
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; };
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; }
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; }
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).
1009 obj.db.SetUser(user);
1010 parent.debug('web', 'checkUserOneTimePassword: success (email).');
1011 func(true, { twoFactorType: 'email' });
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).
1021 obj.db.SetUser(user);
1022 parent.debug('web', 'checkUserOneTimePassword: success (SMS).');
1023 func(true, { twoFactorType: 'sms' });
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).
1033 obj.db.SetUser(user);
1034 parent.debug('web', 'checkUserOneTimePassword: success (Messenger).');
1035 func(true, { twoFactorType: 'messenger' });
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');
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]; } }
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; }
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,
1076 publicKey: webAuthnKey.publicKey,
1077 prevCounter: webAuthnKey.counter,
1078 userHandle: Buffer.from(user._id, 'binary').toString('base64')
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' });
1090 parent.debug('web', 'checkUserOneTimePassword: fail (hardware).');
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' });
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;
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);
1123 // Find a matching OTP key
1125 for (var i = 0; i < user.otphkeys.length; i++) { if ((user.otphkeys[i].type === 2) && (user.otphkeys[i].keyid === keyId)) { match = true; } }
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' });
1136 parent.debug('web', 'checkUserOneTimePassword: fail (Yubikey).');
1144 parent.debug('web', 'checkUserOneTimePassword: fail (2).');
1148 // Return a U2F hardware key challenge
1149 function getHardwareKeyChallenge(req, domain, user, func) {
1151 if (req.session == null) { req.session = {}; } else { try { sec = parent.decryptSessionData(req.session.e); } catch (ex) { } }
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));
1171 // Remove the challenge if present
1172 if (sec.u2f != null) { delete sec.u2f; req.session.e = parent.encryptSessionData(sec); }
1174 parent.debug('web', 'getHardwareKeyChallenge: fail');
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));
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 = {}; }
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));
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;
1209 // Authenticate the user
1210 obj.authenticate(xusername, xpassword, domain, function (err, userid, passhint, loginOptions) {
1212 var user = obj.users[userid];
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)); }
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));
1228 // Check if two factor can be skipped
1229 const twoFactorSkip = checkUserOneTimePasswordSkip(domain, user, req, loginOptions);
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));
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)); }
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)); }
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)); }
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) : '')
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));
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');
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 });
1303 // Get the HTTPS port
1304 var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port if specified
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; }
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;
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)); }
1323 checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result, authData) {
1324 if (result == false) {
1325 var randomWaitTime = 0;
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));
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] });
1347 parent.debug('web', 'handleLoginRequest: 2FA token required');
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)); }
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 });
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)); }
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);
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)); }
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);
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'] });
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);
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);
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;
1442 delete req.session.passhint;
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.
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.
1459 // Decrypt any session data
1460 const sec = parent.decryptSessionData(req.session.e);
1461 sec.rtuser = xusername;
1462 sec.rtpass = xpassword;
1464 req.session.e = parent.encryptSessionData(sec);
1466 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1471 user.pastlogin = user.login;
1472 user.login = user.access = Math.floor(Date.now() / 1000);
1473 obj.db.SetUser(user);
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; }
1484 obj.parent.DispatchEvent(targets, obj, loginEvent);
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 });
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; }
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.
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;
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)); }
1532 parent.debug('web', 'handleLoginRequest: login ok (1)');
1533 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } // Temporary
1535 parent.debug('web', 'handleLoginRequest: login ok (2)');
1536 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
1541 function handleCreateAccountRequest(req, res, direct) {
1542 const domain = checkUserIpAddress(req, res);
1543 if (domain == null) { return; }
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) {
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)); }
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
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)); }
1575 // Always lowercase the email address
1576 if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
1578 // If the email is the username, set this here.
1579 if (domain.usernameisemail) { req.body.username = req.body.email; }
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)); }
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)); }
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)); }
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++; } }
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);
1621 // Check if this request is for an allows email domain
1622 if ((domain.newaccountemaildomains != null) && Array.isArray(domain.newaccountemaildomains)) {
1624 if (typeof req.body.email == 'string') { i = req.body.email.indexOf('@'); }
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)); }
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)); }
1643 // Check if we exceed the maximum number of user accounts
1644 obj.db.isMaxType(domain.limits.maxuseraccounts, 'user', domain.id, function (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)); }
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)); }
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)); }
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.
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.
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 };
1688 // Add user to the group
1689 ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
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);
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) {
1709 delete user.passtype;
1710 obj.db.SetUser(user);
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); }
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);
1719 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
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
1735 // Decrypt any session data
1736 const sec = parent.decryptSessionData(req.session.e);
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)); }
1759 // Authenticate the user
1760 obj.authenticate(sec.rtuser, sec.rtpass, domain, function (err, userid, passhint, loginOptions) {
1763 var user = obj.users[userid];
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)); }
1774 // Check if the password is the same as a previous one
1775 obj.checkOldUserPasswords(domain, user, req.body.rpassword1, function (result) {
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)); }
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; }
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; }
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); }
1803 user.passchange = user.access = nowSeconds;
1804 delete user.passtype;
1805 obj.db.SetUser(user);
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);
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);
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)); }
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
1854 // Always lowercase the email address
1855 if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
1857 // Get the email from the body or session.
1858 var email = req.body.email;
1859 if ((email == null) || (email == '')) { email = req.session.temail; }
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)); }
1868 obj.db.GetUserWithVerifiedEmail(domain.id, email, function (err, docs) {
1869 // Remove all accounts that start with ~ since they are special accounts.
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); }
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)); }
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) {
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) {
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));
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.
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] });
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)); }
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);
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)); }
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)); }
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);
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)); }
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)); }
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
1993 // Always lowercase the email address
1994 if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }
1996 // Get the email from the body or session.
1997 var email = req.body.email;
1998 if ((email == null) || (email == '')) { email = req.session.temail; }
2000 // Check if this request is for an allows email domain
2001 if ((domain.newaccountemaildomains != null) && Array.isArray(domain.newaccountemaildomains)) {
2003 if (typeof req.body.email == 'string') { i = req.body.email.indexOf('@'); }
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)); }
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)); }
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)); }
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.
2035 // Update the user and notify of user email address change
2036 var user = obj.users[req.session.cuserid];
2037 if (user.email != email) {
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);
2047 // Send the verification email
2048 domain.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
2050 // Send the response
2051 req.session.messageid = 2; // Email sent.
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)); }
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
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));
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));
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));
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));
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));
2096 parent.debug('web', 'handleCheckMailRequest: email verification success.');
2098 // Set the verified flag
2099 obj.users[user._id].emailVerified = true;
2100 user.emailVerified = true;
2101 obj.db.SetUser(user);
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);
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));
2111 // Send a notification
2112 obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: 'Email verified', value: user.email, nolog: 1, id: Math.random() });
2115 obj.parent.authLog('https', 'Verified email address ' + user.email + ' for user ' + user.name, { useragent: req.headers['user-agent'] });
2119 } else if (cookie.a == 2) {
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));
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) {
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);
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);
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.');
2151 obj.parent.authLog('https', 'Performed account reset for user ' + user.name);
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));
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));
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));
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
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)) : ''));
2190 render(req, res, getRenderPage('invite', req, domain), getRenderArgs({ messageid: 100 }, req, domain)); // Bad invitation code
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
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));
2205 // Set features we want to send to this page
2207 if (domain.allowsavingdevicecredentials === false) { features |= 1; }
2209 // Get the logged in user if present
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]; }
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]; }
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()]; }
2224 // No user login, exit now
2225 if (user == null) { res.sendStatus(401); return; }
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)) {
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];
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
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
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));
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
2268 req.query.node = null; // Bad nodeid
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; }
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];
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; }
2283 // Figure out the target port
2284 var port = 0, serverCredentials = false;
2285 if (page == 'ssh') {
2288 if (typeof node.sshport == 'number') { port = node.sshport; }
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
2302 if (typeof node.rdpport == 'number') { port = node.rdpport; }
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
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; } }
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));
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; }
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); }
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);
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
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.');
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];
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; }
2388 if (req.query.f) { showagents = parseInt(req.query.ag); }
2389 if (typeof showagents != 'number') { showagents = 0; }
2390 parent.debug('web', 'handleAgentInviteRequest using meshid.');
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];
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));
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;
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'));
2429 res.sendStatus(404);
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
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]; }
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)); }
2455 user = obj.users[req.session.userid];
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; }
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];
2472 // Remove user from the mesh
2473 if (mesh.links[deluser._id] != null) { delete mesh.links[deluser._id]; parent.db.Set(mesh); }
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);
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;
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));
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);
2496 } else if (i.startsWith('ugrp/')) {
2497 // Get the device group
2498 var ugroup = obj.userGroups[i];
2500 // Remove user from the user group
2501 if (ugroup.links[deluser._id] != null) { delete ugroup.links[deluser._id]; parent.db.Set(ugroup); }
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);
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
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 () { }); } }
2523 // Delete all files on the server for this account
2525 var deluserpath = obj.getServerRootFilePath(deluser);
2526 if (deluserpath != null) { obj.deleteFolderRec(deluserpath); }
2530 obj.db.Remove(deluser._id);
2531 delete obj.users[deluser._id];
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.');
2537 parent.debug('web', 'handleDeleteAccountRequest: auth failed.');
2538 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
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
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
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); }
2581 delete user.oldpasswords;
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 };
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); }
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);
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 });
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
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)); }
2634 // Get the current user
2635 var user = obj.users[req.session.userid];
2637 parent.debug('web', 'handlePasswordChangeRequest: user not found.');
2638 if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
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)); }
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) {
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)); }
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; }
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); }
2679 user.passchange = user.access = nowSeconds;
2680 delete user.passtype;
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 });
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);
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);
2719 // Check user membership in required groups
2720 if (groups.requiredGroups.length > 0) {
2722 for (var i in groups.requiredGroups) {
2723 if (groups.userMemberships.indexOf(groups.requiredGroups[i]) != -1) {
2725 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Membership to required group found: "${groups.requiredGroups[i]}"`);
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));
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;
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]); }
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]);
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(', ')}`);
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(', ')}`);
2769 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" No sync memberships found`);
2775 // Check if the user already exists
2776 const userid = 'user/' + domain.id + '/' + req.user.sid;
2777 var user = obj.users[userid];
2779 var newAccountAllowed = false;
2780 var newAccountRealms = null;
2782 if (domain.newaccounts === true) { newAccountAllowed = true; }
2783 if (obj.common.validateStrArray(domain.newaccountrealms)) { newAccountRealms = domain.newaccountrealms; }
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; }
2790 if (newAccountAllowed === true) {
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;
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 };
2814 // Add user to the group
2815 ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
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);
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);
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;
2841 obj.db.SetUser(user);
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);
2849 req.session.userid = userid;
2850 setSessionRandom(req);
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);
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));
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; }
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)
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;
2890 // Update db record for user if there are changes detected
2892 parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: CHANGED: USER: "${req.user.sid}" Updating user database entry`);
2893 obj.db.SetUser(user);
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);
2901 req.session.userid = userid;
2902 setSessionRandom(req);
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}"`);
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 }"`);
2915 parent.authLog('handleStrategyLogin', `LOGIN FAILED: REQUEST CONTAINS NO USER OR SID`);
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("&"); }
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('.','\\.');
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}:)` +
2933 `sitestyle=(\\d+)|` +
2938 `gotomesh=(.{64})|` +
2939 `gotouser=(.{0,64})|` +
2940 `gotougrp=(.{64})|` +
2945 `viewmode=(\\d+)(?=[\\&]|\\b)))`;
2947 var regex = new RegExp(regexstr);
2948 if(regex.test(relayState)){
2953 res.end('<html><head><meta http-equiv="refresh" content=0;url="' + url + '"></head><body></body></html>');
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; }
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; } }
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]; } }
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));
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; }
2982 if ((domain.sspi != null) && ((req.query.login == null) || (obj.parent.loginCookieEncryptionKey == null))) {
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.
2990 parent.debug('web', 'handleRootRequest: SSPI auth ok.');
2991 handleRootRequestEx(req, res, domain, direct);
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)) {
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);
3012 handleRootRequestEx(req, res, domain, direct);
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
3022 // Login using a different system
3023 handleRootRequestEx(req, res, domain, direct);
3027 function handleRootRequestEx(req, res, domain, direct) {
3028 var nologout = false, user = null;
3029 res.set({ 'Cache-Control': 'no-store' });
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***
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]);
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) {
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);
3072 // Store user in memory if not already present
3073 if (!obj.users[jwtUser._id]) {
3074 obj.users[jwtUser._id] = jwtUser;
3077 // Continue with authenticated request
3078 handleRootRequestExAuthenticated(req, res, domain, direct);
3080 // JWT validation failed
3081 parent.debug('web', 'handleRootRequestEx: JWT auth failed');
3082 handleRootRequestExAuthenticated(req, res, domain, direct);
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);
3098 parent.debug('web', 'handleRootRequestEx: cookie auth failed.');
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;
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 });
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.
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 };
3137 // Add user to the group
3138 ugroup.links[user2._id] = { userid: user2._id, name: user2.name, rights: 1 };
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);
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.');
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));
3172 passRequirements = domain.passwordrequirementsstr;
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];
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);
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)) : ''));
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***
3202 // Check if this is a locked account
3203 if ((dbGetFunc.user.siteadmin != null) && ((dbGetFunc.user.siteadmin & 32) != 0) && (dbGetFunc.user.siteadmin != 0xFFFFFFFF)) {
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***
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;
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;
3229 var logoutcontrols = {};
3230 if (obj.args.nousers != true) { logoutcontrols.name = user.name; }
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);
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);
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
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); }
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); }
3256 // Fetch the web state
3257 parent.debug('web', 'handleRootRequestEx: success.');
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.
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);
3269 // Custom user interface
3271 if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); }
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));
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
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
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'); }
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';
3313 // Refresh the session
3314 render(dbGetFunc.req, dbGetFunc.res, getRenderPage(uiViewMode, dbGetFunc.req, domain), getRenderArgs({
3315 authCookie: authCookie,
3316 authRelayCookie: authRelayCookie,
3318 currentNode: currentNode,
3319 logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27'),
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,
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
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);
3355 xdbGetFunc.req = req;
3356 xdbGetFunc.res = res;
3357 xdbGetFunc.user = user;
3358 obj.db.Get('ws' + user._id, xdbGetFunc);
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];
3368 parent.debug('web', 'handleRootRequestEx: sending 2FA challenge.');
3369 getHardwareKeyChallenge(req, domain, user, function (hwchallenge) { handleRootRequestLogin(req, res, domain, hwchallenge, passRequirements); });
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.');
3380 res.redirect(domain.url + getQueryPortion(req)); // BAD***
3382 var user = obj.users[docs[0]._id];
3384 parent.debug('web', 'handleRootRequestEx: password recover 2FA challenge.');
3385 getHardwareKeyChallenge(req, domain, user, function (hwchallenge) { handleRootRequestLogin(req, res, domain, hwchallenge, passRequirements); });
3387 parent.debug('web', 'handleRootRequestEx: password recover 2FA no user.');
3389 res.redirect(domain.url + getQueryPortion(req)); // BAD***
3395 handleRootRequestLogin(req, res, domain, '', passRequirements);
3399 // Return a list of server supported features for a given domain and user
3400 obj.getDomainUserFeatures = function (domain, user, req) {
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; }
3429 if (skip2factor == false) { features += 0x00040000; } // Force 2-factor auth
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 };
3479 function handleRootRequestLogin(req, res, domain, hardwareKeyChallenge, passRequirements) {
3480 parent.debug('web', 'handleRootRequestLogin()');
3482 // JWT authentication is handled by middleware - allow login page to render
3483 // The page will be accessible but actual authentication requires JWT
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
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.
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;
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'))
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; }
3508 // Encrypt the hardware key challenge state if needed
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)
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.
3528 // See if we support two-factor trusted cookies
3529 var twoFactorCookieDays = 30;
3530 if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
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);
3543 authStrategies.push('oidc');
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'); }
3551 // Custom user interface
3553 if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); }
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));
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;
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;
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;
3583 // Render the login page
3585 getRenderPage((domain.sitestyle >= 2) ? 'login2' : 'login', req, domain),
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,
3598 sessiontime: (args.sessiontime) ? args.sessiontime : 60, // Session time in minutes, 60 minutes is the default
3599 passRequirements: passRequirements,
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
3611 hkey: encodeURIComponent(hardwareKeyChallenge).replace(/'/g, '%27'),
3613 flashErrors: JSON.stringify(flashErrors).replace(/"/g, '\\"'),
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),
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'));
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);
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; } }
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 }); }
3660 handleLoginRequest(req, res, true); break;
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 };
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 });
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);
3687 handleLoginRequest(req, res, true); break;
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; }
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
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>';
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
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
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; }
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
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));
3750 res.redirect(domain.url + getQueryPortion(req));
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; }
3763 res.status(200).end(require('svg-captcha')(c.captcha, {}));
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))); });
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); });
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
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));
3801 render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(parent.configurationFiles['terms.txt'].toString()).split('\'').join('\\\''), logoutControls: encodeURIComponent('{}') }, req, domain));
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; }
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));
3820 render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(data).split('\'').join('\\\''), logoutControls: encodeURIComponent('{}') }, req, domain));
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));
3835 render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ logoutControls: encodeURIComponent('{}') }, req, domain));
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()');
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));
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)); }
3866 if ((req.session.userid != user1) && (req.session.userid != user2)) { res.sendStatus(404); return; }
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'); }
3874 // Setup other options
3875 var options = { webrtcconfig: webRtcConfig };
3876 if (typeof domain.meshmessengertitle == 'string') { options.meshMessengerTitle = domain.meshmessengertitle; } else { options.meshMessengerTitle = '!'; }
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]];
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'); }
3890 res.set({ 'Cache-Control': 'no-store' });
3891 render(req, res, getRenderPage('messenger', req, domain), getRenderArgs(options, req, domain));
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()');
3900 // Check if we are in maintenance mode
3901 if (parent.config.settings.maintenancemode != null) { res.sendStatus(404); return; }
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) { }
3909 var imagefile = 'images/messenger.png';
3910 if (domain.webpublicpath != null) {
3911 obj.fs.exists(obj.path.join(domain.webpublicpath, imagefile), function (exists) {
3913 // Use the domain logo picture
3914 try { res.sendFile(obj.path.join(domain.webpublicpath, imagefile)); } catch (ex) { res.sendStatus(404); }
3916 // Use the default logo picture
3917 try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
3920 } else if (parent.webPublicOverridePath) {
3921 obj.fs.exists(obj.path.join(obj.parent.webPublicOverridePath, imagefile), function (exists) {
3923 // Use the override logo picture
3924 try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, imagefile)); } catch (ex) { res.sendStatus(404); }
3926 // Use the default logo picture
3927 try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
3931 // Use the default logo picture
3932 try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
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');
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'));
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()');
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.",
3968 "display": "fullscreen",
3969 "orientation": "any",
3970 "theme_color": "#ffffff",
3971 "background_color": "#ffffff",
3973 "src": "pwalogo.png",
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
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; } }
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); }
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));
4005 render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'download2' : 'download', req, domain), getRenderArgs({ rootCertLink: getRootCertLink(domain), messageid: 2 }, req, domain));
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; }
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; }
4020 const user = obj.users[c.userid];
4021 if ((c == user)) { res.sendStatus(404); return; }
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
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; }
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
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);
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; }
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; }
4049 // Send the file back
4050 try { res.sendFile(obj.path.join(obj.filespath, 'tmp', c.f)); return; } catch (ex) { res.sendStatus(404); }
4053 // Handle logo request
4054 function handleLogoRequest(req, res) {
4055 const domain = checkUserIpAddress(req, res);
4056 if (domain == null) { return; }
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]);
4066 // Use the logo on file
4067 try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.titlepicture)); return; } catch (ex) { }
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); }
4078 // Use the default logo picture
4079 try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
4083 // Handle login logo request
4084 function handleLoginLogoRequest(req, res) {
4085 const domain = checkUserIpAddress(req, res);
4086 if (domain == null) { return; }
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]);
4096 // Use the logo on file
4097 try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.loginpicture)); return; } catch (ex) { res.sendStatus(404); }
4100 res.sendStatus(404);
4104 // Handle PWA logo request
4105 function handlePWALogoRequest(req, res) {
4106 const domain = checkUserIpAddress(req, res);
4107 if (domain == null) { return; }
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]);
4117 // Use the logo on file
4118 try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.pwalogo)); return; } catch (ex) { }
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); }
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); }
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.
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; }
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; }
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; }
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
4189 console.log('Server translation completed.');
4193 res.sendStatus(404);
4198 // Handle welcome image request
4199 function handleWelcomeImageRequest(req, res) {
4200 const domain = checkUserIpAddress(req, res);
4201 if (domain == null) { return; }
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]);
4212 // Use the configured logo picture
4213 try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.welcomepicture)); return; } catch (ex) { }
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) {
4221 // Use the domain logo picture
4222 try { res.sendFile(obj.path.join(domain.webpublicpath, imagefile)); } catch (ex) { res.sendStatus(404); }
4224 // Use the default logo picture
4225 try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
4228 } else if (parent.webPublicOverridePath) {
4229 obj.fs.exists(obj.path.join(obj.parent.webPublicOverridePath, imagefile), function (exists) {
4231 // Use the override logo picture
4232 try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, imagefile)); } catch (ex) { res.sendStatus(404); }
4234 // Use the default logo picture
4235 try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
4239 // Use the default logo picture
4240 try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
4244 // Download a session recording
4245 function handleGetRecordings(req, res) {
4246 const domain = checkUserIpAddress(req, res);
4247 if (domain == null) return;
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; }
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; }
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
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); }
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; }
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; }
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; }
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);
4292 obj.fs.stat(filefullpath, function (err, stats) {
4294 try { ws.close(); } catch (ex) { } // File does not exist
4296 obj.fs.open(filefullpath, 'r', function (err, fd) {
4298 // When data is received from the web socket
4299 ws.on('message', function (msg) {
4300 if (typeof msg != 'string') return;
4302 try { command = JSON.parse(msg); } catch (e) { return; }
4303 if ((command == null) || (typeof command.action != 'string')) return;
4304 switch (command.action) {
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); });
4315 // If error, do nothing
4316 ws.on('error', function (err) { try { ws.close(); } catch (ex) { } obj.fs.close(fd, function (err) { }); });
4318 // If the web socket is closed
4319 ws.on('close', function (req) { try { ws.close(); } catch (ex) { } obj.fs.close(fd, function (err) { }); });
4321 ws.send(JSON.stringify({ "action": "info", "name": req.query.file, "size": stats.size }));
4323 try { ws.close(); } catch (ex) { }
4330 // Serve the player page
4331 function handlePlayerRequest(req, res) {
4332 const domain = checkUserIpAddress(req, res);
4333 if (domain == null) { return; }
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));
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.
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; }
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);
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; }
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; }
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];
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);
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); }
4399 if (doc.viewOnly === true) { cookie.vo = 1; }
4400 handleSharingRequestEx(req, res, domain, cookie);
4404 res.sendStatus(404); return;
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; }
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; }
4417 // Search for the device share public identifier, expire message.
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; }
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];
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; }
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; }
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);
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 + '\".');
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);
4459 if (obj.args.allowhighqualitydesktop !== false) { features2 += 1; } // Enable AllowHighQualityDesktop (Default true)
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));
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);
4484 // Perform redirection
4485 res.redirect(domain.redirects[urlName] + urlArgs + getQueryPortion(req));
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) };
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
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); } });
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; }
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) { }
4552 // Send out the default english MeshCommander
4553 try { res.sendFile(obj.parent.path.join(parent.webPublicPath, 'commander.htm')); } catch (ex) { }
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; }
4562 var authUserid = null;
4563 if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
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
4574 if (authUserid == null) { res.sendStatus(401); return; }
4575 if ((fields == null) || (fields.attrib == null) || (fields.attrib.length != 1)) { res.sendStatus(404); return; }
4578 const user = obj.users[authUserid];
4579 if (user == null) { res.sendStatus(401); return; } // Check this user exists
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) { }
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; }
4604 var authUserid = null;
4605 if ((req.session != null) && (typeof req.session.userid == 'string')) { authUserid = req.session.userid; }
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
4616 if (authUserid == null) { res.sendStatus(401); return; }
4617 if ((fields == null) || (fields.attrib == null) || (fields.attrib.length != 1)) { res.sendStatus(404); return; }
4620 const user = obj.users[authUserid];
4621 if (user == null) { res.sendStatus(401); return; } // Check this user exists
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 });
4631 //try { obj.fs.unlinkSync(file.path); } catch (e) { } // TODO: Remove this file after 30 minutes.
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
4654 if (authUserid == null) { res.sendStatus(401); return; }
4657 const user = obj.users[authUserid];
4658 if ((user == null) || (user.siteadmin & 8) == 0) { res.sendStatus(401); return; } // Check if we have file rights
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; }
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) {
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) { }
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 () {
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
4691 })(xfile.fullpath, names[i], filedata);
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() });
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
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) { }
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
4721 obj.parent.DispatchEvent([user._id], obj, 'updatefiles'); // Fire an event causing this user to update this files
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) { }
4732 // Send a notification
4733 obj.parent.DispatchEvent([user._id], obj, { action: 'notify', value: "Disk quota exceed", nolog: 1, id: Math.random() });
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
4754 if (authUserid == null) { res.sendStatus(401); return; }
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
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; }
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));
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) { }
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 });
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) { }); });
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'); }
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;
4805 // Compute user consent
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
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
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);
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.
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');
4859 // Subscribe to user changes for some groups
4860 for (var i in user.groups) { subscriptions.push('server-users:' + i); }
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;
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 + '.');
4875 try { ws._socket.setKeepAlive(true, 240000); } catch (ex) { } // Set TCP keep alive
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);
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; }
4888 // Check what connectivity is available for this node
4889 var state = parent.GetConnectivityState(req.query.host);
4891 if (!state || state.connectivity == 0) { parent.debug('web', 'ERR: No routing possible (1)'); try { ws.close(); } catch (e) { } return; } else { conn = state.connectivity; }
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);
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);
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
4919 // Check user or device group recording
4920 if ((typeof domain.sessionrecording == 'object') && ((domain.sessionrecording.onlyselectedusers === true) || (domain.sessionrecording.onlyselecteddevicegroups === true))) {
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
4929 // Check user recording
4930 if (domain.sessionrecording.onlyselectedusers === true) {
4931 if ((user.flags != null) && ((user.flags & 2) != 0)) { record = true; } // Record the session
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);
4946 try { obj.fs.mkdirSync(parent.recordpath); } catch (e) { }
4947 recFullFilename = obj.path.join(parent.recordpath, recFilename);
4949 var fd = obj.fs.openSync(recFullFilename, 'w');
4951 // Write the recording file header
4952 parent.debug('relay', 'Relay: Started recording to file: ' + recFullFilename);
4954 magic: 'MeshCentralRelaySession',
4957 username: user.name,
4959 ipaddr1: req.clientIp,
4960 time: new Date().toLocaleString(),
4961 protocol: (req.query.p == 2) ? 101 : 100,
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
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 + '.');
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
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;
4988 // Setup a new CIRA channel
4989 if ((port == 16993) || (port == 16995)) {
4991 var ser = new SerialTunnel();
4992 var chnl = parent.mpsserver.SetupChannel(ciraconn, port);
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
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
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) { } }
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';
5011 tlsoptions.minVersion = 'TLSv1';
5013 var tlsock = obj.tls.connect(tlsoptions, function () { parent.debug('webrelay', "CIRA Secure TLS Connection"); ws._socket.resume(); });
5015 tlsock.setEncoding('binary');
5016 tlsock.on('error', function (err) { parent.debug('webrelay', "CIRA TLS Connection Error", err); });
5018 // Decrypted tunnel from TLS communication to be forwarded to websocket
5019 tlsock.on('data', function (data) {
5021 if (ws.interceptor) { data = ws.interceptor.processAmtData(data); } // Run data thru interceptor
5022 try { ws.send(data); } catch (ex) { }
5025 // If TLS is on, forward it through TLSSocket
5026 ws.forwardclient = tlsock;
5027 ws.forwardclient.xtls = 1;
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) { } }
5034 ws.forwardclient.onData = function (ciraconn, data) {
5035 // Run data thru interceptor
5036 if (ws.interceptor) { data = ws.interceptor.processAmtData(data); }
5038 if (data.length > 0) {
5039 if (ws.logfile == null) {
5040 try { ws.send(data); } catch (e) { }
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
5048 // TODO: Flow control? (Dont' really need it with AMT, but would be nice)
5049 ws.forwardclient.onSendOk = function (ciraconn) { };
5054 ws.forwardclient = parent.mpsserver.SetupChannel(ciraconn, port);
5055 ws.forwardclient.xtls = 0;
5056 ws._socket.resume();
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) { } }
5063 ws.forwardclient.onData = function (ciraconn, data) {
5064 //parent.debug('webrelaydata', 'Relay CIRA data to WS', data.length);
5066 // Run data thru interceptor
5067 if (ws.interceptor) { data = ws.interceptor.processAmtData(data); }
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) { }
5074 // Log to recording file
5075 obj.meshRelayHandler.recordingEntry(ws.logfile, 2, 0, data, function () { try { ws.send(data); } catch (ex) { console.log(ex); } });
5080 // TODO: Flow control? (Dont' really need it with AMT, but would be nice)
5081 ws.forwardclient.onSendOk = function (ciraconn) { };
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'); }
5091 if (ws.interceptor) { data = ws.interceptor.processBrowserData(data); } // Run data thru interceptor
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) { }
5098 // Log to recording file
5099 obj.meshRelayHandler.recordingEntry(ws.logfile, 2, 2, data, function () { try { ws.forwardclient.write(data); } catch (ex) { } });
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.');
5108 // Log the disconnection
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));
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);
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;
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);
5154 // If the web socket is closed, close the associated TCP connection.
5155 ws.on('close', function () {
5156 parent.debug('webrelay', 'Websocket relay closed.');
5158 // Log the disconnection
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;
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);
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;
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);
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
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;
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 + '.');
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');
5226 if (typeof msg == 'string') { msg = Buffer.from(msg, 'binary'); }
5227 if (ws.interceptor) { msg = ws.interceptor.processBrowserData(msg); } // Run data thru interceptor
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) { }
5234 // Log to recording file
5235 obj.meshRelayHandler.recordingEntry(ws.logfile, 2, 2, msg, function () { try { ws.forwardclient.write(msg); } catch (ex) { } });
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
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));
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);
5253 if (ws.forwardclient) { try { ws.forwardclient.destroy(); } catch (e) { } }
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);
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
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));
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);
5295 if (ws.forwardclient) { try { ws.forwardclient.destroy(); } catch (e) { } }
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);
5324 // Compute target port
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;
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();
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';
5342 tlsoptions.minVersion = 'TLSv1';
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();
5350 ws.forwardclient.setEncoding('binary');
5351 ws.forwardclient.xstate = 0;
5352 ws.forwardclient.forwardwsocket = ws;
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')); }
5362 if (ws.interceptor) { data = ws.interceptor.processAmtData(data); } // Run data thru interceptor
5363 if (ws.logfile == null) {
5365 try { ws.send(data); } catch (e) { }
5367 // Log to recording file
5368 obj.meshRelayHandler.recordingEntry(ws.logfile, 2, 0, data, function () { try { ws.send(data); } catch (e) { } });
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) { }
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) { }
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 }); }
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();
5398 // Log the connection
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);
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);
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);
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
5444 // When data is received from the web socket, echo it back
5445 ws.on('message', function (data) {
5446 if (typeof data == 'string') {
5449 try { cmd = JSON.parse(data); } catch (ex) { }
5450 if ((cmd == null) || (cmd.action != 'download') || (cmd.sub == null)) return;
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)
5458 callback.xws = this;
5459 obj.fs.open(this.xfilepath + '.part', 'w', callback);
5465 if (data.length < 4) return;
5466 var flags = data.readInt32BE(0);
5467 if ((data.length > 4)) {
5469 this.xfilelen += (data.length - 4);
5471 var callback = function onFileDataWritten(err, bytesWritten, buffer) {
5472 if (onFileDataWritten.xflags & 1) {
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;
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) { }
5484 try { onFileDataWritten.xws.send(JSON.stringify({ action: 'download', sub: 'ack', id: onFileDataWritten.xws.xid })); } catch (ex) { } // Ask for a directory (test)
5487 callback.xws = this;
5488 callback.xflags = flags;
5489 obj.fs.write(this.xfile, data, 4, data.length - 4, callback);
5494 parent.debug('web', "Completed downloads of agent dumpfile, " + this.xfilelen + " bytes.");
5496 obj.fs.close(this.xfile, function (err) { });
5497 obj.fs.rename(this.xfilepath + '.part', this.xfilepath, function (err) { });
5500 this.send(JSON.stringify({ action: 'markcoredump' })); // Ask to delete the core dump file
5501 try { this.close(); } catch (ex) { }
5504 this.send(JSON.stringify({ action: 'download', sub: 'ack', id: this.xid })); // Ask for a directory (test)
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] + '.'); });
5513 // If closed, do nothing
5514 ws.on('close', function (req) {
5516 obj.fs.close(this.xfile, function (err) { });
5517 obj.fs.unlink(this.xfilepath + '.part', function (err) { }); // Remove a partial file
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
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); }
5533 try { ws.send(data); } catch (e) { console.log(e); }
5537 // If error, do nothing.
5538 ws.on('error', function (err) { console.log('Echo server error from ' + req.clientIp + ', ' + err.toString().split('\r')[0] + '.'); });
5540 // If closed, do nothing
5541 ws.on('close', function (req) { });
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
5557 // 2FA event subscription
5558 obj.parent.AddEventDispatch(['2fadev-' + cookie.s], ws);
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) { }
5568 try { ws.send(JSON.stringify({ approved: false })); } catch (ex) { }
5572 // We do not accept any data on this connection.
5573 ws.on('message', function (data) { this.close(); });
5575 // If error, do nothing.
5576 ws.on('error', function (err) { });
5578 // If closed, unsubscribe
5579 ws.on('close', function (req) { obj.parent.RemoveAllEventDispatch(this); });
5581 // Perform push notification to device
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) {
5589 try { ws.send(JSON.stringify({ sent: true, code: code })); } catch (ex) { }
5591 try { ws.send(JSON.stringify({ sent: false })); } catch (ex) { }
5594 } catch (ex) { console.log(ex); }
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) {
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]); }
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;
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); }
5616 obj.fs.rmdirSync(path);
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);
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'))) {
5631 // Read the data, we need to get the arg field
5633 req.on('data', function (chunk) { eventData += chunk; });
5634 req.on('end', function () {
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">');
5639 var nodeid = eventData.substring(i + 30, i + 30 + 64);
5640 if (nodeid.length == 64) {
5641 var nodekey = 'node/' + domain.id + '/' + nodeid;
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');
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)) {
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];
5660 // Update the database
5661 var oldname = node.host;
5662 node.host = amthost;
5663 obj.db.Set(obj.cleanDevice(node));
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 };
5668 // Remove the Intel AMT password before eventing this.
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;
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);
5681 if (parent.amtEventHandler) { parent.amtEventHandler.handleAmtEvent(eventData, nodeid, amthost); }
5694 } catch (e) { console.log(e); }
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);
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; }
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
5716 const archive = require('archiver')('zip', { level: 9 }); // Sets the compression method to maximum.
5718 // Good practice to catch this error explicitly
5719 archive.on('error', function (err) { throw err; });
5721 // Set the archive name
5722 res.attachment((domain.title ? domain.title : 'MeshCentral') + '-Backup-' + new Date().toLocaleDateString().replace('/', '-').replace('/', '-') + '.zip');
5724 // Pipe archive data to the file
5727 // Append files from a glob pattern
5728 archive.directory(obj.parent.datapath, false);
5730 // Finalize the archive (ie we are done appending files but streams have to finish yet)
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; }
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
5752 if (authUserid == null) { res.sendStatus(401); return; }
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
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);
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; }
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; }
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
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; }
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; }
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) + ';');
5792 // Get the agent filename
5793 var meshagentFilename = 'meshagent';
5794 if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) { meshagentFilename = domain.agentcustomization.filename; }
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; }
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) { }
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) { }
5825 try { res.sendStatus(404); } catch (ex) { }
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); }
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; }
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; }
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; }
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.
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; }
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);
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; }
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'; }
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'; }
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); }
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); }
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'; }
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); }
5919 if (domain.agentTranslations != null) { meshsettings += 'translation=' + domain.agentTranslations + '\r\n'; } // Translation strings, not for MeshCentral Assistant
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 });
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 });
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
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 ';
5943 if (domain.agentnoproxy === true) {
5944 cmdoptions.wgetoptionshttp += '--no-proxy ';
5945 cmdoptions.wgetoptionshttps += '--no-proxy ';
5946 cmdoptions.curloptionshttp += '--noproxy \'*\' ';
5947 cmdoptions.curloptionshttps += '--noproxy \'*\' ';
5949 for (var i in cmdoptions) { data = data.split('{{{' + i + '}}}').join(cmdoptions[i]); }
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
5955 // Send meshcmd for a specific platform back
5956 var agentid = parseInt(req.query.meshcmd);
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;
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;
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;
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;
5990 if (argentInfo.signedMeshCmdPath != null) {
5991 // If we have a pre-signed MeshCmd, send that.
5992 res.sendFile(argentInfo.signedMeshCmdPath);
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 });
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];
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; }
6006 // Download tools using a cookie
6007 if (c.download == req.query.meshaction) {
6008 if (req.query.meshaction == 'winrouter') {
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) { } }
6017 } else if (req.query.meshaction == 'winassistant') {
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) { } }
6026 } else if (req.query.meshaction == 'macrouter') {
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) { } }
6039 // Check if the cookie authenticates a user
6041 if (c.userid != null) {
6042 user = obj.users[c.userid];
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);
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
6057 if (user == null) { try { res.sendStatus(404); } catch (ex) { } return; }
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];
6066 // Create the meshaction.txt file for meshcmd.exe
6068 action: req.query.meshaction,
6070 remoteName: node.name,
6071 remoteNodeId: node._id,
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
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'; }
6085 setContentDispositionHeader(res, 'application/octet-stream', 'meshaction.txt', null, 'meshaction.txt');
6086 res.send(JSON.stringify(meshaction, null, ' '));
6089 } else if (req.query.meshaction == 'generic') {
6091 username: user.name,
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
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, ' '));
6104 } else if (req.query.meshaction == 'winrouter') {
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) { } }
6113 } else if (req.query.meshaction == 'winassistant') {
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) { } }
6122 } else if (req.query.meshaction == 'macrouter') {
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) { } }
6132 try { res.sendStatus(401); } catch (ex) { }
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; }
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;
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;
6155 try { res.sendStatus(404); } catch (ex) { } return;
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); }
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>';
6169 var coreDumpPath = obj.path.join(parent.datapath, '..', 'meshcentral-coredumps');
6170 if (obj.fs.existsSync(coreDumpPath)) {
6171 var files = obj.fs.readdirSync(coreDumpPath);
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));
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)
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(' ') + '</td><td>' + d.agentinfo.desc.split(' ').join(' ') + '</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>';
6206 response += '</table><a href="' + req.originalUrl.split('?')[0] + (req.query.key ? ('?key=' + encodeURIComponent(req.query.key)) : '') + '">Mesh Agents</a></body></html>';
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(' ') + '</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>';
6219 response += '</table><a href="' + req.originalUrl.split('?')[0] + (req.query.key ? ('?key=' + encodeURIComponent(req.query.key)) : '') + '">Mesh Agents</a></body></html>';
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));
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');
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(' ') + '</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>'; }
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>';
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>';
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);
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;
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
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; }
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; }
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; }
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; }
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; }
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; }
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();
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; }
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'; }
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); }
6358 if (domain.agentTranslations != null) { meshsettings += 'translation=' + domain.agentTranslations + '\r\n'; }
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; });
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);
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);
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);
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);
6392 // Set the agent download including the mesh name.
6393 setContentDispositionHeader(res, 'application/octet-stream', meshfilename, null, 'MeshAgent.zip');
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();
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();
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();
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(); });
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) });
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; }
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; }
6464 // Fetch the mesh object
6465 var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.id];
6466 if (mesh == null) { return null; }
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; }
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();
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; }
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'; }
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); }
6506 if (domain.agentTranslations != null) { meshsettings += 'translation=' + domain.agentTranslations + '\r\n'; }
6507 return meshsettings;
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; }
6516 var meshsettings = getMshFromRequest(req, res, domain);
6517 if (meshsettings == null) { res.sendStatus(401); return; }
6519 // Get the agent filename
6520 var meshagentFilename = 'meshagent';
6521 if ((domain.agentcustomization != null) && (typeof domain.agentcustomization.filename == 'string')) { meshagentFilename = domain.agentcustomization.filename; }
6523 setContentDispositionHeader(res, 'application/octet-stream', meshagentFilename + '.msh', null, 'meshagent.msh');
6524 res.send(meshsettings);
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; }
6537 obj.db.Get(req.query.id, function (err, docs) {
6538 if (docs.length != 1) {
6539 res.sendStatus(401);
6543 // Check if we have right to this node
6544 if (obj.GetNodeRights(user, node.meshid, node._id) == 0) { res.sendStatus(401); return; }
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; }
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);
6568 prevState = docs[i].power;
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);
6576 if (docs[i].oldPower != null) {
6577 xevents.push('\"' + timedoc.toISOString() + '\",\"' + localTime + '\",' + docs[i].power + ',' + docs[i].oldPower);
6579 xevents.push('\"' + timedoc.toISOString() + '\",\"' + localTime + '\",' + docs[i].power);
6583 res.send(xevents.join('\r\n'));
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; }
6598 parent.pluginHandler.handleAdminReq(req, res, user, obj);
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; }
6608 parent.pluginHandler.handleAdminPostReq(req, res, user, obj);
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; }
6618 parent.pluginHandler.refreshJS(req, res);
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) } });
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.
6656 if (obj.useNodeDefaultTLSCiphers) {
6657 ciphers = require("tls").DEFAULT_CIPHERS;
6660 if (obj.tlsCiphers) {
6661 ciphers = obj.tlsCiphers;
6662 if (Array.isArray(obj.tlsCiphers)) {
6663 ciphers = obj.tlsCiphers.join(":");
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) } });
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; }
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) } });
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) } });
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
6710 obj.app.set('trust proxy', obj.args.trustedproxy);
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]; } });
6718 else if (typeof obj.args.tlsoffload == 'object') {
6719 // Reverse proxy should add the "X-Forwarded-*" headers
6721 obj.app.set('trust proxy', obj.args.tlsoffload);
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]; } });
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');
6735 // Setup the cookie session
6736 const sessionOptions = {
6737 name: 'xid', // Recommended security practice to not use the default cookie name
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')
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) {
6751 if (request.session && !request.session.save) {
6752 request.session.save = function (cb) {
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)) {
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'
6764 response.setHeader('Accept-CH', secCH.join(', '));
6765 response.setHeader('Critical-CH', secCH.join(', '));
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
6776 '/createaccount', // Allow for first admin account creation only
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) ||
6802 // Skip if user is already authenticated via session
6803 if (req.session && req.session.userid) {
6807 // Check for JWT token
6808 if (obj.parent.jwtAuth) {
6809 const token = obj.parent.jwtAuth.extractToken(req);
6812 obj.parent.jwtAuth.validateToken(token, function (jwtUser) {
6814 // JWT authenticated successfully
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}`);
6824 return; // Wait for callback
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; }
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;
6851 // Legacy session without a random, add one.
6852 setSessionRandom(req);
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;
6869 // Useful for debugging reverse proxy issues
6870 parent.debug('httpheaders', req.method, req.url, req.headers);
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);
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++;
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;
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;
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; }
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)))
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();
6925 req.clientIp = ipex;
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]; }
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.
6935 req.clientIp = ipex;
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; }
6943 // Get the domain for this request
6944 const domain = req.xdomain = getDomain(req);
6945 parent.debug('webrequest', '(' + req.clientIp + ') ' + req.url);
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; }
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) : '';
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; }
6964 // If using duo add apihostname to CSP
6966 if ((typeof domain.duo2factor == 'object') && (typeof domain.duo2factor.apihostname == 'string')) {
6967 duoSrc = domain.duo2factor.apihostname;
6970 // Finish setup security 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"
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'; } }
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]; } }
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 = {}; }
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; }
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; } }
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);
7003 res.send = function sendWrapper(...args) {
7005 send.apply(this, args);
7007 console.error(`Error in res.send | ${err.code} | ${err.message} | ${res.stack}`);
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.'); }
7016 // Continue processing the request
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; }
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)))
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();
7039 req.clientIp = ipex;
7042 req.clientIp = ipex;
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');
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 };
7061 setupAllDomainAuthStrategies().then(() => finalizeWebserver());
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]);
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]);
7078 function setupHTTPHandlers() {
7079 // Setup all HTTP handlers
7080 if (parent.pluginHandler != null) {
7081 parent.pluginHandler.callHook('hook_setupHttpHandlers', obj, parent);
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);
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);
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) { }
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) { }
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);
7156 obj.parent.jwtAuth.validateToken(token, function (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);
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);
7168 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth' })); } catch (ex) { }
7169 try { ws.close(); } catch (ex) { }
7173 return; // Exit early, callback will handle the rest
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
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.
7185 obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws1, req1, obj.args, domain, user, authData); // User is authenticated
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
7210 obj.meshRelayHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Normal relay 1-to-1
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) { }
7220 obj.meshRelayHandler.CreateLocalRelay(obj, ws1, req1, domain, user, cookie); // Local relay
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() }));
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);
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;
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');
7259 if (!token && req.headers.authorization) {
7260 token = req.headers.authorization.split(' ')[1];
7261 console.log('[CANVAS] Extracted token from Authorization header');
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' }));
7274 console.log('[CANVAS] Token received:', token.substring(0, 20) + '...');
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';
7283 if (TEMP_TEST_MODE) {
7284 console.log('[CANVAS] ⚠️ TEST MODE: Bypassing JWT validation');
7286 _id: 'user//test-user',
7288 tenant_id: '00000000-0000-0000-0000-000000000001',
7291 handleAuthenticatedConnection(meshUser);
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' }));
7303 obj.parent.jwtAuth.validateToken(token, function (meshUser) {
7305 console.error('[CANVAS] Invalid JWT token or user not found');
7306 ws.send(JSON.stringify({ type: 'error', message: 'Authentication failed' }));
7310 handleAuthenticatedConnection(meshUser);
7313 // Handle authenticated connection (extracted to reuse for test mode)
7314 function handleAuthenticatedConnection(meshUser) {
7316 const userId = meshUser._id;
7317 const tenantId = meshUser.tenant_id || meshUser.tenantId;
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}`);
7322 // CRITICAL: Resume socket immediately - Express-ws might pause it
7324 if (ws._socket && typeof ws._socket.resume === 'function') {
7325 console.log('[CANVAS] Resuming socket...');
7326 ws._socket.resume();
7329 console.error('[CANVAS] Error resuming socket:', e.message);
7332 // Send immediate test message to verify WebSocket works
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');
7338 console.error('[CANVAS] Error sending test message:', e.message);
7341 // Phase 2: Create peer object for desktop multiplexor integration
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
7353 // Mark as browser viewer (required by desktop multiplexor)
7354 if (!peer.req.query) peer.req.query = {};
7355 peer.req.query.browser = true;
7358 ws.canvasPeer = peer;
7360 // Phase 2: Setup message handler for input events from dashboard
7361 ws.on('message', function(msg) {
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);
7371 // Handle JSON control messages
7372 const data = JSON.parse(msg);
7373 parent.debug('web', `[CANVAS] Control message: ${data.type}`);
7375 // Handle ping/pong for connection testing
7376 if (data.type === 'ping') {
7377 ws.send(JSON.stringify({
7379 timestamp: Date.now(),
7380 server: 'meshcentral'
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}`);
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}`);
7400 console.error('[CANVAS] Message parse error:', err.message);
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}`);
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');
7419 ws.on('error', function(err) {
7420 console.error('[CANVAS] WebSocket error:', err.message);
7423 // Send initial connection success message with error handling
7425 const connectedMsg = JSON.stringify({
7430 message: 'Canvas desktop endpoint connected (Phase 2 - Screen Streaming)',
7432 capabilities: ['ping', 'auth', 'screen', 'input']
7435 ws.send(connectedMsg, function(err) {
7437 console.error('[CANVAS] Failed to send connected message:', err.message);
7439 console.log('[CANVAS] Connected message sent successfully');
7443 console.log(`[CANVAS] Phase 2 - Initial handshake queued for dashboard`);
7445 console.error('[CANVAS] ERROR sending connected message:', err.message);
7448 // Phase 2: Connect to desktop multiplexor
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);
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' }));
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' }));
7466 let deskMultiplexor = obj.desktoprelays[nodeId];
7467 console.log('[CANVAS] Existing multiplexor for node:', deskMultiplexor ? 'Found' : 'Not found');
7469 if (deskMultiplexor == null || deskMultiplexor == 1) {
7470 console.log('[CANVAS] Creating new desktop multiplexor for node:', nodeId);
7472 // Mark as pending creation
7473 obj.desktoprelays[nodeId] = 1;
7475 // Create new multiplexor
7476 const CreateDesktopMultiplexor = obj.meshDesktopMultiplexHandler.CreateDesktopMultiplexor;
7477 console.log('[CANVAS] CreateDesktopMultiplexor function loaded:', typeof CreateDesktopMultiplexor);
7479 CreateDesktopMultiplexor(
7480 obj, // Use webserver object as parent
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;
7491 // Add ourselves as a viewer
7492 multiplexor.addPeer(peer);
7493 console.log('[CANVAS] Added as peer to desktop multiplexor');
7495 // Resume socket traffic
7496 if (ws._socket && ws._socket.resume) {
7497 ws._socket.resume();
7500 // Send ready message
7501 ws.send(JSON.stringify({ type: 'ready', message: 'Desktop streaming active' }));
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' }));
7511 console.log('[CANVAS] Using existing desktop multiplexor for node:', nodeId);
7512 peer.deskMultiplexor = deskMultiplexor;
7514 // Add ourselves as a viewer to existing multiplexor
7515 deskMultiplexor.addPeer(peer);
7516 console.log('[CANVAS] Added as peer to existing desktop multiplexor');
7518 // Resume socket traffic
7519 if (ws._socket && ws._socket.resume) {
7520 ws._socket.resume();
7523 // Send ready message
7524 ws.send(JSON.stringify({ type: 'ready', message: 'Desktop streaming active' }));
7527 console.log(`[CANVAS] Phase 2 - Complete setup for user ${userId} to node ${nodeId}`);
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 }));
7533 } // end handleAuthenticatedConnection
7535 // End of Custom Canvas Desktop Endpoint
7536 // ========================================================================
7538 obj.app.get(url + 'invite', handleInviteRequest);
7539 obj.app.post(url + 'invite', obj.bodyParser.urlencoded({ extended: false }), handleInviteRequest);
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);
7547 // New account CAPTCHA request
7548 if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
7549 obj.app.get(url + 'newAccountCaptcha.ashx', handleNewAccountCaptchaRequest);
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);
7558 // Setup IP-KVM relay if supported
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);
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);
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); }
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); }
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);
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);
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); }
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.`);
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(); });
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).
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>');
7632 domain.passport.authenticate('twitter-' + domain.id, { failureRedirect: domain.url })(req, res, function (err) { if (err != null) { console.log(err); } next(); });
7634 }, handleStrategyLogin);
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);
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);
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);
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);
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);
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).
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>');
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; }
7688 }, handleStrategyLogin);
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);
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;
7706 redirectPath = url + 'auth-oidc-callback';
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
7718 }, handleStrategyLogin);
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/',''))
7730 domain.passport.authenticate('saml-' + domain.id, { failureRedirect: domain.url, failureFlash: true })(req, res, next);
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);
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);
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);
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);
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);
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
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) : '')
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) {
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);
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
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;
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] });
7866 res.redirect(domain.url + getQueryPortion(req));
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
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);
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) : '')
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();
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));
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); } } }
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']);
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); }
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); }
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); }
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); }
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
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)); }
7967 // Setup the alternative agent-only port
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); }
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
7983 obj.meshRelayHandler.CreateMeshRelay(obj, ws1, req1, domain, user, cookie); // Normal relay 1-to-1
7988 // Allows agents to transfer files
7989 obj.agentapp.ws(url + 'devicefile.ashx', function (ws, req) { obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, ws, null, req, domain); });
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
7994 // Setup agent downloads for meshcore updates
7995 obj.agentapp.get(url + 'meshagents', obj.handleMeshAgentRequest);
7997 // Setup agent file downloads
7998 obj.agentapp.get(url + 'agentdownload.ashx', handleAgentDownloadFile);
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); })
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();
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');
8018 // Decode the relay cookie
8019 if (req.query.c == null) { res.sendStatus(404); return; }
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; }
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; }
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;
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; }
8063 // No session identifier was setup, exit now
8064 if (webSessionId == null) { res.sendStatus(404); return; }
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; }
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.
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) : '') + '/');
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;
8096 // Check if this host is older then oldest so far
8097 if (oldestRelayTime > xrelaySession.lastOperation) {
8098 oldestRelayHost = host;
8099 oldestRelayTime = xrelaySession.lastOperation;
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
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;
8116 // No free ones, close the oldest one
8117 selectedHost = oldestRelayHost;
8119 xrelaySessionId = webSessionId + '/' + selectedHost;
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]; }
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; }
8136 // Set the multi-tunnel session
8137 webRelaySessions[xrelaySessionId] = relaySession;
8139 // Setup the cleanup timer if needed
8140 if (obj.cleanupTimer == null) { webRelayCleanupTimer = setInterval(checkWebRelaySessionsTimeout, 10000); }
8142 // Redirect to root.
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);
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');
8157 // Handle all incoming requests as web relays
8158 obj.webRelayRouter.get('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
8160 // Handle all incoming requests as web relays
8161 obj.webRelayRouter.post('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
8163 // Handle all incoming requests as web relays
8164 obj.webRelayRouter.put('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
8166 // Handle all incoming requests as web relays
8167 obj.webRelayRouter.delete('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
8169 // Handle all incoming requests as web relays
8170 obj.webRelayRouter.options('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
8172 // Handle all incoming requests as web relays
8173 obj.webRelayRouter.head('/*', function (req, res) { try { handleWebRelayRequest(req, res); } catch (ex) { console.log(ex); } })
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();
8186 obj.fs.stat(themeFilePath, function (err, stats) {
8187 if (err || !stats.isFile()) return next();
8188 res.sendFile(themeFilePath);
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
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));
8209 // Start regular disconnection list flush every 2 minutes.
8210 obj.wsagentsDisconnectionsTimer = setInterval(function () { obj.wsagentsDisconnections = {}; }, 120000);
8213 function finalizeWebserver() {
8214 // Setup all HTTP handlers
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));
8230 // Start server on a free port.
8231 CheckListenPort(obj.args.port, obj.args.portbind, StartWebServer);
8233 // Start on a second agent-only alternative port if needed.
8234 if (obj.args.agentport) { CheckListenPort(obj.args.agentport, obj.args.agentportbind, StartAltWebServer); }
8236 // We are done starting the web server.
8237 if (doneFunc) doneFunc();
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));
8252 // Auth strategy flags
8253 const domainAuthStrategyConsts = {
8257 reddit: 8, // Deprecated
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;
8270 // Setup auth strategies using passport if needed
8271 if (typeof domain.authstrategies != 'object') return authStrategyFlags;
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')());
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);
8294 authStrategyFlags |= domainAuthStrategyConsts.twitter;
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);
8311 authStrategyFlags |= domainAuthStrategyConsts.google;
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);
8333 authStrategyFlags |= domainAuthStrategyConsts.github;
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) {
8345 try { userex = require('jwt-simple').decode(params.id_token, '', true); } catch (ex) { }
8346 parent.authLog('setupDomainAuthStrategy', 'Azure profile: ' + JSON.stringify(userex));
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(); }
8352 return done(null, user);
8355 authStrategyFlags |= domainAuthStrategyConsts.azure;
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.');
8363 const certPath = obj.common.joinPath(obj.parent.datapath, domain.authstrategies.saml.cert);
8364 var cert = obj.fs.readFileSync(certPath);
8366 parent.debug('error', 'Unable to read SAML IdP certificate: ' + domain.authstrategies.saml.cert);
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;
8385 if (typeof profile.email == 'string') { user.email = profile.email; }
8386 return done(null, user);
8389 authStrategyFlags |= domainAuthStrategyConsts.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.');
8399 var cert = obj.fs.readFileSync(obj.common.joinPath(obj.parent.datapath, domain.authstrategies.intel.cert));
8401 parent.debug('error', 'Unable to read Intel SAML IdP certificate: ' + domain.authstrategies.intel.cert);
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);
8422 authStrategyFlags |= domainAuthStrategyConsts.intelSaml
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.');
8432 var cert = obj.fs.readFileSync(obj.common.joinPath(obj.parent.datapath, domain.authstrategies.jumpcloud.cert));
8434 parent.debug('error', 'Unable to read JumpCloud IdP certificate: ' + domain.authstrategies.jumpcloud.cert);
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);
8452 authStrategyFlags |= domainAuthStrategyConsts.jumpCloudSaml
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
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' }
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)) {
8476 let error = new Error('OIDC: Missing issuer URI.');
8477 parent.authLog('error', `${error.message} STRATEGY: ${JSON.stringify(strategy)}`);
8480 strategy.issuer.issuer = presetIssuer
8481 parent.authLog('setupDomainAuthStrategy', `OIDC: PRESET: ${preset.toUpperCase()}: Using preset issuer: ${presetIssuer}`);
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);
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 });
8494 strategy.options.params = Object.assign(strategy.options.params || {}, { 'scope': ['openid', 'profile', 'email'] });
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' }
8503 strategy.options.params.scope.push(groupScope)
8505 strategy.options.params.scope = strategy.options.params.scope.join(' ')
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 });
8511 // Discover additional information if available, use endpoints from config if present
8514 parent.authLog('setupDomainAuthStrategy', `OIDC: Discovering Issuer Endpoints: ${strategy.issuer.issuer}`);
8515 issuer = await strategy.obj.openidClient.Issuer.discover(strategy.issuer.issuer);
8517 let error = new Error('OIDC: Discovery failed.', { cause: err });
8518 parent.authLog('setupDomainAuthStrategy', `ERROR: ${JSON.stringify(error)} ISSUER_URI: ${strategy.issuer.issuer}`);
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));
8525 strategy.issuer = issuer?.metadata;
8526 strategy.obj.issuer = issuer;
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; }
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';
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';
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
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'); }
8553 authStrategyFlags |= domainAuthStrategyConsts.oidc
8555 function migrateOldConfigs(strategy) {
8558 'clientid': 'client_id',
8559 'clientsecret': 'client_secret',
8560 'callbackurl': 'redirect_uri'
8563 'authorizationurl': 'authorization_endpoint',
8564 'tokenurl': 'token_endpoint',
8565 'userinfourl': 'userinfo_endpoint'
8568 'tenantid': 'tenant_id',
8569 'customerid': 'customer_id'
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]}`);
8579 parent.authLog('migrateOldConfigs', `OIDC: OLD CONFIG: Moving old config to new location. strategy.${key} => strategy.${type}.${value}`);
8580 strategy[type][value] = strategy[key];
8582 delete strategy[key]
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`);
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)
8596 delete strategy.scope
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;
8602 strategy.groups.sync = true;
8604 parent.authLog('migrateOldConfigs', `OIDC: OLD CONFIG: Moving old config to new location. strategy.groups.sync.enabled => strategy.groups.sync`);
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') {
8618 parent.debug('error', 'OIDC: Unable to find callback function in parameters');
8623 // If profile is null/undefined, extract user info from the tokenset
8624 if (!profile && tokenset && tokenset.id_token) {
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());
8637 parent.debug('error', `OIDC: Failed to decode id_token: ${err.message}`);
8641 // Initialize user object
8642 let user = { 'strategy': 'oidc' }
8643 let claims = obj.common.validateObject(strategy.custom.claims) ? strategy.custom.claims : 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;
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;
8664 // Ensure we have a valid sid before proceeding
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'));
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
8677 // Setup end session endpoint
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}`);
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);
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 });
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);
8704 async function getGroups(preset, tokenset) {
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) => {
8710 'headers': { authorization: 'Bearer ' + tokenset.access_token }
8712 if (obj.httpsProxyAgent) { options.agent = obj.httpsProxyAgent; }
8713 const req = require('https').get(url, options, (res) => {
8715 res.on('data', (chunk) => {
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);
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);
8732 if (Buffer.isBuffer(data[0])) {
8733 data = Buffer.concat(data);
8734 data = data.toString();
8735 } else { // else if (typeof data[0] == 'string')
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);
8744 if (preset == 'azure') {
8745 data = JSON.parse(data);
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);
8754 if (preset == 'google') {
8755 data = data.split('\n');
8756 data = data.join('');
8757 data = JSON.parse(data);
8761 for (var i in data) {
8762 if (typeof data[i].displayName == 'string') {
8763 groups.push(data[i].displayName);
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}`);
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);
8787 return authStrategyFlags;
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);
8801 // No web relay session with this relay identifier, close the HTTP request.
8802 res.sendStatus(404);
8805 // The user is not logged in or does not have a relay identifier, close the HTTP request.
8806 res.sendStatus(404);
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);
8821 // No multi-tunnel session with this relay identifier, close the websocket.
8825 // The user is not logged in or does not have a relay identifier, close the websocket.
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) {
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
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) { }
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) { }
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') }));
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) { }
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'))
8882 // See if we support two-factor trusted cookies
8883 var twoFactorCookieDays = 30;
8884 if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
8886 // Check if two factor can be skipped
8887 const twoFactorSkip = checkUserOneTimePasswordSkip(domain, user, req, loginOptions);
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) { }
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); }
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); }
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) { }
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) { }
8949 // We are authenticated
8951 ws.removeAllListeners(['message', 'close', 'error']);
8952 func(ws, req, domain, user, authData);
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) { }
8966 // We are authenticated
8968 ws.removeAllListeners(['message', 'close', 'error']);
8969 func(ws, req, domain, user, twoFactorSkip);
8976 // Invalid authentication
8977 try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2c' })); } catch (ex) { }
8978 try { ws.close(); } catch (ex) { }
8986 // If error, do nothing
8987 ws.on('error', function (err) { try { ws.close(); } catch (e) { console.log(e); } });
8989 // If the web socket is closed
8990 ws.on('close', function (req) { try { ws.close(); } catch (e) { console.log(e); } });
8992 // Resume the socket to perform inner authentication
8993 try { ws._socket.resume(); } catch (ex) { }
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;
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; }
9006 // Hold this websocket until we are ready.
9009 // Check IP filtering and domain
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; }
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; }
9020 // Check if inner authentication is requested
9021 if (req.headers['x-meshauth'] === '*') { func(ws, req, domain, null); return; }
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'))
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];
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) { }
9039 // See if we support two-factor trusted cookies
9040 var twoFactorCookieDays = 30;
9041 if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
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) { }
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); }
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) { }
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) { }
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) { }
9105 req.session.userid = user._id;
9106 req.session.ip = req.clientIp;
9107 setSessionRandom(req);
9108 func(ws, req, domain, user, null, authData);
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) { }
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);
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()]);
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) { }
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) + '\".');
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);
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);
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);
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) { }
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) { }
9192 // Check if a 2nd factor is needed
9193 if (checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true) {
9195 // See if we support two-factor trusted cookies
9196 var twoFactorCookieDays = 30;
9197 if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }
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) { }
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) { }
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) { }
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) { }
9243 func(ws, req, domain, user, null, authData);
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) { }
9255 req.session.userid = user._id;
9256 req.session.ip = req.clientIp;
9257 setSessionRandom(req);
9258 func(ws, req, domain, user);
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()]);
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) { }
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()]);
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]);
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) { }
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);
9297 } catch (e) { console.log(e); }
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); } } }
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) : '') + '.'); });
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) : '') + '.'); }
9321 obj.parent.updateServerState('servername', certificates.CommonName);
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); }
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) : '') + '.'); }
9331 obj.parent.updateServerState('http-port', port);
9332 if (args.aliasport != null) { obj.parent.updateServerState('http-aliasport', args.aliasport); }
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)) {
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.');
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);
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) : '') + '.'); });
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) : '') + '.'); });
9362 obj.parent.debug('https', 'Server listening on 0.0.0.0 port ' + port + '.');
9363 obj.parent.updateServerState('https-agent-port', port);
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);
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;
9378 // Check we have agent rights
9379 if (((obj.GetMeshRights(user, agent.dbMeshKey) & MESHRIGHT_AGENTCONSOLE) != 0) || (user.siteadmin == 0xFFFFFFFF)) { agent.close(disconnectMode); }
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
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;
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
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]);
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; } } }
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 ]
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
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; }
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];
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 });
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];
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 })
9490 // Set the new session counts
9491 obj.sessionsCount = newSessionsCount;
9493 // Figure out the userid
9494 userid = changedSessionId.split('/').slice(0, 3).join('/');
9496 // Recount only changedSessionId
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; }
9503 // If the count changed, update and event
9504 if (newcount != oldcount) {
9505 x = userid.split('/');
9506 var u = obj.users[userid];
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;
9517 /* Access Control Functions */
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
9536 rights &= (0xFFFFFFFF - substract);
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;
9541 rights &= (0xFFFFFFFF - substract);
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); } }
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
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
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
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;
9580 // If no links, stop here.
9581 if (user.links == null) { func(null, 0, false); return; }
9583 // Check device link
9584 var rights = 0, visible = false, r = user.links[nodeid];
9586 if (r.rights == 0xFFFFFFFF) { func(nodes[0], removeUserRights(0xFFFFFFFF, user), true); return; } // User has full rights thru a device link, stop here.
9591 // Check device group link
9592 r = user.links[nodes[0].meshid];
9594 if (r.rights == 0xFFFFFFFF) { func(nodes[0], removeUserRights(0xFFFFFFFF, user), true); return; } // User has full rights thru a device group link, stop here.
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];
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
9610 r = g.links[nodeid];
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
9620 // Remove any user rights
9621 rights = removeUserRights(rights, user);
9623 // Return the rights we found
9624 func(nodes[0], rights, visible);
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 []; }
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]); } }
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); }
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 []; }
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); } }
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); }
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); }
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; }
9703 if (typeof mesh == 'string') {
9705 } else if ((typeof mesh == 'object') && (typeof mesh._id == 'string')) {
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); }
9712 // Check direct user to device group permissions
9713 if (user.links == null) return 0;
9715 r = user.links[meshid];
9717 var rights = r.rights;
9718 if (rights == 0xFFFFFFFF) { return removeUserRights(rights, user); } // If the user has full access thru direct link, stop here.
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];
9726 r = g.links[meshid];
9728 if (r.rights == 0xFFFFFFFF) {
9729 return removeUserRights(r.rights, user); // If the user hash full access thru a user group link, stop here.
9731 rights |= r.rights; // Add to existing rights (TODO: Deal with reverse rights)
9739 return removeUserRights(rights, user);
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; }
9748 if (typeof mesh == 'string') {
9750 } else if ((typeof mesh == 'object') && (typeof mesh._id == 'string')) {
9752 } else return false;
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; }
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.
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.
9772 var GetNodeRightsCache = {};
9773 var GetNodeRightsCacheCount = 0;
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; }
9781 if (typeof mesh == 'string') { meshid = mesh; } else if ((typeof mesh == 'object') && (typeof mesh._id == 'string')) { meshid = mesh._id; } else return 0;
9783 // Check if we have this in the cache
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
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++;
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++;
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; }
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++;
9826 obj.InvalidateNodeCache = function (user, mesh, nodeid) {
9827 if (user == null) { return; }
9829 if (typeof user == 'string') { user = obj.users[user]; }
9830 if (user == null) { return 0; }
9832 if (typeof mesh == 'string') { meshid = mesh; } else if ((typeof mesh == 'object') && (typeof mesh._id == 'string')) { meshid = mesh._id; };
9835 for (let [key, val] of Object.entries(GetNodeRightsCache[user._id] || {})) {
9836 GetNodeRightsCacheCount -= Object.keys(val).length
9838 delete GetNodeRightsCache[user._id];
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;
9847 if (((GetNodeRightsCache[user._id] || {})[meshid] || {})[nodeid]) {
9848 delete ((GetNodeRightsCache[user._id] || {})[meshid] || {})[nodeid]
9849 GetNodeRightsCacheCount--;
9853 obj.FlushGetNodeRightsCache = function() {
9854 GetNodeRightsCache = {};
9855 GetNodeRightsCacheCount = 0;
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); } } }
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); } }
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
9885 delete user2.passhint;
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
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; }
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) {
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.
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
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; }
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
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
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'
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; }
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]; }
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]; }
9972 out.desktopsettings = JSON.stringify(out.desktopsettings);
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);
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; }
9987 if ((domain != null) && (domain.webviewspath != null)) { // If the domain has a web views path, use that first
9989 p = obj.path.join(domain.webviewspath, pagename + '-mobile-min');
9990 if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Minify + Override document
9992 p = obj.path.join(domain.webviewspath, pagename + '-mobile');
9993 if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Override document
9995 if (obj.parent.webViewsOverridePath != null) {
9997 p = obj.path.join(obj.parent.webViewsOverridePath, pagename + '-mobile-min');
9998 if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Minify + Override document
10000 p = obj.path.join(obj.parent.webViewsOverridePath, pagename + '-mobile');
10001 if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Override document
10004 p = obj.path.join(obj.parent.webViewsPath, pagename + '-mobile-min');
10005 if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile + Minify document
10007 p = obj.path.join(obj.parent.webViewsPath, pagename + '-mobile');
10008 if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Mobile document
10010 if ((domain != null) && (domain.webviewspath != null)) { // If the domain has a web views path, use that first
10012 p = obj.path.join(domain.webviewspath, pagename + '-min');
10013 if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Minify + Override document
10015 p = obj.path.join(domain.webviewspath, pagename);
10016 if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Override document
10018 if (obj.parent.webViewsOverridePath != null) {
10020 p = obj.path.join(obj.parent.webViewsOverridePath, pagename + '-min');
10021 if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Minify + Override document
10023 p = obj.path.join(obj.parent.webViewsOverridePath, pagename);
10024 if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Override document
10027 p = obj.path.join(obj.parent.webViewsPath, pagename + '-min');
10028 if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Minify document
10030 p = obj.path.join(obj.parent.webViewsPath, pagename);
10031 if (obj.fs.existsSync(p + '.handlebars')) { return p; } // Default document
10035 function generateCustomCSSTags(customFilesObject, currentTemplate) {
10038 cssTags += '<link keeplink=1 type="text/css" href="styles/custom.css" media="screen" rel="stylesheet" title="CSS" />\n ';
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 ';
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 ';
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 ';
10076 return cssTags.trim();
10079 function generateCustomJSTags(customFilesObject, currentTemplate) {
10082 jsTags += '<script keeplink=1 type="text/javascript" src="scripts/custom.js"></script>\n ';
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 ';
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 ';
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 ';
10119 return jsTags.trim();
10122 function generateThemePackCSSTags(domain, currentTemplate) {
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 ';
10134 function generateThemePackJSTags(domain, currentTemplate) {
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 ';
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';
10155 ((page == 'login2') && (domain.loginpicture == null) && (domain.titlehtml == null)) ||
10156 ((page != 'login2') && (domain.titlepicture == null) && (domain.titlehtml == null))
10158 if (domain.title == null) {
10159 xargs.title1 = 'MeshCentral';
10162 xargs.title1 = domain.title;
10163 xargs.title2 = domain.title2 ? domain.title2 : '';
10166 xargs.title1 = domain.title1 ? domain.title1 : '';
10167 xargs.title2 = (domain.title1 && domain.title2) ? domain.title2 : '';
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
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; }
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') : '';
10186 // Generate custom CSS and JS tags
10187 if (xargs.customFiles) {
10189 var customFiles = JSON.parse(decodeURIComponent(xargs.customFiles));
10190 xargs.customCSSTags = generateCustomCSSTags(customFiles, page);
10191 xargs.customJSTags = generateCustomJSTags(customFiles, page);
10193 xargs.customCSSTags = generateCustomCSSTags(null, page);
10194 xargs.customJSTags = generateCustomJSTags(null, page);
10197 xargs.customCSSTags = generateCustomCSSTags(null, page);
10198 xargs.customJSTags = generateCustomJSTags(null, page);
10200 xargs.customCSSTags += generateThemePackCSSTags(domain, page);
10201 xargs.customJSTags += generateThemePackJSTags(domain, page);
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
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];
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);
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
10240 // See if the session is connected
10241 var sessions = obj.wssessions[command.userid];
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)); }
10250 if (parent.multiServer != null) {
10251 // TODO: Add multi-server support
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);
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) { } }
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);
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; }
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());
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()); } }
10299 return acceptLanguages;
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
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".
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('-');
10326 const shortAcceptedLanguage = acceptLanguages[i].substring(0, ptr);
10327 if (fileOptions[shortAcceptedLanguage] != null) { foundLanguage = shortAcceptedLanguage; }
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); }
10337 if (user && (user.llang != foundLanguage)) { user.llang = foundLanguage; obj.db.SetUser(user); } // Set user 'last language' used if needed.
10344 // No matches found, render the default English page.
10345 res.render(filename, args);
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'); }
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]); }
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]); }
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]); }
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'); }
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]); }
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]); }
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;
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;
10466 // Update the database
10467 obj.db.SetUser(user);
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);
10478 completionFunc.pushCount = user.webpush.length;
10479 completionFunc.user = user;
10480 completionFunc.domain = domain;
10481 completionFunc.failCount = 0;
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);
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;
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);
10517 db.Set({ _id: 'pmt_' + node.pmt, type: 'pmt', domain: node.domain, time: Date.now(), nodeid: node._id })
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);
10530 // Return decoded user agent information
10531 obj.getUserAgentInfo = function (req) {
10532 var browser = 'Unknown', os = 'Unknown';
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);
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
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';
10550 ua.osStr += '/' + ua.platformVersion
10554 } catch (ex) { return { browserStr: browser, osStr: os } }
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('?');
10562 if (req.body && req.body.urlargs) {
10563 return req.body.urlargs;
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]);
10576 return (filteredParams.length > 0 ? ('?' + filteredParams.join('&')) : '');
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; }
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; }
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; } }
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('');
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; }
10601 var x = { 'Cache-Control': 'no-store', 'Content-Type': type, 'Content-Disposition': 'attachment; filename="' + altname + '"' };
10602 if (typeof size == 'number') { x['Content-Length'] = size; }
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; }
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; }
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; }
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
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
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
10665 while ((ipTable.length > 0) && (ipTable[0] < cutoffTime)) { ipTable.shift(); }
10666 if (ipTable.length == 0) { delete obj.badLoginTable[ip]; }
10669 obj.badLoginTableLastClean = 0;
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; }
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; }
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
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
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
10723 while ((ipTable.length > 0) && (ipTable[0] < cutoffTime)) { ipTable.shift(); }
10724 if (ipTable.length == 0) { delete obj.bad2faTable[ip]; }
10727 obj.bad2faTableLastClean = 0;
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
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);
10751 ws.on('message', xfunc);
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'); }
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]]; }
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);
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;
10785 return header + value + '\r\n';
10788 // Check that everything is cleaned up
10789 function checkWebRelaySessionsTimeout() {
10790 for (var i in webRelaySessions) { webRelaySessions[i].checkTimeout(); }
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;
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; }
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)
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.
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); }
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 = {}; }
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]; }
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: {} };
10857 // Save the new group
10859 if (db.changeStream == false) { obj.userGroups[ugrpid] = ugrp; }
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.
10865 // Log in the auth log
10866 parent.authLog('https', userMembershipType.toUpperCase() + ': Created user group ' + ugrp.name);
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;
10875 parent.DispatchEvent([user._id], obj, 'resubscribe');
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);
10883 // Add a user to the user group
10884 ugrp.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
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);
10892 // Log in the auth log
10893 parent.authLog('https', userMembershipType.toUpperCase() + ': Adding ' + user.name + ' to user group ' + userMemberships[i] + '.');
10895 // User is already part of this user group
10896 delete existingUserMemberships[ugrpid];
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];
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);
10914 parent.DispatchEvent([user._id], obj, 'resubscribe');
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];
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);
10931 return userChanged;