Ctrl K
ComunidadGuías…

webserver.js

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
* @description MeshCentral web server
* @author Ylian Saint-Hilaire
* @copyright Intel Corporation 2018-2022
* @license Apache-2.0
* @version v0.0.1
*/

/*jslint node: true */
/*jshint node: true */
/*jshint strict:false */
/*jshint -W097 */
/*jshint esversion: 6 */
'use strict';

// SerialTunnel object is used to embed TLS within another connection.
function SerialTunnel(options) {
    var obj = new require('stream').Duplex(options);
    obj.forwardwrite = null;
    obj.updateBuffer = function (chunk) { this.push(chunk); };
    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
    obj._read = function (size) { }; // Push nothing, anything to read should be pushed from updateBuffer()
    return obj;
}

// Append an exact IP address to an agent block list file.
function AppendAgentBlockedIp(filename, ip, fsModule) {
    if ((typeof filename != 'string') || (filename.length == 0) || (typeof ip != 'string') || (ip.length == 0)) { return false; }
    try {
        const separator = (fsModule.existsSync(filename) && (fsModule.statSync(filename).size > 0)) ? '\r\n' : '';
        fsModule.appendFileSync(filename, separator + ip + '\r\n');
        return true;
    } catch (ex) { return false; }
}

// Create a sliding-window limiter for new agent registrations by source IP.
function CreateAgentRegistrationLimiter(config, nowFunc, ipMatchFunc, onBlockFunc) {
    if ((config == null) || (typeof config != 'object') || Array.isArray(config)) { return null; }
    if (!Number.isSafeInteger(config.agents) || (config.agents < 1)) { return null; }
    if (!Number.isSafeInteger(config.minutes) || (config.minutes < 1)) { return null; }

    const obj = {};
    const windowMilliseconds = config.minutes * 60000;
    const autoBlock = (config.autoblock !== false);
    const registrations = Object.create(null);
    const now = (typeof nowFunc == 'function') ? nowFunc : Date.now;
    var operationsSinceClean = 0;
    var exclusions = [];

    if (typeof config.exclude == 'string') {
        exclusions = config.exclude.split(',').map(function (x) { return x.trim(); }).filter(function (x) { return x.length > 0; });
    } else if (Array.isArray(config.exclude)) {
        exclusions = config.exclude.filter(function (x) { return typeof x == 'string'; }).map(function (x) { return x.trim(); }).filter(function (x) { return x.length > 0; });
    }

    function normalizeIp(ip) {
        if ((typeof ip != 'string') || (ip.length == 0) || (ip.length > 128)) { return null; }
        ip = ip.toLowerCase();
        if (ip.indexOf('::ffff:') == 0) { ip = ip.substring(7); }
        if (require('net').isIP(ip) == 0) { return null; }
        return ip;
    }

    function isExcluded(ip) {
        if (exclusions.length == 0) { return false; }
        const ipMatch = (typeof ipMatchFunc == 'function') ? ipMatchFunc : require('ipcheck').match;
        for (var i = 0; i < exclusions.length; i++) {
            try { if (ipMatch(ip, exclusions[i]) == true) { return true; } } catch (ex) { }
        }
        return false;
    }

    function prune(ip, currentTime) {
        const timestamps = registrations[ip];
        if (timestamps == null) { return null; }
        const cutoffTime = currentTime - windowMilliseconds;
        while ((timestamps.length > 0) && (timestamps[0] <= cutoffTime)) { timestamps.shift(); }
        if (timestamps.length == 0) { delete registrations[ip]; return null; }
        return timestamps;
    }

    obj.clean = function () {
        const currentTime = now();
        for (var ip in registrations) { prune(ip, currentTime); }
        operationsSinceClean = 0;
    };

    obj.isBlocked = function (ip) {
        if (autoBlock == false) { return false; }
        ip = normalizeIp(ip);
        if ((ip == null) || isExcluded(ip)) { return false; }
        const timestamps = prune(ip, now());
        return (timestamps != null) && (timestamps.length >= config.agents);
    };

    // Returns false when this registration would exceed the configured limit.
    obj.register = function (ip) {
        ip = normalizeIp(ip);
        if (ip == null) { return false; }
        if (isExcluded(ip)) { return true; }
        const currentTime = now();
        var timestamps = prune(ip, currentTime);
        if ((timestamps != null) && (timestamps.length >= config.agents)) { return false; }
        if (timestamps == null) { timestamps = registrations[ip] = []; }
        timestamps.push(currentTime);
        if ((autoBlock == true) && (timestamps.length == config.agents) && (typeof onBlockFunc == 'function')) { try { onBlockFunc(ip); } catch (ex) { } }
        if (++operationsSinceClean > 100) { obj.clean(); }
        return true;
    };

    return obj;
}

// ExpressJS login sample
// https://github.com/expressjs/express/blob/master/examples/auth/index.js

// Polyfill startsWith/endsWith for older NodeJS
if (!String.prototype.startsWith) { String.prototype.startsWith = function (searchString, position) { position = position || 0; return this.substr(position, searchString.length) === searchString; }; }
if (!String.prototype.endsWith) { String.prototype.endsWith = function (searchString, position) { var subjectString = this.toString(); if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) { position = subjectString.length; } position -= searchString.length; var lastIndex = subjectString.lastIndexOf(searchString, position); return lastIndex !== -1 && lastIndex === position; }; }

// Construct a HTTP server object
module.exports.CreateWebServer = function (parent, db, args, certificates, doneFunc) {
    var obj = {}, i = 0;

    // Modules
    obj.fs = require('fs');
    obj.net = require('net');
    obj.tls = require('tls');
    obj.path = require('path');
    obj.os = require('os');
    obj.bodyParser = require('body-parser');
    obj.exphbs = require('express-handlebars');
    obj.crypto = require('crypto');
    obj.common = require('./common.js');
    obj.express = require('express');
    obj.meshAgentHandler = require('./meshagent.js');
    obj.meshRelayHandler = require('./meshrelay.js');
    obj.meshDeviceFileHandler = require('./meshdevicefile.js');
    obj.meshDesktopMultiplexHandler = require('./meshdesktopmultiplex.js');
    obj.meshIderHandler = require('./amt/amt-ider.js');
    obj.meshUserHandler = require('./meshuser.js');
    obj.interceptor = require('./interceptor');
    obj.uaparser = require('ua-parser-js');
    obj.uaclienthints = require('ua-client-hints-js');
    const constants = (obj.crypto.constants ? obj.crypto.constants : require('constants')); // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead.

    // Setup WebAuthn / FIDO2
    obj.webauthn = require('./webauthn.js').CreateWebAuthnModule();

    if (process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']) {
        obj.httpsProxyAgent = new (require('https-proxy-agent').HttpsProxyAgent)(process.env['HTTP_PROXY'] || process.env['HTTPS_PROXY'] || process.env['http_proxy'] || process.env['https_proxy']);
    }

    // Variables
    obj.args = args;
    obj.parent = parent;
    obj.filespath = parent.filespath;
    obj.db = db;
    obj.app = obj.express();
    if (obj.args.agentport) { obj.agentapp = obj.express(); }
    if (args.compression === true) {
        obj.app.use(require('compression')({ filter: function (req, res) {
            if (req.path == '/devicefile.ashx') return false; // Don't compress device file transfers to show file sizes
            if ((args.relaydns != null) && (obj.args.relaydns.indexOf(req.hostname) >= 0)) return false; // Don't compress DNS relay requests
            return require('compression').filter(req, res);
        }}));
    }
    obj.app.disable('x-powered-by');
    obj.tlsServer = null;
    obj.tcpServer = null;
    obj.certificates = certificates;
    obj.users = {};                             // UserID --> User
    obj.oidcLogoutUrls = {};                    // UserID --> OIDC end session URL from the last login
    obj.meshes = {};                            // MeshID --> Mesh (also called device group)
    obj.userGroups = {};                        // UGrpID --> User Group
    obj.useNodeDefaultTLSCiphers = args.usenodedefaulttlsciphers; // Use TLS ciphers provided by node
    obj.tlsCiphers = args.tlsciphers;           // List of TLS ciphers to use
    obj.userAllowedIp = args.userallowedip;     // List of allowed IP addresses for users
    obj.agentAllowedIp = args.agentallowedip;   // List of allowed IP addresses for agents
    obj.agentBlockedIp = args.agentblockedip;   // List of blocked IP addresses for agents
    obj.tlsSniCredentials = null;
    obj.dnsDomains = {};
    obj.relaySessionCount = 0;
    obj.relaySessionErrorCount = 0;
    obj.blockedUsers = 0;
    obj.blockedAgents = 0;
    obj.persistAgentBlockedIp = function (ip) {
        if (parent.agentBlockedIpFile == null) { return false; }
        if (AppendAgentBlockedIp(parent.agentBlockedIpFile, ip, obj.fs) == false) {
            console.log('Unable to permanently block agent IP address ' + ip + ' in ' + parent.agentBlockedIpFile + '; temporary block remains active.');
            return false;
        }
        if (!Array.isArray(parent.config.settings.agentblockedip)) { parent.config.settings.agentblockedip = []; }
        if (parent.config.settings.agentblockedip.indexOf(ip) < 0) { parent.config.settings.agentblockedip.push(ip); }
        obj.agentBlockedIp = parent.config.settings.agentblockedip;
        parent.debug('agent', 'Permanently blocked agent IP address ' + ip + ' in ' + parent.agentBlockedIpFile + '.');
        return true;
    };
    obj.agentRegistrationLimiter = CreateAgentRegistrationLimiter(parent.config.settings.agentenrollmentratelimitbyip, null, null, obj.persistAgentBlockedIp);
    obj.recordAgentRegistration = function (ip) { return (obj.agentRegistrationLimiter == null) || obj.agentRegistrationLimiter.register(ip); };
    obj.renderPages = null;
    obj.renderLanguages = [];
    obj.destroyedSessions = {};                 // userid/req.session.x --> destroyed session time

    const isWindowsPlatform = (obj.os.platform() === 'win32');
    const safeUploadTempRoots = (function () {
        const roots = [];
        const addRoot = function (p) {
            if (typeof p !== 'string') { return; }
            var resolved;
            try { resolved = obj.path.normalize(obj.path.resolve(p)); } catch (ex) { return; }
            if (resolved.length === 0) { return; }
            if ((resolved.length > 1) && resolved.endsWith(obj.path.sep)) { resolved = resolved.slice(0, -1); }
            const comparison = isWindowsPlatform ? resolved.toLowerCase() : resolved;
            const comparisonWithSep = comparison + obj.path.sep;
            roots.push({ comparison: comparison, comparisonWithSep: comparisonWithSep });
        };
        addRoot(obj.os.tmpdir());
        if (typeof obj.parent.filespath === 'string') { addRoot(obj.path.join(obj.parent.filespath, 'tmp')); }
        return roots;
    })();
    function resolveSafeUploadTempPath(tempPath) {
        if (typeof tempPath !== 'string') { return null; }
        var resolvedPath;
        try { resolvedPath = obj.path.normalize(obj.path.resolve(tempPath)); } catch (ex) { return null; }
        var comparisonPath = isWindowsPlatform ? resolvedPath.toLowerCase() : resolvedPath;
        var comparisonPathNoTrailing = comparisonPath;
        if ((comparisonPathNoTrailing.length > 1) && comparisonPathNoTrailing.endsWith(obj.path.sep)) { comparisonPathNoTrailing = comparisonPathNoTrailing.slice(0, -1); }
        for (var i = 0; i < safeUploadTempRoots.length; i++) {
            var root = safeUploadTempRoots[i];
            if ((comparisonPathNoTrailing === root.comparison) || comparisonPath.startsWith(root.comparisonWithSep)) { return resolvedPath; }
        }
        return null;
    }

    // Web relay sessions
    var webRelayNextSessionId = 1;
    var webRelaySessions = {}                   // UserId/SessionId/Host --> Web Relay Session
    var webRelayCleanupTimer = null;

    // Monitor web relay session removals
    parent.AddEventDispatch(['server-shareremove'], obj);
    obj.HandleEvent = function (source, event, ids, id) {
        if (event.action == 'removedDeviceShare') {
            for (var relaySessionId in webRelaySessions) {
                // A share was removed that matches an active session, close the web relay session.
                if (webRelaySessions[relaySessionId].xpublicid === event.publicid) { webRelaySessions[relaySessionId].close(); }
            }
        }
    }

    // Mesh Rights
    const MESHRIGHT_EDITMESH = 0x00000001;
    const MESHRIGHT_MANAGEUSERS = 0x00000002;
    const MESHRIGHT_MANAGECOMPUTERS = 0x00000004;
    const MESHRIGHT_REMOTECONTROL = 0x00000008;
    const MESHRIGHT_AGENTCONSOLE = 0x00000010;
    const MESHRIGHT_SERVERFILES = 0x00000020;
    const MESHRIGHT_WAKEDEVICE = 0x00000040;
    const MESHRIGHT_SETNOTES = 0x00000080;
    const MESHRIGHT_REMOTEVIEWONLY = 0x00000100;
    const MESHRIGHT_NOTERMINAL = 0x00000200;
    const MESHRIGHT_NOFILES = 0x00000400;
    const MESHRIGHT_NOAMT = 0x00000800;
    const MESHRIGHT_DESKLIMITEDINPUT = 0x00001000;
    const MESHRIGHT_LIMITEVENTS = 0x00002000;
    const MESHRIGHT_CHATNOTIFY = 0x00004000;
    const MESHRIGHT_UNINSTALL = 0x00008000;
    const MESHRIGHT_NODESKTOP = 0x00010000;
    const MESHRIGHT_REMOTECOMMAND = 0x00020000;
    const MESHRIGHT_RESETOFF = 0x00040000;
    const MESHRIGHT_GUESTSHARING = 0x00080000;
    const MESHRIGHT_DEVICEDETAILS = 0x00100000;
    const MESHRIGHT_RELAY = 0x00200000;
    const MESHRIGHT_NOREGISTRY = 0x00400000;
    const MESHRIGHT_NOSOFTWARE = 0x00800000;
    const MESHRIGHT_ADMIN = 0xFFFFFFFF;

    // Site rights
    const SITERIGHT_SERVERBACKUP = 0x00000001;
    const SITERIGHT_MANAGEUSERS = 0x00000002;
    const SITERIGHT_SERVERRESTORE = 0x00000004;
    const SITERIGHT_FILEACCESS = 0x00000008;
    const SITERIGHT_SERVERUPDATE = 0x00000010;
    const SITERIGHT_LOCKED = 0x00000020;
    const SITERIGHT_NONEWGROUPS = 0x00000040;
    const SITERIGHT_NOMESHCMD = 0x00000080;
    const SITERIGHT_USERGROUPS = 0x00000100;
    const SITERIGHT_RECORDINGS = 0x00000200;
    const SITERIGHT_LOCKSETTINGS = 0x00000400;
    const SITERIGHT_ALLEVENTS = 0x00000800;
    const SITERIGHT_NONEWDEVICES = 0x00001000;
    const SITERIGHT_ADMIN = 0xFFFFFFFF;

    // Setup SSPI authentication if needed
    if ((obj.parent.platform == 'win32') && (obj.args.nousers != true) && (obj.parent.config != null) && (obj.parent.config.domains != null)) {
        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 }); } }
    }

    // Perform hash on web certificate and agent certificate
    obj.webCertificateHash = parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.web.cert);
    obj.webCertificateHashs = { '': obj.webCertificateHash };
    obj.webCertificateHashBase64 = Buffer.from(obj.webCertificateHash, 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
    obj.webCertificateFullHash = parent.certificateOperations.getCertHashBinary(obj.certificates.web.cert);
    obj.webCertificateFullHashs = { '': obj.webCertificateFullHash };
    obj.webCertificateExpire = { '': parent.certificateOperations.getCertificateExpire(parent.certificates.web.cert) };
    obj.agentCertificateHashHex = parent.certificateOperations.getPublicKeyHash(obj.certificates.agent.cert);
    obj.agentCertificateHashBase64 = Buffer.from(obj.agentCertificateHashHex, 'hex').toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
    obj.agentCertificateAsn1 = parent.certificateOperations.forge.asn1.toDer(parent.certificateOperations.forge.pki.certificateToAsn1(parent.certificateOperations.forge.pki.certificateFromPem(parent.certificates.agent.cert))).getBytes();
    obj.defaultWebCertificateHash = obj.certificates.webdefault ? parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.webdefault.cert) : null;
    obj.defaultWebCertificateFullHash = obj.certificates.webdefault ? parent.certificateOperations.getCertHashBinary(obj.certificates.webdefault.cert) : null;

    // Compute the hash of all of the web certificates for each domain
    for (var i in obj.parent.config.domains) {
        if (obj.parent.config.domains[i].certhash != null) {
            // If the web certificate hash is provided, use it.
            obj.webCertificateHashs[i] = obj.webCertificateFullHashs[i] = Buffer.from(obj.parent.config.domains[i].certhash, 'hex').toString('binary');
            if (obj.parent.config.domains[i].certkeyhash != null) { obj.webCertificateHashs[i] = Buffer.from(obj.parent.config.domains[i].certkeyhash, 'hex').toString('binary'); }
            delete obj.webCertificateExpire[i]; // Expire time is not provided
        } else if ((obj.parent.config.domains[i].dns != null) && (obj.parent.config.domains[i].certs != null)) {
            // If the domain has a different DNS name, use a different certificate hash.
            // Hash the full certificate
            obj.webCertificateFullHashs[i] = parent.certificateOperations.getCertHashBinary(obj.parent.config.domains[i].certs.cert);
            obj.webCertificateExpire[i] = Date.parse(parent.certificateOperations.forge.pki.certificateFromPem(obj.parent.config.domains[i].certs.cert).validity.notAfter);
            try {
                // Decode a RSA certificate and hash the public key.
                obj.webCertificateHashs[i] = parent.certificateOperations.getPublicKeyHashBinary(obj.parent.config.domains[i].certs.cert);
            } catch (ex) {
                // This may be a ECDSA certificate, hash the entire cert.
                obj.webCertificateHashs[i] = obj.webCertificateFullHashs[i];
            }
        } else if ((obj.parent.config.domains[i].dns != null) && (obj.certificates.dns[i] != null)) {
            // If this domain has a DNS and a matching DNS cert, use it. This case works for wildcard certs.
            obj.webCertificateFullHashs[i] = parent.certificateOperations.getCertHashBinary(obj.certificates.dns[i].cert);
            obj.webCertificateHashs[i] = parent.certificateOperations.getPublicKeyHashBinary(obj.certificates.dns[i].cert);
            obj.webCertificateExpire[i] = Date.parse(parent.certificateOperations.forge.pki.certificateFromPem(obj.certificates.dns[i].cert).validity.notAfter);
        } else if (i != '') {
            // For any other domain, use the default cert.
            obj.webCertificateFullHashs[i] = obj.webCertificateFullHashs[''];
            obj.webCertificateHashs[i] = obj.webCertificateHashs[''];
            obj.webCertificateExpire[i] = obj.webCertificateExpire[''];
        }
    }

    // If we are running the legacy swarm server, compute the hash for that certificate
    if (parent.certificates.swarmserver != null) {
        obj.swarmCertificateAsn1 = parent.certificateOperations.forge.asn1.toDer(parent.certificateOperations.forge.pki.certificateToAsn1(parent.certificateOperations.forge.pki.certificateFromPem(parent.certificates.swarmserver.cert))).getBytes();
        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' });
        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' });
    }

    // Main lists
    obj.wsagents = {};                // NodeId --> Agent
    obj.wsagentsWithBadWebCerts = {}; // NodeId --> Agent
    obj.wsagentsDisconnections = {};
    obj.wsagentsDisconnectionsTimer = null;
    obj.duplicateAgentsLog = {};
    obj.wssessions = {};              // UserId --> Array Of Sessions
    obj.wssessions2 = {};             // "UserId + SessionRnd" --> Session  (Note that the SessionId is the UserId + / + SessionRnd)
    obj.wsPeerSessions = {};          // ServerId --> Array Of "UserId + SessionRnd"
    obj.wsPeerSessions2 = {};         // "UserId + SessionRnd" --> ServerId
    obj.wsPeerSessions3 = {};         // ServerId --> UserId --> [ SessionId ]
    obj.sessionsCount = {};           // Merged session counters, used when doing server peering. UserId --> SessionCount
    obj.wsrelays = {};                // Id -> Relay
    obj.desktoprelays = {};           // Id -> Desktop Multiplexer Relay
    obj.wsPeerRelays = {};            // Id -> { ServerId, Time }
    var tlsSessionStore = {};         // Store TLS session information for quick resume.
    var tlsSessionStoreCount = 0;     // Number of cached TLS session information in store.

    // Setup randoms
    obj.crypto.randomBytes(48, function (err, buf) { obj.httpAuthRandom = buf; });
    obj.crypto.randomBytes(16, function (err, buf) { obj.httpAuthRealm = buf.toString('hex'); });
    obj.crypto.randomBytes(48, function (err, buf) { obj.relayRandom = buf; });

    // Get non-english web pages and emails
    getRenderList();
    getEmailLanguageList();

    // Setup DNS domain TLS SNI credentials
    {
        var dnscount = 0;
        obj.tlsSniCredentials = {};
        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++; } }
        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; }
    }
    function TlsSniCallback(name, cb) {
        var c = obj.tlsSniCredentials[name];
        if (c != null) {
            cb(null, c);
        } else {
            cb(null, obj.tlsSniCredentials['']);
        }
    }

    function EscapeHtml(x) { if (typeof x == 'string') return x.replace(/&/g, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;'); if (typeof x == 'boolean') return x; if (typeof x == 'number') return x; }
    //function EscapeHtmlBreaks(x) { if (typeof x == "string") return x.replace(/&/g, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;').replace(/\r/g, '<br />').replace(/\n/g, '').replace(/\t/g, '&nbsp;&nbsp;'); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
    // Fetch all users from the database, keep this in memory
    obj.db.GetAllType('user', function (err, docs) {
        obj.common.unEscapeAllLinksFieldName(docs);
        var domainUserCount = {}, i = 0;
        for (i in parent.config.domains) { domainUserCount[i] = 0; }
        for (i in docs) { var u = obj.users[docs[i]._id] = docs[i]; domainUserCount[u.domain]++; }
        for (i in parent.config.domains) {
            if ((parent.config.domains[i].share == null) && (domainUserCount[i] == 0)) {
                // If newaccounts is set to no new accounts, but no accounts exists, temporarily allow account creation.
                //if ((parent.config.domains[i].newaccounts === 0) || (parent.config.domains[i].newaccounts === false)) { parent.config.domains[i].newaccounts = 2; }
                console.log('Server ' + ((i == '') ? '' : (i + ' ')) + 'has no users, next new account will be site administrator.');
            }
        }

        // Fetch all device groups (meshes) from the database, keep this in memory
        // As we load things in memory, we will also be doing some cleaning up.
        // We will not save any clean up in the database right now, instead it will be saved next time there is a change.
        obj.db.GetAllType('mesh', function (err, docs) {
            obj.common.unEscapeAllLinksFieldName(docs);
            for (var i in docs) { obj.meshes[docs[i]._id] = docs[i]; } // Get all meshes, including deleted ones.

            // Fetch all user groups from the database, keep this in memory
            obj.db.GetAllType('ugrp', function (err, docs) {
                obj.common.unEscapeAllLinksFieldName(docs);

                // Perform user group link cleanup
                for (var i in docs) {
                    const ugrp = docs[i];
                    if (ugrp.links != null) {
                        for (var j in ugrp.links) {
                            if (j.startsWith('user/') && (obj.users[j] == null)) { delete ugrp.links[j]; } // User group has a link to a user that does not exist
                            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
                        }
                    }
                    obj.userGroups[docs[i]._id] = docs[i]; // Get all user groups
                }

                // Mapping between users and groups
                for (var ugrpId in obj.userGroups) {
                    const ugrp = obj.userGroups[ugrpId];
                    if (ugrp.links != null) {
                        for (var userId in ugrp.links) {
                            if (userId.startsWith('user/') && (obj.users[userId] != null)) {
                                const user = obj.users[userId];
                                if (user.links == null) { user.links = {}; }
                                if (user.links[ugrpId] == null) {
                                    // Adding group link to user
                                    user.links[ugrpId] = { rights: ugrp.links[userId].rights || 1 };
                                }
                            }
                        }
                    }
                }

                // Perform device group link cleanup
                for (var i in obj.meshes) {
                    const mesh = obj.meshes[i];
                    if (mesh.links != null) {
                        for (var j in mesh.links) {
                            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
                            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
                        }
                    }
                }

                // Perform user link cleanup
                for (var i in obj.users) {
                    const user = obj.users[i];
                    if (user.links != null) {
                        for (var j in user.links) {
                            if (j.startsWith('ugrp/') && (obj.userGroups[j] == null)) { delete user.links[j]; } // User has a link to a user group that does not exist
                            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
                            //else if (j.startsWith('node/') && (obj.nodes[j] == null)) { delete user.links[j]; } // TODO
                        }
                        //if (Object.keys(user.links).length == 0) { delete user.links; }
                    }
                }

                // We loaded the users, device groups and user group state, start the server
                serverStart();
            });
        });
    });

    // Clean up a device, used before saving it in the database
    obj.cleanDevice = function (device) {
        // Check device links, if a link points to an unknown user, remove it.
        if (device.links != null) {
            for (var j in device.links) {
                if ((obj.users[j] == null) && (obj.userGroups[j] == null)) {
                    delete device.links[j];
                    if (Object.keys(device.links).length == 0) { delete device.links; }
                }
            }
        }
        return device;
    }

    // Return statistics about this web server
    obj.getStats = function () {
        return {
            users: Object.keys(obj.users).length,
            meshes: Object.keys(obj.meshes).length,
            dnsDomains: Object.keys(obj.dnsDomains).length,
            relaySessionCount: obj.relaySessionCount,
            relaySessionErrorCount: obj.relaySessionErrorCount,
            wsagents: Object.keys(obj.wsagents).length,
            wsagentsDisconnections: Object.keys(obj.wsagentsDisconnections).length,
            wsagentsDisconnectionsTimer: Object.keys(obj.wsagentsDisconnectionsTimer).length,
            wssessions: Object.keys(obj.wssessions).length,
            wssessions2: Object.keys(obj.wssessions2).length,
            wsPeerSessions: Object.keys(obj.wsPeerSessions).length,
            wsPeerSessions2: Object.keys(obj.wsPeerSessions2).length,
            wsPeerSessions3: Object.keys(obj.wsPeerSessions3).length,
            sessionsCount: Object.keys(obj.sessionsCount).length,
            wsrelays: Object.keys(obj.wsrelays).length,
            wsPeerRelays: Object.keys(obj.wsPeerRelays).length,
            tlsSessionStore: Object.keys(tlsSessionStore).length,
            blockedUsers: obj.blockedUsers,
            blockedAgents: obj.blockedAgents
        };
    }

    // Agent counters
    obj.agentStats = {
        createMeshAgentCount: 0,
        agentClose: 0,
        agentBinaryUpdate: 0,
        agentMeshCoreBinaryUpdate: 0,
        coreIsStableCount: 0,
        verifiedAgentConnectionCount: 0,
        clearingCoreCount: 0,
        updatingCoreCount: 0,
        recoveryCoreIsStableCount: 0,
        meshDoesNotExistCount: 0,
        invalidPkcsSignatureCount: 0,
        invalidRsaSignatureCount: 0,
        invalidJsonCount: 0,
        unknownAgentActionCount: 0,
        agentBadWebCertHashCount: 0,
        agentBadSignature1Count: 0,
        agentBadSignature2Count: 0,
        agentMaxSessionHoldCount: 0,
        invalidDomainMeshCount: 0,
        invalidMeshTypeCount: 0,
        invalidDomainMesh2Count: 0,
        invalidMeshType2Count: 0,
        duplicateAgentCount: 0,
        maxDomainDevicesReached: 0,
        agentInTrouble: 0,
        agentInBigTrouble: 0,
        agentRegistrationBlockCount: 0
    }
    obj.getAgentStats = function () { return obj.agentStats; }

    // Traffic counters
    obj.trafficStats = {
        httpRequestCount: 0,
        httpWebSocketCount: 0,
        httpIn: 0,
        httpOut: 0,
        relayCount: {},
        relayIn: {},
        relayOut: {},
        localRelayCount: {},
        localRelayIn: {},
        localRelayOut: {},
        AgentCtrlIn: 0,
        AgentCtrlOut: 0,
        LMSIn: 0,
        LMSOut: 0,
        CIRAIn: 0,
        CIRAOut: 0
    }
    obj.trafficStats.time = Date.now();
    obj.getTrafficStats = function () { return obj.trafficStats; }
    obj.getTrafficDelta = function (oldTraffic) { // Return the difference between the old and new data along with the delta time.
        const data = obj.common.Clone(obj.trafficStats);
        data.time = Date.now();
        const delta = calcDelta(oldTraffic ? oldTraffic : {}, data);
        if (oldTraffic && oldTraffic.time) { delta.delta = (data.time - oldTraffic.time); }
        delta.time = data.time;
        return { current: data, delta: delta }
    }
    function calcDelta(oldData, newData) { // Recursive function that computes the difference of all numbers
        const r = {};
        for (var i in newData) {
            if (typeof newData[i] == 'object') { r[i] = calcDelta(oldData[i] ? oldData[i] : {}, newData[i]); }
            if (typeof newData[i] == 'number') { if (typeof oldData[i] == 'number') { r[i] = (newData[i] - oldData[i]); } else { r[i] = newData[i]; } }
        }
        return r;
    }

    // Keep a record of the last agent issues.
    obj.getAgentIssues = function () { return obj.agentIssues; }
    obj.setAgentIssue = function (agent, issue) {
        var addrport = agent.issueRemoteAddrPort || agent.remoteaddrport || '';

		if (!addrport) {
			var addr = agent.issueRemoteAddr || agent.remoteaddr || '';

			if (!addr && agent.ws && agent.ws._socket && agent.ws._socket.remoteAddress) {
				addr = agent.ws._socket.remoteAddress;
			}

			addrport = addr;
		}
        obj.agentIssues.push([new Date().toLocaleString(), addrport, issue]);
        while (obj.agentIssues.length > 50) { obj.agentIssues.shift(); }
    }
    obj.agentIssues = [];

    // Authenticate the user
    obj.authenticate = function (name, pass, domain, fn) {
        if ((typeof (name) != 'string') || (typeof (pass) != 'string') || (typeof (domain) != 'object')) { fn(new Error('invalid fields')); return; }
        if (name.startsWith('~t:')) {
            // Login token, try to fetch the token from the database
            obj.db.Get('logintoken-' + name, function (err, docs) {
                if (err != null) { fn(err); return; }
                if ((docs == null) || (docs.length != 1)) { fn(new Error('login token not found')); return; }
                const loginToken = docs[0];
                if ((loginToken.expire != 0) && (loginToken.expire < Date.now())) { fn(new Error('login token expired')); return; }

                // Default strong password hashing (pbkdf2 SHA384)
                require('./pass').hash(pass, loginToken.salt, function (err, hash, tag) {
                    if (err) return fn(err);
                    if (hash == loginToken.hash) {
                        // Login username and password are valid.
                        var user = obj.users[loginToken.userid];
                        if (!user) { fn(new Error('cannot find user')); return; }
                        if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }

                        // Successful login token authentication
                        var loginOptions = { tokenName: loginToken.name, tokenUser: loginToken.tokenUser };
                        if (loginToken.expire != 0) { loginOptions.expire = loginToken.expire; }
                        return fn(null, user._id, null, loginOptions);
                    }
                    fn(new Error('invalid password'));
                }, 0);
            });
        } else if (domain.auth == 'ldap') {
            // This method will handle LDAP login
            const ldapHandler = function ldapHandlerFunc(err, xxuser) {
                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; }

                // Save this LDAP user to file if needed
                if (typeof domain.ldapsaveusertofile == 'string') {
                    obj.fs.appendFile(domain.ldapsaveusertofile, JSON.stringify(xxuser) + '\r\n\r\n', function (err) { });
                }

                // Work on getting the userid for this LDAP user
                var shortname = null;
                var username = xxuser['displayName'];
                if (typeof domain.ldapusername == 'string') {
                    if (domain.ldapusername.indexOf('{{{') >= 0) { username = assembleStringFromObject(domain.ldapusername, xxuser); } else { username = xxuser[domain.ldapusername]; }
                } else { username = xxuser['displayName'] ? xxuser['displayName'] : xxuser['name']; }
                if (domain.ldapuserbinarykey) {
                    // Use a binary key as the userid
                    if (xxuser[domain.ldapuserbinarykey]) { shortname = Buffer.from(xxuser[domain.ldapuserbinarykey], 'binary').toString('hex').toLowerCase(); }
                } else if (domain.ldapuserkey) {
                    // Use a string key as the userid
                    if (xxuser[domain.ldapuserkey]) { shortname = xxuser[domain.ldapuserkey]; }
                } else {
                    // Use the default key as the userid
                    if (xxuser['objectSid']) { shortname = Buffer.from(xxuser['objectSid'], 'binary').toString('hex').toLowerCase(); }
                    else if (xxuser['objectGUID']) { shortname = Buffer.from(xxuser['objectGUID'], 'binary').toString('hex').toLowerCase(); }
                    else if (xxuser['name']) { shortname = xxuser['name']; }
                    else if (xxuser['cn']) { shortname = xxuser['cn']; }
                }
                if (shortname == null) { fn(new Error('no user identifier')); if (ldapHandlerFunc.ldapobj) { try { ldapHandlerFunc.ldapobj.close(); } catch (ex) { console.log(ex); } } return; }
                if (username == null) { username = shortname; }
                var userid = 'user/' + domain.id + '/' + shortname;

                // Get the list of groups this user is a member of.
                var userMemberships = xxuser[(typeof domain.ldapusergroups == 'string') ? domain.ldapusergroups : 'memberOf'];
                if (typeof userMemberships == 'string') { userMemberships = [userMemberships]; }
                if (Array.isArray(userMemberships) == false) { userMemberships = []; }

                // See if the user is required to be part of an LDAP user group in order to log into this server.
                if (typeof domain.ldapuserrequiredgroupmembership == 'string') { domain.ldapuserrequiredgroupmembership = [domain.ldapuserrequiredgroupmembership]; }
                if (Array.isArray(domain.ldapuserrequiredgroupmembership)) {
                    // Look for a matching LDAP user group
                    var userMembershipMatch = false;
                    for (var i in domain.ldapuserrequiredgroupmembership) { if (userMemberships.indexOf(domain.ldapuserrequiredgroupmembership[i]) >= 0) { userMembershipMatch = true; } }
                    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
                }

                // Check if user is in an site administrator group
                var siteAdminGroup = null;
                if (typeof domain.ldapsiteadmingroups == 'string') { domain.ldapsiteadmingroups = [domain.ldapsiteadmingroups]; }
                if (Array.isArray(domain.ldapsiteadmingroups)) {
                    siteAdminGroup = false;
                    for (var i in domain.ldapsiteadmingroups) {
                        if (userMemberships.indexOf(domain.ldapsiteadmingroups[i]) >= 0) { siteAdminGroup = domain.ldapsiteadmingroups[i]; }
                    }
                }

                // See if we need to sync LDAP user memberships with user groups
                if (domain.ldapsyncwithusergroups === true) { domain.ldapsyncwithusergroups = {}; }
                if (typeof domain.ldapsyncwithusergroups == 'object') {
                    // LDAP user memberships sync is enabled, see if there are any filters to apply
                    if (typeof domain.ldapsyncwithusergroups.filter == 'string') { domain.ldapsyncwithusergroups.filter = [domain.ldapsyncwithusergroups.filter]; }
                    if (Array.isArray(domain.ldapsyncwithusergroups.filter)) {
                        const g = [];
                        for (var i in userMemberships) {
                            var match = false;
                            for (var j in domain.ldapsyncwithusergroups.filter) {
                                if (userMemberships[i].indexOf(domain.ldapsyncwithusergroups.filter[j]) >= 0) { match = true; }
                            }
                            if (match) { g.push(userMemberships[i]); }
                        }
                        userMemberships = g;
                    }
                } else {
                    // LDAP user memberships sync is disabled, sync the user with empty membership
                    userMemberships = [];
                }

                // Get the email address for this LDAP user
                var email = null;
                if (domain.ldapuseremail) { email = xxuser[domain.ldapuseremail]; } else if (xxuser['mail']) { email = xxuser['mail']; } // Use given field name or default
                if (Array.isArray(email)) { email = email[0]; } // Mail may be multivalued in LDAP in which case, answer is an array. Use the 1st value.
                if (email) { email = email.toLowerCase(); } // it seems some code elsewhere also lowercase the emailaddress, so let's be consistent.

                // Get the real name for this LDAP user
                var realname = null;
                if (typeof domain.ldapuserrealname == 'string') {
                    if (domain.ldapuserrealname.indexOf('{{{') >= 0) { realname = assembleStringFromObject(domain.ldapuserrealname, xxuser); } else { realname = xxuser[domain.ldapuserrealname]; }
                }
                else { if (typeof xxuser['name'] == 'string') { realname = xxuser['name']; } }

                // Get the phone number for this LDAP user
                var phonenumber = null;
                if (domain.ldapuserphonenumber) { phonenumber = xxuser[domain.ldapuserphonenumber]; }
                else { if (typeof xxuser['telephoneNumber'] == 'string') { phonenumber = xxuser['telephoneNumber']; } }

                // Work on getting the image of this LDAP user
                var userimage = null, userImageBuffer = null;
                if (xxuser._raw) { // Using _raw allows us to get data directly as buffer.
                    if (domain.ldapuserimage && xxuser[domain.ldapuserimage]) { userImageBuffer = xxuser._raw[domain.ldapuserimage]; }
                    else if (xxuser['thumbnailPhoto']) { userImageBuffer = xxuser._raw['thumbnailPhoto']; }
                    else if (xxuser['jpegPhoto']) { userImageBuffer = xxuser._raw['jpegPhoto']; }
                    if (userImageBuffer != null) {
                        if ((userImageBuffer[0] == 0xFF) && (userImageBuffer[1] == 0xD8) && (userImageBuffer[2] == 0xFF) && (userImageBuffer[3] == 0xE0)) { userimage = 'data:image/jpeg;base64,' + userImageBuffer.toString('base64'); }
                        if ((userImageBuffer[0] == 0x89) && (userImageBuffer[1] == 0x50) && (userImageBuffer[2] == 0x4E) && (userImageBuffer[3] == 0x47)) { userimage = 'data:image/png;base64,' + userImageBuffer.toString('base64'); }
                    }
                }

                // Display user information extracted from LDAP data
                parent.authLog('ldapHandler', 'LDAP user login, id: ' + shortname + ', username: ' + username + ', email: ' + email + ', realname: ' + realname + ', phone: ' + phonenumber + ', image: ' + (userimage != null));

                // If there is a testing userid, use that
                if (ldapHandlerFunc.ldapShortName) {
                    shortname = ldapHandlerFunc.ldapShortName;
                    userid = 'user/' + domain.id + '/' + shortname;
                }

                // Save the user image
                if (userimage != null) { parent.db.Set({ _id: 'im' + userid, image: userimage }); } else { db.Remove('im' + userid); }

                // Close the LDAP object
                if (ldapHandlerFunc.ldapobj) { try { ldapHandlerFunc.ldapobj.close(); } catch (ex) { console.log(ex); } }

                // Check if the user already exists
                var user = obj.users[userid];
                if (user == null) {
                    // This user does not exist, create a new account.
                    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 };
                    if (email) { user['email'] = email; user['emailVerified'] = true; }
                    if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; }
                    if (obj.common.validateStrArray(domain.newaccountrealms)) { user.groups = domain.newaccountrealms; }
                    var usercount = 0;
                    for (var i in obj.users) { if (obj.users[i].domain == domain.id) { usercount++; } }
                    if (usercount == 0) { user.siteadmin = 4294967295; /*if (domain.newaccounts === 2) { delete domain.newaccounts; }*/ } // If this is the first user, give the account site admin.

                    // Auto-join any user groups
                    if (typeof domain.newaccountsusergroups == 'object') {
                        for (var i in domain.newaccountsusergroups) {
                            var ugrpid = domain.newaccountsusergroups[i];
                            if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
                            var ugroup = obj.userGroups[ugrpid];
                            if (ugroup != null) {
                                // Add group to the user
                                if (user.links == null) { user.links = {}; }
                                user.links[ugroup._id] = { rights: 1 };

                                // Add user to the group
                                ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
                                db.Set(ugroup);

                                // Notify user group change
                                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 };
                                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.
                                parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
                            }
                        }
                    }

                    // Check the user real name
                    if (realname) { user.realname = realname; }

                    // Check the user phone number
                    if (phonenumber) { user.phone = phonenumber; }

                    // Indicate that this user has a image
                    if (userimage != null) { user.flags = 1; }

                    // See if the user is a member of the site admin group.
                    if (typeof siteAdminGroup === 'string') {
                        parent.authLog('ldapHandler', `LDAP: Granting site admin privilages to new user "${user.name}" found in admin group: ${siteAdminGroup}`);
                        user.siteadmin = 0xFFFFFFFF;
                    }

                    // Sync the user with LDAP matching user groups
                    if (syncExternalUserGroups(domain, user, userMemberships, 'ldap') == true) { userChanged = true; }

                    obj.users[user._id] = user;
                    obj.db.SetUser(user);
                    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 };
                    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.
                    obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
                    return fn(null, user._id);
                } else {
                    var userChanged = false;

                    // This is an existing user
                    // If the display username has changes, update it.
                    if (user.name != username) { user.name = username; userChanged = true; }

                    // Check if user email has changed
                    if (user.email && !email) { // email unset in ldap => unset
                        delete user.email;
                        delete user.emailVerified;
                        userChanged = true;
                    } else if (user.email != email) { // update email
                        user['email'] = email;
                        user['emailVerified'] = true;
                        userChanged = true;
                    }

                    // Check the user real name
                    if (realname != user.realname) { user.realname = realname; userChanged = true; }

                    // Check the user phone number
                    if (phonenumber != user.phone) { user.phone = phonenumber; userChanged = true; }

                    // Check the user image flag
                    if ((userimage != null) && ((user.flags == null) || ((user.flags & 1) == 0))) { if (user.flags == null) { user.flags = 1; } else { user.flags += 1; } userChanged = true; }
                    if ((userimage == null) && (user.flags != null) && ((user.flags & 1) != 0)) { if (user.flags == 1) { delete user.flags; } else { user.flags -= 1; } userChanged = true; }

                    // See if the user is a member of the site admin group.
                    if ((typeof siteAdminGroup === 'string') && (user.siteadmin !== 0xFFFFFFFF)) {
                        parent.authLog('ldapHandler', `LDAP: Granting site admin privilages to user "${user.name}" found in administrator group: ${siteAdminGroup}`);
                        user.siteadmin = 0xFFFFFFFF;
                        userChanged = true;
                    } else if ((siteAdminGroup === false) && (user.siteadmin === 0xFFFFFFFF)) {
                        parent.authLog('ldapHandler', `LDAP: Revoking site admin privilages from user "${user.name}" since they are not found in any administrator groups.`);
                        delete user.siteadmin;
                        userChanged = true;
                    }

                    // Synd the user with LDAP matching user groups
                    if (syncExternalUserGroups(domain, user, userMemberships, 'ldap') == true) { userChanged = true; }

                    // If the user changed, save the changes to the database here
                    if (userChanged) {
                        obj.db.SetUser(user);
                        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 };
                        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.
                        parent.DispatchEvent(['*', 'server-users', user._id], obj, event);
                    }

                    // If user is locker out, block here.
                    if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
                    return fn(null, user._id);
                }
            }

            if (domain.ldapoptions.url == 'test') {
                // Test LDAP login
                var xxuser = domain.ldapoptions[name.toLowerCase()];
                if (xxuser == null) { fn(new Error('invalid password')); return; } else {
                    ldapHandler.ldapShortName = name.toLowerCase();
                    if (typeof xxuser == 'string') {
                        // The test LDAP user points to a JSON file where the user information is, load it.
                        ldapHandler(null, require(xxuser));
                    } else {
                        // The test user information is in the config.json, use it.
                        ldapHandler(null, xxuser);
                    }
                }
            } else {
                // LDAP login
                var LdapAuth = require('ldapauth-fork');
                if (domain.ldapoptions == null) { domain.ldapoptions = {}; }
                domain.ldapoptions.includeRaw = true; // This allows us to get data as buffers which is useful for images.
                var ldap = new LdapAuth(domain.ldapoptions);
                ldapHandler.ldapobj = ldap;
                ldap.on('error', function (err) { parent.debug('ldap', 'LDAP OnError: ' + err); try { ldap.close(); } catch (ex) { console.log(ex); } }); // Close the LDAP object
                ldap.authenticate(name, pass, ldapHandler);
            }
        } else {
            // Regular login
            var user = obj.users['user/' + domain.id + '/' + name.toLowerCase()];
            // Query the db for the given username
            if (!user) { fn(new Error('cannot find user')); return; }
            // Apply the same algorithm to the POSTed password, applying the hash against the pass / salt, if there is a match we found the user
            if (user.salt == null) {
                fn(new Error('invalid password'));
            } else {
                if (user.passtype != null) {
                    // IIS default clear or weak password hashing (SHA-1)
                    require('./pass').iishash(user.passtype, pass, user.salt, function (err, hash) {
                        if (err) return fn(err);
                        if (hash == user.hash) {
                            // Update the password to the stronger format.
                            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);
                            if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
                            return fn(null, user._id);
                        }
                        fn(new Error('invalid password'), null, user.passhint);
                    });
                } else {
                    // Default strong password hashing (pbkdf2 SHA384)
                    require('./pass').hash(pass, user.salt, function (err, hash, tag) {
                        if (err) return fn(err);
                        if (hash == user.hash) {
                            if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; }
                            return fn(null, user._id);
                        }
                        fn(new Error('invalid password'), null, user.passhint);
                    }, 0);
                }
            }
        }
    };

    /*
    obj.restrict = function (req, res, next) {
        console.log('restrict', req.url);
        var domain = getDomain(req);
        if (req.session.userid) {
            next();
        } else {
            req.session.messageid = 111; // Access denied.
            res.redirect(domain.url + 'login');
        }
    };
    */

    // Check if the source IP address is in the IP list, return false if not.
    function checkIpAddressEx(req, res, ipList, closeIfThis, redirectUrl) {
        try {
            if (req.connection) {
                // HTTP(S) request
                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; } } }
                if (closeIfThis === false) { if (typeof redirectUrl == 'string') { res.redirect(redirectUrl); } else { res.sendStatus(401); } }
            } else {
                // WebSocket request
                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; } } }
                if (closeIfThis === false) { try { req.close(); } catch (e) { } }
            }
        } catch (e) { console.log(e); } // Should never happen
        return false;
    }

    // Check if the source IP address is allowed, return domain if allowed
    // If there is a fail and null is returned, the request or connection is closed already.
    function checkUserIpAddress(req, res) {
        if ((parent.config.settings.userblockedip != null) && (checkIpAddressEx(req, res, parent.config.settings.userblockedip, true, parent.config.settings.ipblockeduserredirect) == true)) { obj.blockedUsers++; return null; }
        if ((parent.config.settings.userallowedip != null) && (checkIpAddressEx(req, res, parent.config.settings.userallowedip, false, parent.config.settings.ipblockeduserredirect) == false)) { obj.blockedUsers++; return null; }
        const domain = (req.url ? getDomain(req) : getDomain(res));
        if (domain == null) { parent.debug('web', 'handleRootRequest: invalid domain.'); try { res.sendStatus(404); } catch (ex) { } return; }
        if ((domain.userblockedip != null) && (checkIpAddressEx(req, res, domain.userblockedip, true, domain.ipblockeduserredirect) == true)) { obj.blockedUsers++; return null; }
        if ((domain.userallowedip != null) && (checkIpAddressEx(req, res, domain.userallowedip, false, domain.ipblockeduserredirect) == false)) { obj.blockedUsers++; return null; }
        return domain;
    }

    // Check if the source IP address is allowed, return domain if allowed
    // If there is a fail and null is returned, the request or connection is closed already.
    function checkAgentIpAddress(req, res) {
        if ((parent.config.settings.agentblockedip != null) && (checkIpAddressEx(req, res, parent.config.settings.agentblockedip, null) == true)) { obj.blockedAgents++; return null; }
        if ((parent.config.settings.agentallowedip != null) && (checkIpAddressEx(req, res, parent.config.settings.agentallowedip, null) == false)) { obj.blockedAgents++; return null; }
        const clientIp = (req.clientIp != null) ? req.clientIp : res.clientIp;
        if ((obj.agentRegistrationLimiter != null) && obj.agentRegistrationLimiter.isBlocked(clientIp)) {
            obj.blockedAgents++;
            obj.agentStats.agentRegistrationBlockCount++;
            parent.debug('agent', 'Agent connection from ' + clientIp + ' blocked by new registration limit.');
            return null;
        }
        const domain = (req.url ? getDomain(req) : getDomain(res));
        if ((domain.agentblockedip != null) && (checkIpAddressEx(req, res, domain.agentblockedip, null) == true)) { obj.blockedAgents++; return null; }
        if ((domain.agentallowedip != null) && (checkIpAddressEx(req, res, domain.agentallowedip, null) == false)) { obj.blockedAgents++; return null; }
        return domain;
    }

    // True when the address is IPv4/IPv6 loopback (not a general private-network address)
    function isLoopbackAddress(addr) {
        if (typeof addr != 'string') { return false; }
        if (addr.indexOf('::ffff:') == 0) { addr = addr.substring(7); }
        if ((addr == '127.0.0.1') || (addr == '::1') || (addr == 'localhost')) { return true; }
        return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(addr);
    }

    // Domainid URL selection is only for real local connections, not a Host: localhost header alone
    function isLocalhostDomainSelect(req) {
        if ((req == null) || (req.hostname != 'localhost') || (req.query == null) || (req.query.domainid == null)) { return false; }
        if (isLoopbackAddress(req.clientIp) == false) { return false; }
        var peer = null;
        if ((req.connection != null) && (typeof req.connection.remoteAddress == 'string')) { peer = req.connection.remoteAddress; }
        else if ((req.socket != null) && (typeof req.socket.remoteAddress == 'string')) { peer = req.socket.remoteAddress; }
        if ((peer != null) && (isLoopbackAddress(peer) == false)) { return false; }
        return true;
    }

    // Return the current domain of the request
    // Request or connection says open regardless of the response
    function getDomain(req) {
        if (req.xdomain != null) { return req.xdomain; } // Domain already set for this request, return it.
        if (isLocalhostDomainSelect(req)) { const d = parent.config.domains[req.query.domainid]; if (d != null) return d; } // Local access with the domainid specified in the URL
        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.
        const x = req.url.split('/');
        if (x.length < 2) return parent.config.domains[''];
        const y = parent.config.domains[x[1].toLowerCase()];
        if ((y != null) && (y.dns == null)) { return parent.config.domains[x[1].toLowerCase()]; }
        return parent.config.domains[''];
    }

    function parseAllowedFramingOrigins(val) {
        if (val == null) return [];
        var arr = [];
        if (Array.isArray(val)) { arr = val.slice(); } else if (typeof val == 'string') { arr = val.split(',').map(function (s) { return s.trim(); }).filter(function (s) { return s.length > 0; }); } else { return []; }
        var out = [];
        for (var i = 0; i < arr.length; i++) {
            var o = arr[i].trim().replace(/\/+$/, '');
            if (o.length === 0) continue;
            if (o.indexOf('https://') === 0 || o.indexOf('http://') === 0) { out.push(o); }
        }
        return out;
    }

    function handleLogoutRequest(req, res) {
        const domain = checkUserIpAddress(req, res);
        if (domain == null) { return; }
        if (domain.auth == 'sspi') { parent.debug('web', 'handleLogoutRequest: failed checks.'); res.sendStatus(404); return; }
        if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key

        // If a HTTP header is required, check new UserRequiredHttpHeader
        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; } }

        res.set({ 'Cache-Control': 'no-store' });
        // Destroy the user's session to log them out will be re-created next request
        var userid = req.session.userid;
        var oidcLogoutUrl = null;
        if (userid != null) { oidcLogoutUrl = obj.oidcLogoutUrls[userid]; delete obj.oidcLogoutUrls[userid]; }
        if (req.session.userid) {
            var user = obj.users[req.session.userid];
            if (user != null) {
                obj.parent.authLog('https', 'User ' + user.name + ' logout from ' + req.clientIp + ' port ' + req.connection.remotePort, { sessionid: req.session.x, useragent: req.headers['user-agent'] });
                obj.parent.DispatchEvent(['*'], obj, { etype: 'user', userid: user._id, username: user.name, action: 'logout', msgid: 2, msg: 'Account logout', domain: domain.id });
            }
            if (req.session.x) { clearDestroyedSessions(); obj.destroyedSessions[req.session.userid + '/' + req.session.x] = Date.now(); } // Destroy this session
        }
        req.session = null;
        parent.debug('web', 'handleLogoutRequest: success.');

        // If this user was logged in using an authentication strategy and there is a logout URL, use it.
        if ((userid != null) && (domain.authstrategies?.authStrategyFlags != null)) {
            let logouturl = null;
            let userStrategy = ((userid.split('/')[2]).split(':')[0]).substring(1);
            // Setup logout url for oidc
            if (userStrategy == 'oidc' && domain.authstrategies.oidc != null) {
                if (typeof domain.authstrategies.oidc.logouturl == 'string') {
                    logouturl = domain.authstrategies.oidc.logouturl;
                } else if (typeof oidcLogoutUrl == 'string') {
                    logouturl = oidcLogoutUrl;
                } else if (typeof domain.authstrategies.oidc.issuer.end_session_endpoint == 'string' && typeof domain.authstrategies.oidc.client.post_logout_redirect_uri == 'string') {
                    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;
                } else if (typeof domain.authstrategies.oidc.issuer.end_session_endpoint == 'string') {
                    logouturl = domain.authstrategies.oidc.issuer.end_session_endpoint;
                }
                // Log out all other strategies
            } else if ((domain.authstrategies[userStrategy] != null) && (typeof domain.authstrategies[userStrategy].logouturl == 'string')) { logouturl = domain.authstrategies[userStrategy].logouturl; }
            // If custom logout was setup, use it
            if (logouturl != null) {
                parent.authLog('handleLogoutRequest', userStrategy.toUpperCase() + ': LOGOUT');
                res.redirect(logouturl);
                return;
            }
        }

        // This is the default logout redirect to the login page
        if (req.query.key != null) { res.redirect(domain.url + 'login?key=' + encodeURIComponent(req.query.key)); } else { res.redirect(domain.url + 'login'); }
    }

    // Return an object with 2FA type if 2-step auth can be skipped
    function checkUserOneTimePasswordSkip(domain, user, req, loginOptions) {
        if (parent.config.settings.no2factorauth == true) return null;

        // If this login occurred using a login token, no 2FA needed.
        if ((loginOptions != null) && (typeof loginOptions.tokenName === 'string')) { return { twoFactorType: 'tokenlogin' }; }

        // Check if we can skip 2nd factor auth because of the source IP address
        if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
            for (var i in domain.passwordrequirements.skip2factor) { if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) { return { twoFactorType: 'ipaddr' }; } }
        }

        // Check if a 2nd factor cookie is present
        if (typeof req.headers.cookie == 'string') {
            const cookies = req.headers.cookie.split('; ');
            for (var i in cookies) {
                if (cookies[i].startsWith('twofactor=')) {
                    var twoFactorCookie = null;
                    try { twoFactorCookie = obj.parent.decodeCookie(decodeURIComponent(cookies[i].substring(10)), obj.parent.loginCookieEncryptionKey, (30 * 24 * 60)); } catch (ex) { }
                    if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { return { twoFactorType: 'cookie' }; }
                }
            }
        }

        return null;
    }

    // Return true if this user has 2-step auth active
    function checkUserOneTimePasswordRequired(domain, user, req, loginOptions) {
        // If this login occurred using a login token, no 2FA needed.
        if ((loginOptions != null) && (typeof loginOptions.tokenName === 'string')) { return false; }

        // Check if we can skip 2nd factor auth because of the source IP address
        if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
            for (var i in domain.passwordrequirements.skip2factor) { if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) return false; }
        }

        // Check if a 2nd factor cookie is present
        if (typeof req.headers.cookie == 'string') {
            const cookies = req.headers.cookie.split('; ');
            for (var i in cookies) {
                if (cookies[i].startsWith('twofactor=')) {
                    var twoFactorCookie = null;
                    try { twoFactorCookie = obj.parent.decodeCookie(decodeURIComponent(cookies[i].substring(10)), obj.parent.loginCookieEncryptionKey, (30 * 24 * 60)); } catch (ex) { }
                    if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { return false; }
                }
            }
        }

        // Check if a 2nd factor is present
        return userHasSecondFactor(domain, user);
    }

    // Return true if this user has at least one second factor set up
    function userHasSecondFactor(domain, user) {
        if (parent.config.settings.no2factorauth === true) return false;

        // See if SMS 2FA is available
        var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));

        // See if Messenger 2FA is available
        var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));

        return (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)));
    }

    // Return true when force2factor applies to this user and no second factor is set up yet.
    // Same conditions as the 0x00040000 feature flag, so the server enforces what the web pages show.
    obj.force2factorPending = function (domain, user, req) {
        if ((user == null) || (obj.args.nousers == true) || (parent.config.settings.no2factorauth === true) || (domain.auth == 'sspi')) return false;
        if ((domain.passwordrequirements == null) || (domain.passwordrequirements.force2factor !== true) || (user._id.split('/')[2][0] == '~')) return false;
        if (obj.parent.certificates.CommonName.indexOf('.') == -1) return false; // 2FA can't be set up without a server name
        if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements.skip2factor != null)) {
            for (var i in domain.passwordrequirements.skip2factor) { if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) return false; }
        }
        return (userHasSecondFactor(domain, user) == false);
    }

    // Check the 2-step auth token
    function checkUserOneTimePassword(req, domain, user, token, hwtoken, func) {
        parent.debug('web', 'checkUserOneTimePassword()');
        const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.nousers !== true) && (parent.config.settings.no2factorauth !== true));
        if (twoStepLoginSupported == false) { parent.debug('web', 'checkUserOneTimePassword: not supported.'); func(true); return; };
        if (obj.checkAllow2Fa(req) == false) { func(false); return; }
        if (typeof token != 'string') { token = null; } // A repeated form field arrives as an array
        if (typeof hwtoken != 'string') { hwtoken = null; }

        // Count bad tokens here so the websocket logins share the limit with the login page. Asking for a code to be sent is not an attempt.
        const func2 = func, attempted = (((token != null) && (token != '') && (['**email**', '**sms**', '**msg**'].indexOf(token) == -1)) || ((hwtoken != null) && (hwtoken != '') && (['**sms**', '**msg**'].indexOf(hwtoken) == -1)));
        func = function (result, authData) { if ((result !== true) && attempted) { obj.setbad2Fa(req); } func2(result, authData); }

        // Check if we can use OTP tokens with email
        var otpemail = (domain.mailserver != null);
        if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; }
        var otpsms = (parent.smsserver != null);
        if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; }
        var otpmsg = ((parent.msgserver != null) && (parent.msgserver.providers != 0));
        if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.msg2factor == false)) { otpmsg = false; }

        // Check 2FA login cookie
        if ((token != null) && (token.startsWith('cookie='))) {
            var twoFactorCookie = null;
            try { twoFactorCookie = obj.parent.decodeCookie(decodeURIComponent(token.substring(7)), obj.parent.loginCookieEncryptionKey, (30 * 24 * 60)); } catch (ex) { }
            if ((twoFactorCookie != null) && ((twoFactorCookie.ip == null) || checkCookieIp(twoFactorCookie.ip, req.clientIp)) && (twoFactorCookie.userid == user._id)) { func(true, { twoFactorType: 'cookie' }); return; }
        }

        // Check email key
        if ((otpemail) && (user.otpekey != null) && (user.otpekey.d != null) && (user.otpekey.k === token)) {
            var deltaTime = (Date.now() - user.otpekey.d);
            if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the email token (10000 * 60 * 5).
                user.otpekey = {};
                obj.db.SetUser(user);
                parent.debug('web', 'checkUserOneTimePassword: success (email).');
                func(true, { twoFactorType: 'email' });
                return;
            }
        }

        // Check SMS key
        if ((otpsms) && (user.phone != null) && (user.otpsms != null) && (user.otpsms.d != null) && (user.otpsms.k === token)) {
            var deltaTime = (Date.now() - user.otpsms.d);
            if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the SMS token (10000 * 60 * 5).
                delete user.otpsms;
                obj.db.SetUser(user);
                parent.debug('web', 'checkUserOneTimePassword: success (SMS).');
                func(true, { twoFactorType: 'sms' });
                return;
            }
        }

        // Check messenger key
        if ((otpmsg) && (user.msghandle != null) && (user.otpmsg != null) && (user.otpmsg.d != null) && (user.otpmsg.k === token)) {
            var deltaTime = (Date.now() - user.otpmsg.d);
            if ((deltaTime > 0) && (deltaTime < 300000)) { // Allow 5 minutes to use the Messenger token (10000 * 60 * 5).
                delete user.otpmsg;
                obj.db.SetUser(user);
                parent.debug('web', 'checkUserOneTimePassword: success (Messenger).');
                func(true, { twoFactorType: 'messenger' });
                return;
            }
        }

        // Check hardware key
        if (user.otphkeys && (user.otphkeys.length > 0) && (typeof (hwtoken) == 'string') && (hwtoken.length > 0)) {
            var authResponse = null;
            try { authResponse = JSON.parse(hwtoken); } catch (ex) { }
            if ((authResponse != null) && (typeof authResponse.id == 'string') && (typeof authResponse.authenticatorData == 'string') && (typeof authResponse.clientDataJSON == 'string') && (typeof authResponse.signature == 'string') && (typeof authResponse.userHandle == 'string')) {
                // Get all WebAuthn keys
                var webAuthnKeys = [];
                for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 3) { webAuthnKeys.push(user.otphkeys[i]); } }
                if (webAuthnKeys.length > 0) {
                    // Decode authentication response
                    var clientAssertionResponse = { response: {} };
                    clientAssertionResponse.id = authResponse.id;
                    clientAssertionResponse.rawId = Buffer.from(authResponse.id, 'base64');
                    clientAssertionResponse.response.authenticatorData = Buffer.from(authResponse.authenticatorData, 'base64');
                    clientAssertionResponse.response.clientDataJSON = Buffer.from(authResponse.clientDataJSON, 'base64');
                    clientAssertionResponse.response.signature = Buffer.from(authResponse.signature, 'base64');
                    clientAssertionResponse.response.userHandle = Buffer.from(authResponse.userHandle, 'base64');

                    // Look for the key with clientAssertionResponse.id
                    var webAuthnKey = null;
                    for (var i = 0; i < webAuthnKeys.length; i++) { if (webAuthnKeys[i].keyId == clientAssertionResponse.id) { webAuthnKey = webAuthnKeys[i]; } }

                    // If we found a valid key to use, let's validate the response
                    if (webAuthnKey != null) {
                        // Figure out the origin
                        var httpport = ((args.aliasport != null) ? args.aliasport : args.port);
                        var origin = 'https://' + (domain.dns ? domain.dns : parent.certificates.CommonName);
                        if (httpport != 443) { origin += ':' + httpport; }

                        var u2fchallenge = null;
                        if ((req.session != null) && (req.session.e != null)) { const sec = parent.decryptSessionData(req.session.e); if (sec != null) { u2fchallenge = sec.u2f; } }
                        var assertionExpectations = {
                            challenge: u2fchallenge,
                            origin: origin,
                            factor: 'either',
                            fmt: 'fido-u2f',
                            publicKey: webAuthnKey.publicKey,
                            prevCounter: webAuthnKey.counter,
                            userHandle: Buffer.from(user._id, 'binary').toString('base64')
                        };

                        var webauthnResponse = null;
                        try { webauthnResponse = obj.webauthn.verifyAuthenticatorAssertionResponse(clientAssertionResponse.response, assertionExpectations); } catch (ex) { parent.debug('web', 'checkUserOneTimePassword: exception ' + ex); console.log(ex); }
                        if ((webauthnResponse != null) && (webauthnResponse.verified === true)) {
                            // Update the hardware key counter and accept the 2nd factor
                            webAuthnKey.counter = webauthnResponse.counter;
                            obj.db.SetUser(user);
                            parent.debug('web', 'checkUserOneTimePassword: success (hardware).');
                            func(true, { twoFactorType: 'fido' });
                        } else {
                            parent.debug('web', 'checkUserOneTimePassword: fail (hardware).');
                            func(false);
                        }
                        return;
                    }
                }
            }
        }

        // Check Google Authenticator
        if (user.otpsecret && (typeof (token) == 'string') && (token.length == 6)){
            const otplib = require('otplib');
            var verified = null;
            try {
                verified = otplib.verifySync({ 
                    epochTolerance: 60, 
                    token: token, 
                    secret: user.otpsecret,
                    guardrails: otplib.createGuardrails({
                        MIN_SECRET_BYTES: 10, // https://github.com/yeojz/otplib/issues/671#issuecomment-4368647105
                    })
                });
            } catch (ex) { parent.debug('web', 'checkUserOneTimePassword: exception ' + ex); }
            if ((verified != null) && (verified.valid === true)) {
                parent.debug('web', 'checkUserOneTimePassword: success (authenticator).');
                func(true, { twoFactorType: 'otp' });
                return;
            }
        };

        // Check written down keys
        if ((user.otpkeys != null) && (user.otpkeys.keys != null) && (typeof (token) == 'string') && (token.length == 8)) {
            var tokenNumber = parseInt(token);
            for (var i = 0; i < user.otpkeys.keys.length; i++) {
                if ((tokenNumber === user.otpkeys.keys[i].p) && (user.otpkeys.keys[i].u === true)) {
                    parent.debug('web', 'checkUserOneTimePassword: success (one-time).');
                    user.otpkeys.keys[i].u = false; obj.db.SetUser(user); func(true, { twoFactorType: 'backup' }); return;
                }
            }
        }

        // Check OTP hardware key (Yubikey OTP)
        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)) {
            var keyId = token.substring(0, 12);

            // Find a matching OTP key
            var match = false;
            for (var i = 0; i < user.otphkeys.length; i++) { if ((user.otphkeys[i].type === 2) && (user.otphkeys[i].keyid === keyId)) { match = true; } }

            // If we have a match, check the OTP
            if (match === true) {
                var yub = require('yub');
                yub.init(domain.yubikey.id, domain.yubikey.secret);
                yub.verify(token, function (err, results) {
                    if ((results != null) && (results.status == 'OK')) {
                        parent.debug('web', 'checkUserOneTimePassword: success (Yubikey).');
                        func(true, { twoFactorType: 'hwotp' });
                    } else {
                        parent.debug('web', 'checkUserOneTimePassword: fail (Yubikey).');
                        func(false);
                    }
                });
                return;
            }
        }

        parent.debug('web', 'checkUserOneTimePassword: fail (2).');
        func(false);
    }

    // Return a U2F hardware key challenge
    function getHardwareKeyChallenge(req, domain, user, func) {
        var sec = {};
        if (req.session == null) { req.session = {}; } else { try { sec = parent.decryptSessionData(req.session.e); } catch (ex) { } }

        if (user.otphkeys && (user.otphkeys.length > 0)) {
            // Get all WebAuthn keys
            var webAuthnKeys = [];
            for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 3) { webAuthnKeys.push(user.otphkeys[i]); } }
            if (webAuthnKeys.length > 0) {
                // Generate a Webauthn challenge, this is really easy, no need to call any modules to do this.
                var authnOptions = { type: 'webAuthn', keyIds: [], timeout: 60000, challenge: obj.crypto.randomBytes(64).toString('base64') };
                // userVerification: 'preferred' use security pin if possible (default), 'required' always use security pin, 'discouraged' do not use security pin.
                authnOptions.userVerification = (domain.passwordrequirements && domain.passwordrequirements.fidopininput) ? domain.passwordrequirements.fidopininput : 'preferred'; // Use the domain setting if it exists, otherwise use 'preferred'.{
                for (var i = 0; i < webAuthnKeys.length; i++) { authnOptions.keyIds.push(webAuthnKeys[i].keyId); }
                sec.u2f = authnOptions.challenge;
                req.session.e = parent.encryptSessionData(sec);
                parent.debug('web', 'getHardwareKeyChallenge: success');
                func(JSON.stringify(authnOptions));
                return;
            }
        }

        // Remove the challenge if present
        if (sec.u2f != null) { delete sec.u2f; req.session.e = parent.encryptSessionData(sec); }

        parent.debug('web', 'getHardwareKeyChallenge: fail');
        func('');
    }

    // Redirect a root request to a different page
    function handleRootRedirect(req, res, direct) {
        const domain = checkUserIpAddress(req, res);
        if (domain == null) { return; }
        res.redirect(domain.rootredirect + getQueryPortion(req));
    }

    function handleLoginRequest(req, res, direct) {
        const domain = checkUserIpAddress(req, res);
        if (domain == null) { return; }
        if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
        if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed
        if (req.session == null) { req.session = {}; }

        const blockingMode = parent.config.settings.maxinvalidlogin?.blocking || 'iprange';

        // Check if this is a banned ip address
        if (blockingMode !== 'username' && obj.checkAllowLogin(req) === false) {
            // Wait and redirect the user
            setTimeout(function () {
                req.session.messageid = 114; // IP address blocked, try again later.
                if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
            }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
            return;
        }

        // Normally, use the body username/password. If this is a token, use the username/password in the session.
        var xusername = req.body.username, xpassword = req.body.password;
        if ((xusername == null) && (xpassword == null) && (req.body.token != null)) {
            const sec = parent.decryptSessionData(req.session.e);
            xusername = sec.tuser; xpassword = sec.tpass;
        }

        // Check if the user is locked out
        if (blockingMode === 'username' && obj.checkAllowLogin(null, xusername) === false) {
            // Wait and redirect the user
            setTimeout(function () {
                req.session.messageid = 110; // Account locked
                if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
            }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
            return;
        }

        // Authenticate the user
        obj.authenticate(xusername, xpassword, domain, function (err, userid, passhint, loginOptions) {
            if (userid) {
                var user = obj.users[userid];

                // Check if we are in maintenance mode
                if ((parent.config.settings.maintenancemode != null) && (user.siteadmin != 4294967295)) {
                    req.session.messageid = 115; // Server under maintenance
                    req.session.loginmode = 1;
                    if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                    return;
                }

                var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.email != null) && (user.emailVerified == true) && (user.otpekey != null));
                var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
                var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
                var push2fa = ((parent.firebase != null) && (user.otpdev != null));
                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));

                // Check if two factor can be skipped
                const twoFactorSkip = checkUserOneTimePasswordSkip(domain, user, req, loginOptions);

                // Check if this user has 2-step login active
                if ((twoFactorSkip == null) && (req.session.loginmode != 6) && checkUserOneTimePasswordRequired(domain, user, req, loginOptions)) {
                    if ((req.body.hwtoken == '**timeout**')) {
                        delete req.session; // Clear the session
                        res.redirect(domain.url + getQueryPortion(req));
                        return;
                    }

                    if ((req.body.hwtoken == '**email**') && email2fa) {
                        user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
                        obj.db.SetUser(user);
                        parent.debug('web', 'Sending 2FA email to: ' + user.email);
                        domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key);
                        req.session.messageid = 2; // "Email sent" message
                        req.session.loginmode = 4;
                        if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                        return;
                    }

                    if ((req.body.hwtoken == '**sms**') && sms2fa) {
                        // Cause a token to be sent to the user's phone number
                        user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
                        obj.db.SetUser(user);
                        parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
                        parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
                        // Ask for a login token & confirm sms was sent
                        req.session.messageid = 4; // "SMS sent" message
                        req.session.loginmode = 4;
                        if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                        return;
                    }

                    if ((req.body.hwtoken == '**msg**') && msg2fa) {
                        // Cause a token to be sent to the user's messenger account
                        user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
                        obj.db.SetUser(user);
                        parent.debug('web', 'Sending 2FA message to: ' + user.msghandle);
                        parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req));
                        // Ask for a login token & confirm message was sent
                        req.session.messageid = 6; // "Message sent" message
                        req.session.loginmode = 4;
                        if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                        return;
                    }

                    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')) {
                        // Redirect to duo here
                        const duo = require('@duosecurity/duo_universal');
                        const client = new duo.Client({
                            clientId: domain.duo2factor.integrationkey,
                            clientSecret: domain.duo2factor.secretkey,
                            apiHost: domain.duo2factor.apihostname,
                            redirectUrl: obj.generateBaseURL(domain, req) + 'auth-duo' + (domain.loginkey != null ? ('?key=' + domain.loginkey) : '')
                        });
                        // Decrypt any session data
                        const sec = parent.decryptSessionData(req.session.e);
                        sec.duostate = client.generateState();
                        req.session.e = parent.encryptSessionData(sec);
                        parent.debug('web', 'Redirecting user ' + user._id + ' to Duo');
                        res.redirect(client.createAuthUrl(user._id.split('/')[2], sec.duostate));
                        return;
                    }

                    // Handle device push notification 2FA request
                    // We create a browser cookie, send it back and when the browser connects it's web socket, it will trigger the push notification.
                    if ((req.body.hwtoken == '**push**') && push2fa && ((domain.passwordrequirements == null) || (domain.passwordrequirements.push2factor != false))) {
                        const logincodeb64 = Buffer.from(obj.common.zeroPad(getRandomSixDigitInteger(), 6)).toString('base64');
                        const sessioncode = obj.crypto.randomBytes(24).toString('base64');

                        // Create a browser cookie so the browser can connect using websocket and wait for device accept/reject.
                        const browserCookie = parent.encodeCookie({ a: 'waitAuth', c: logincodeb64, u: user._id, n: user.otpdev, s: sessioncode, d: domain.id });

                        // Get the HTTPS port
                        var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port if specified

                        // Get the agent connection server name
                        var serverName = obj.getWebServerName(domain, req);
                        if (typeof obj.args.agentaliasdns == 'string') { serverName = obj.args.agentaliasdns; }

                        // Build the connection URL. If we are using a sub-domain or one with a DNS, we need to craft the URL correctly.
                        var xdomain = (domain.dns == null) ? domain.id : '';
                        if (xdomain != '') xdomain += '/';
                        var url = 'wss://' + serverName + ':' + httpsPort + '/' + xdomain + '2fahold.ashx?c=' + browserCookie;

                        // Request that the login page wait for device auth
                        req.session.messageid = 5; // "Sending notification..." message
                        req.session.passhint = url;
                        req.session.loginmode = 8;
                        if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                        return;
                    }

                    checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result, authData) {
                        if (result == false) {
                            var randomWaitTime = 0;

                            // Check if 2FA is allowed for this IP address
                            if (obj.checkAllow2Fa(req) == false) {
                                // Wait and redirect the user
                                setTimeout(function () {
                                    req.session.messageid = 114; // IP address blocked, try again later.
                                    if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                                }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
                                return;
                            }

                            // 2-step auth is required, but the token is not present or not valid.
                            if ((req.body.token != null) || (req.body.hwtoken != null)) {
                                randomWaitTime = 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095); // This is a fail, wait a random time. 2 to 6 seconds.
                                req.session.messageid = 108; // Invalid token, try again.
                                obj.parent.authLog('https', 'Failed 2FA for ' + xusername + ' from ' + cleanRemoteAddr(req.clientIp) + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'] });
                                parent.debug('web', 'handleLoginRequest: invalid 2FA token');
                                const ua = obj.getUserAgentInfo(req);
                                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] });
                            } else {
                                parent.debug('web', 'handleLoginRequest: 2FA token required');
                            }

                            // Wait and redirect the user
                            setTimeout(function () {
                                req.session.loginmode = 4;
                                if ((user.email != null) && (user.emailVerified == true) && (domain.mailserver != null) && (user.otpekey != null)) { req.session.temail = 1; } else { delete req.session.temail; }
                                if ((user.phone != null) && (parent.smsserver != null)) { req.session.tsms = 1; } else { delete req.session.tsms; }
                                if ((user.msghandle != null) && (parent.msgserver != null) && (parent.msgserver.providers != 0)) { req.session.tmsg = 1; } else { delete req.session.tmsg; }
                                if ((user.otpdev != null) && (parent.firebase != null)) { req.session.tpush = 1; } else { delete req.session.tpush; }
                                if ((user.otpduo != null)) { req.session.tduo = 1; } else { delete req.session.tduo; }
                                req.session.e = parent.encryptSessionData({ tuserid: userid, tuser: xusername, tpass: xpassword });
                                if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                            }, randomWaitTime);
                        } else {
                            // Check if we need to remember this device
                            if ((req.body.remembertoken === 'on') && ((domain.twofactorcookiedurationdays == null) || (domain.twofactorcookiedurationdays > 0))) {
                                var maxCookieAge = domain.twofactorcookiedurationdays;
                                if (typeof maxCookieAge != 'number') { maxCookieAge = 30; }
                                const twoFactorCookie = obj.parent.encodeCookie({ userid: user._id, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey);
                                res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: parent.config.settings.sessionsamesite, secure: true });
                            }

                            // Check if email address needs to be confirmed
                            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'))
                            if (emailcheck && (user.emailVerified !== true)) {
                                parent.debug('web', 'Redirecting using ' + user.name + ' to email check login page');
                                req.session.messageid = 3; // "Email verification required" message
                                req.session.loginmode = 7;
                                req.session.passhint = user.email;
                                req.session.cuserid = userid;
                                if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                                return;
                            }

                            // Login successful
                            parent.debug('web', 'handleLoginRequest: successful 2FA login');
                            if (authData != null) { if (loginOptions == null) { loginOptions = {}; } loginOptions.twoFactorType = authData.twoFactorType; }
                            completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions);
                        }
                    });
                    return;
                }

                // Check if email address needs to be confirmed
                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'))
                if (emailcheck && (user.emailVerified !== true)) {
                    parent.debug('web', 'Redirecting using ' + user.name + ' to email check login page');
                    req.session.messageid = 3; // "Email verification required" message
                    req.session.loginmode = 7;
                    req.session.passhint = user.email;
                    req.session.cuserid = userid;
                    if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                    return;
                }

                // Login successful
                parent.debug('web', 'handleLoginRequest: successful login');
                if (twoFactorSkip != null) { if (loginOptions == null) { loginOptions = {}; } loginOptions.twoFactorType = twoFactorSkip.twoFactorType; }
                completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions);
            } else {
                // Login failed, log the error
                obj.parent.authLog('https', 'Failed password for ' + xusername + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'] });

                // Wait a random delay
                setTimeout(function () {
                    // If the account is locked, display that.
                    if (typeof xusername == 'string') {
                        var xuserid = 'user/' + domain.id + '/' + xusername.toLowerCase();
                        if (err == 'locked') {
                            parent.debug('web', 'handleLoginRequest: login failed, locked account');
                            req.session.messageid = 110; // Account locked.
                            const ua = obj.getUserAgentInfo(req);
                            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] });
                            obj.setbadLogin(req, xusername);
                        } else if (err == 'denied') {
                            parent.debug('web', 'handleLoginRequest: login failed, access denied');
                            req.session.messageid = 111; // Access denied.
                            const ua = obj.getUserAgentInfo(req);
                            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] });
                            obj.setbadLogin(req, xusername);
                        } else {
                            parent.debug('web', 'handleLoginRequest: login failed, bad username and password');
                            req.session.messageid = 112; // Login failed, check username and password.
                            const ua = obj.getUserAgentInfo(req);
                            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] });
                            obj.setbadLogin(req, xusername);
                        }
                    }

                    // Clean up login mode and display password hint if present.
                    delete req.session.loginmode;
                    if ((passhint != null) && (passhint.length > 0)) {
                        req.session.passhint = passhint;
                    } else {
                        delete req.session.passhint;
                    }

                    if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095)); // Wait for 2 to ~6 seconds.
            }
        });
    }

    function completeLoginRequest(req, res, domain, user, userid, xusername, xpassword, direct, loginOptions) {
        // Check if we need to change the password
        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))))) {
            // Request a password change
            parent.debug('web', 'handleLoginRequest: login ok, password change requested');
            req.session.loginmode = 6;
            req.session.messageid = 113; // Password change requested.

            // Decrypt any session data
            const sec = parent.decryptSessionData(req.session.e);
            sec.rtuser = xusername;
            sec.rtpass = xpassword;
            sec.rtreset = true;
            req.session.e = parent.encryptSessionData(sec);

            if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
            return;
        }

        // Save login time
        user.pastlogin = user.login;
        user.login = user.access = Math.floor(Date.now() / 1000);
        obj.db.SetUser(user);

        // Notify account login
        const targets = ['*', 'server-users', user._id];
        if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
        const ua = obj.getUserAgentInfo(req);
        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 };
        if (loginOptions != null) {
            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.
            if (loginOptions.twoFactorType != null) { loginEvent.twoFactorType = loginOptions.twoFactorType; }
        }
        obj.parent.DispatchEvent(targets, obj, loginEvent);

        // Regenerate session when signing in to prevent fixation
        //req.session.regenerate(function () {
        // Store the user's primary key in the session store to be retrieved, or in this case the entire user object
        delete req.session.e;
        delete req.session.u2f;
        delete req.session.loginmode;
        delete req.session.tuserid;
        delete req.session.tuser;
        delete req.session.tpass;
        delete req.session.temail;
        delete req.session.tsms;
        delete req.session.tmsg;
        delete req.session.tduo;
        delete req.session.tpush;
        delete req.session.messageid;
        delete req.session.passhint;
        delete req.session.cuserid;
        delete req.session.expire;
        delete req.session.currentNode;
        req.session.userid = userid;
        req.session.ip = req.clientIp;
        setSessionRandom(req);
        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 });

        // If a login token was used, add this information and expire time to the session.
        if ((loginOptions != null) && (loginOptions.tokenName != null) && (loginOptions.tokenUser != null)) {
            req.session.loginToken = loginOptions.tokenUser;
            if (loginOptions.expire != null) { req.session.expire = loginOptions.expire; }
        }

        if (req.body.viewmode) { req.session.viewmode = req.body.viewmode; }
        if (req.body.host) {
            // TODO: This is a terrible search!!! FIX THIS.
            /*
            obj.db.GetAllType('node', function (err, docs) {
                for (var i = 0; i < docs.length; i++) {
                    if (docs[i].name == req.body.host) {
                        req.session.currentNode = docs[i]._id;
                        break;
                    }
                }
                console.log("CurrentNode: " + req.session.currentNode);
                // This redirect happens after finding node is completed
                if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
            });
            */
            parent.debug('web', 'handleLoginRequest: login ok (1)');
            if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } // Temporary
        } else {
            parent.debug('web', 'handleLoginRequest: login ok (2)');
            if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
        }
        //});
    }

    function handleCreateAccountRequest(req, res, direct) {
        const domain = checkUserIpAddress(req, res);
        if (domain == null) { return; }
        if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handleCreateAccountRequest: failed checks.'); res.sendStatus(404); return; }
        if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
        if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
        if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed

        // Check if we are in maintenance mode
        if (parent.config.settings.maintenancemode != null) {
            req.session.messageid = 115; // Server under maintenance
            req.session.loginmode = 1;
            if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
            return;
        }

        // Always lowercase the email address
        if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }

        // If the email is the username, set this here.
        if (domain.usernameisemail) { req.body.username = req.body.email; }

        // Check if there is domain.newAccountToken, check if supplied token is valid
        if ((domain.newaccountspass != null) && (domain.newaccountspass != '') && (req.body.newaccountspass != domain.newaccountspass)) {
            parent.debug('web', 'handleCreateAccountRequest: Invalid account creation token');
            req.session.loginmode = 2;
            req.session.messageid = 103; // Invalid account creation token.
            if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
            return;
        }

        // If needed, check the new account creation CAPTCHA
        if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
            const c = parent.decodeCookie(req.body.captchaargs, parent.loginCookieEncryptionKey, 10); // 10 minute timeout
            if ((c == null) || (c.type != 'newAccount') || (typeof c.captcha != 'string') || (c.captcha.length < 5) || (c.captcha != req.body.anewaccountcaptcha)) {
                req.session.loginmode = 2;
                req.session.messageid = 117; // Invalid security check
                if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                return;
            }
        }

        // Accounts that start with ~ are not allowed
        if ((typeof req.body.username != 'string') || (req.body.username.length < 1) || (req.body.username[0] == '~')) {
            parent.debug('web', 'handleCreateAccountRequest: unable to create account (0)');
            req.session.loginmode = 2;
            req.session.messageid = 100; // Unable to create account.
            if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
            return;
        }

        // Count the number of users in this domain
        var domainUserCount = 0;
        for (var i in obj.users) { if (obj.users[i].domain == domain.id) { domainUserCount++; } }

        // Check if we are allowed to create new users using the login screen
        if ((domain.newaccounts !== 1) && (domain.newaccounts !== true) && (domainUserCount > 0)) {
            parent.debug('web', 'handleCreateAccountRequest: domainUserCount > 1.');
            res.sendStatus(401);
            return;
        }

        // Check if this request is for an allows email domain
        if ((domain.newaccountemaildomains != null) && Array.isArray(domain.newaccountemaildomains)) {
            var i = -1;
            if (typeof req.body.email == 'string') { i = req.body.email.indexOf('@'); }
            if (i == -1) {
                parent.debug('web', 'handleCreateAccountRequest: unable to create account (1)');
                req.session.loginmode = 2;
                req.session.messageid = 100; // Unable to create account.
                if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                return;
            }
            var emailok = false, emaildomain = req.body.email.substring(i + 1).toLowerCase();
            for (var i in domain.newaccountemaildomains) { if (emaildomain == domain.newaccountemaildomains[i].toLowerCase()) { emailok = true; } }
            if (emailok == false) {
                parent.debug('web', 'handleCreateAccountRequest: unable to create account (2)');
                req.session.loginmode = 2;
                req.session.messageid = 100; // Unable to create account.
                if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                return;
            }
        }

        // Check if we exceed the maximum number of user accounts
        obj.db.isMaxType(domain.limits.maxuseraccounts, 'user', domain.id, function (maxExceed) {
            if (maxExceed) {
                parent.debug('web', 'handleCreateAccountRequest: account limit reached');
                req.session.loginmode = 2;
                req.session.messageid = 101; // Account limit reached.
                if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
            } else {
                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)) {
                    parent.debug('web', 'handleCreateAccountRequest: unable to create account (3)');
                    req.session.loginmode = 2;
                    req.session.messageid = 100; // Unable to create account.
                    if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                } else {
                    // Check if this email was already verified
                    obj.db.GetUserWithVerifiedEmail(domain.id, req.body.email, function (err, docs) {
                        if ((docs != null) && (docs.length > 0)) {
                            parent.debug('web', 'handleCreateAccountRequest: Existing account with this email address');
                            req.session.loginmode = 2;
                            req.session.messageid = 102; // Existing account with this email address.
                            if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                        } else {
                            // Check if user exists
                            if (obj.users['user/' + domain.id + '/' + req.body.username.toLowerCase()]) {
                                parent.debug('web', 'handleCreateAccountRequest: Username already exists');
                                req.session.loginmode = 2;
                                req.session.messageid = 104; // Username already exists.
                            } else {
                                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 };
                                if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; }
                                if (obj.common.validateStrArray(domain.newaccountrealms)) { user.groups = domain.newaccountrealms; }
                                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; }
                                if (domainUserCount == 0) { user.siteadmin = 4294967295; /*if (domain.newaccounts === 2) { delete domain.newaccounts; }*/ } // If this is the first user, give the account site admin.

                                // Auto-join any user groups
                                if (typeof domain.newaccountsusergroups == 'object') {
                                    for (var i in domain.newaccountsusergroups) {
                                        var ugrpid = domain.newaccountsusergroups[i];
                                        if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
                                        var ugroup = obj.userGroups[ugrpid];
                                        if (ugroup != null) {
                                            // Add group to the user
                                            if (user.links == null) { user.links = {}; }
                                            user.links[ugroup._id] = { rights: 1 };

                                            // Add user to the group
                                            ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
                                            db.Set(ugroup);

                                            // Notify user group change
                                            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 };
                                            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.
                                            parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
                                        }
                                    }
                                }

                                obj.users[user._id] = user;
                                req.session.userid = user._id;
                                req.session.ip = req.clientIp; // Bind this session to the IP address of the request
                                setSessionRandom(req);
                                // Create a user, generate a salt and hash the password
                                require('./pass').hash(req.body.password1, function (err, salt, hash, tag) {
                                    if (err) throw err;
                                    user.salt = salt;
                                    user.hash = hash;
                                    delete user.passtype;
                                    obj.db.SetUser(user);

                                    // Send the verification email
                                    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); }
                                }, 0);
                                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 };
                                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.
                                obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
                            }
                            if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                        }
                    });
                }
            }
        });
    }

    // Called to process an account password reset
    function handleResetPasswordRequest(req, res, direct) {
        const domain = checkUserIpAddress(req, res);
        if (domain == null) { return; }
        if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
        if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
        if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed

        // Decrypt any session data
        const sec = parent.decryptSessionData(req.session.e);

        // Check everything is ok
        const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false) || (sec.rtreset === true));
        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')) {
            parent.debug('web', 'handleResetPasswordRequest: checks failed');
            delete req.session.e;
            delete req.session.u2f;
            delete req.session.loginmode;
            delete req.session.tuserid;
            delete req.session.tuser;
            delete req.session.tpass;
            delete req.session.temail;
            delete req.session.tsms;
            delete req.session.tmsg;
            delete req.session.tpush;
            delete req.session.messageid;
            delete req.session.passhint;
            delete req.session.cuserid;
            if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
            return;
        }

        // Authenticate the user
        obj.authenticate(sec.rtuser, sec.rtpass, domain, function (err, userid, passhint, loginOptions) {
            if (userid) {
                // Login
                var user = obj.users[userid];

                // If we have password requirements, check this here.
                if (!obj.common.checkPasswordRequirements(req.body.rpassword1, domain.passwordrequirements)) {
                    parent.debug('web', 'handleResetPasswordRequest: password rejected, use a different one (1)');
                    req.session.loginmode = 6;
                    req.session.messageid = 105; // Password rejected, use a different one.
                    if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                    return;
                }

                // Check if the password is the same as a previous one
                obj.checkOldUserPasswords(domain, user, req.body.rpassword1, function (result) {
                    if (result != 0) {
                        // This is the same password as an older one, request a password change again
                        parent.debug('web', 'handleResetPasswordRequest: password rejected, use a different one (2)');
                        req.session.loginmode = 6;
                        req.session.messageid = 105; // Password rejected, use a different one.
                        if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                    } else {
                        // Update the password, use a different salt.
                        require('./pass').hash(req.body.rpassword1, function (err, salt, hash, tag) {
                            const nowSeconds = Math.floor(Date.now() / 1000);
                            if (err) { parent.debug('web', 'handleResetPasswordRequest: hash error.'); throw err; }

                            if (domain.passwordrequirements != null) {
                                // Save password hint if this feature is enabled
                                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; }

                                // Save previous password if this feature is enabled
                                if ((typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
                                    if (user.oldpasswords == null) { user.oldpasswords = []; }
                                    user.oldpasswords.push({ salt: user.salt, hash: user.hash, start: user.passchange, end: nowSeconds });
                                    const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
                                    if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
                                }
                            }

                            user.salt = salt;
                            user.hash = hash;
                            user.passchange = user.access = nowSeconds;
                            delete user.passtype;
                            obj.db.SetUser(user);

                            // Event the account change
                            var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'User password reset', domain: domain.id };
                            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.
                            obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);

                            // Login successful
                            parent.debug('web', 'handleResetPasswordRequest: success');
                            req.session.userid = userid;
                            req.session.ip = req.clientIp; // Bind this session to the IP address of the request
                            setSessionRandom(req);
                            const sec = parent.decryptSessionData(req.session.e);
                            completeLoginRequest(req, res, domain, obj.users[userid], userid, sec.tuser, sec.tpass, direct, loginOptions);
                        }, 0);
                    }
                }, 0);
            } else {
                // Failed, error out.
                parent.debug('web', 'handleResetPasswordRequest: failed authenticate()');
                delete req.session.e;
                delete req.session.u2f;
                delete req.session.loginmode;
                delete req.session.tuserid;
                delete req.session.tuser;
                delete req.session.tpass;
                delete req.session.temail;
                delete req.session.tsms;
                delete req.session.tmsg;
                delete req.session.tpush;
                delete req.session.messageid;
                delete req.session.passhint;
                delete req.session.cuserid;
                if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                return;
            }
        });
    }

    // Called to process an account reset request
    function handleResetAccountRequest(req, res, direct) {
        const domain = checkUserIpAddress(req, res);
        if (domain == null) { return; }
        const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false));
        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; }
        if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
        if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
        if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed

        // Always lowercase the email address
        if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }

        // Get the email from the body or session.
        var email = req.body.email;
        if ((email == null) || (email == '')) { email = req.session.temail; }

        // Check the email string format
        if (!email || checkEmail(email) == false) {
            parent.debug('web', 'handleResetAccountRequest: Invalid email');
            req.session.loginmode = 3;
            req.session.messageid = 106; // Invalid email.
            if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
        } else {
            obj.db.GetUserWithVerifiedEmail(domain.id, email, function (err, docs) {
                // Remove all accounts that start with ~ since they are special accounts.
                var cleanDocs = [];
                if ((err == null) && (docs.length > 0)) {
                    for (var i in docs) {
                        const user = docs[i];
                        const locked = ((user.siteadmin != null) && (user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)); // No password recovery for locked accounts
                        const specialAccount = (user._id.split('/')[2].startsWith('~')); // No password recovery for special accounts
                        if ((specialAccount == false) && (locked == false)) { cleanDocs.push(user); }
                    }
                }
                docs = cleanDocs;

                // Check if we have any account that match this email address
                if ((err != null) || (docs.length == 0)) {
                    parent.debug('web', 'handleResetAccountRequest: Account not found');
                    req.session.loginmode = 3;
                    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.
                    if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                } else {
                    // 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.
                    var responseSent = false;
                    for (var i in docs) {
                        var user = obj.users[docs[i]._id] || docs[i]; // Use the cached user, the login path checks that copy for used codes
                        if (checkUserOneTimePasswordRequired(domain, user, req) == true) {
                            // Second factor setup, request it now.
                            checkUserOneTimePassword(req, domain, user, req.body.token, req.body.hwtoken, function (result, authData) {
                                if (result == false) {
                                    if (i == 0) {

                                        // Check if 2FA is allowed for this IP address
                                        if (obj.checkAllow2Fa(req) == false) {
                                            // Wait and redirect the user
                                            setTimeout(function () {
                                                req.session.messageid = 114; // IP address blocked, try again later.
                                                if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                                            }, 2000 + (obj.crypto.randomBytes(2).readUInt16BE(0) % 4095));
                                            return;
                                        }

                                        // 2-step auth is required, but the token is not present or not valid.
                                        parent.debug('web', 'handleResetAccountRequest: Invalid 2FA token, try again');
                                        if ((req.body.token != null) || (req.body.hwtoken != null)) {
                                            var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
                                            var msg2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false)) && (parent.msgserver != null) && (parent.msgserver.providers != 0) && (user.msghandle != null));
                                            if ((req.body.hwtoken == '**sms**') && sms2fa) {
                                                // Cause a token to be sent to the user's phone number
                                                user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
                                                obj.db.SetUser(user);
                                                parent.debug('web', 'Sending 2FA SMS for password recovery to: ' + user.phone);
                                                parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
                                                req.session.messageid = 4; // SMS sent.
                                            } else if ((req.body.hwtoken == '**msg**') && msg2fa) {
                                                // Cause a token to be sent to the user's messager account
                                                user.otpmsg = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
                                                obj.db.SetUser(user);
                                                parent.debug('web', 'Sending 2FA message for password recovery to: ' + user.msghandle);
                                                parent.msgserver.sendToken(domain, user.msghandle, user.otpmsg.k, obj.getLanguageCodes(req));
                                                req.session.messageid = 6; // Message sent.
                                            } else {
                                                req.session.messageid = 108; // Invalid token, try again.
                                                const ua = obj.getUserAgentInfo(req);
                                                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] });
                                            }
                                        }
                                        req.session.loginmode = 5;
                                        req.session.temail = email;
                                        if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                                    }
                                } else {
                                    // Send email to perform recovery.
                                    delete req.session.temail;
                                    if (domain.mailserver != null) {
                                        domain.mailserver.sendAccountResetMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
                                        if (i == 0) {
                                            parent.debug('web', 'handleResetAccountRequest: Hold on, reset mail sent.');
                                            req.session.loginmode = 1;
                                            req.session.messageid = 1; // If valid, reset mail sent.
                                            if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                                        }
                                    } else {
                                        if (i == 0) {
                                            parent.debug('web', 'handleResetAccountRequest: Unable to sent email.');
                                            req.session.loginmode = 3;
                                            req.session.messageid = 109; // Unable to sent email.
                                            if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                                        }
                                    }
                                }
                            });
                        } else {
                            // No second factor, send email to perform recovery.
                            if (domain.mailserver != null) {
                                domain.mailserver.sendAccountResetMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);
                                if (i == 0) {
                                    parent.debug('web', 'handleResetAccountRequest: Hold on, reset mail sent.');
                                    req.session.loginmode = 1;
                                    req.session.messageid = 1; // If valid, reset mail sent.
                                    if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                                }
                            } else {
                                if (i == 0) {
                                    parent.debug('web', 'handleResetAccountRequest: Unable to sent email.');
                                    req.session.loginmode = 3;
                                    req.session.messageid = 109; // Unable to sent email.
                                    if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                                }
                            }
                        }
                    }
                }
            });
        }
    }

    // Handle account email change and email verification request
    function handleCheckAccountEmailRequest(req, res, direct) {
        const domain = checkUserIpAddress(req, res);
        if (domain == null) { return; }
        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; }
        if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
        if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
        if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed

        // Always lowercase the email address
        if (req.body.email) { req.body.email = req.body.email.toLowerCase(); }

        // Get the email from the body or session.
        var email = req.body.email;
        if ((email == null) || (email == '')) { email = req.session.temail; }

        // Check if this request is for an allows email domain
        if ((domain.newaccountemaildomains != null) && Array.isArray(domain.newaccountemaildomains)) {
            var i = -1;
            if (typeof req.body.email == 'string') { i = req.body.email.indexOf('@'); }
            if (i == -1) {
                parent.debug('web', 'handleCreateAccountRequest: unable to create account (1)');
                req.session.loginmode = 7;
                req.session.messageid = 106; // Invalid email.
                if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                return;
            }
            var emailok = false, emaildomain = req.body.email.substring(i + 1).toLowerCase();
            for (var i in domain.newaccountemaildomains) { if (emaildomain == domain.newaccountemaildomains[i].toLowerCase()) { emailok = true; } }
            if (emailok == false) {
                parent.debug('web', 'handleCreateAccountRequest: unable to create account (2)');
                req.session.loginmode = 7;
                req.session.messageid = 106; // Invalid email.
                if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                return;
            }
        }

        // Check the email string format
        if (!email || checkEmail(email) == false) {
            parent.debug('web', 'handleCheckAccountEmailRequest: Invalid email');
            req.session.loginmode = 7;
            req.session.messageid = 106; // Invalid email.
            if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
        } else {
            // Check is email already exists
            obj.db.GetUserWithVerifiedEmail(domain.id, email, function (err, docs) {
                if ((err != null) || ((docs.length > 0) && (docs.find(function (u) { return (u._id === req.session.cuserid); }) < 0))) {
                    // Email already exists
                    req.session.messageid = 102; // Existing account with this email address.
                } else {
                    // Update the user and notify of user email address change
                    var user = obj.users[req.session.cuserid];
                    if (user.email != email) {
                        user.email = email;
                        db.SetUser(user);
                        var targets = ['*', 'server-users', user._id];
                        if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
                        var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Account changed: ' + user.name, domain: domain.id };
                        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.
                        parent.DispatchEvent(targets, obj, event);
                    }

                    // Send the verification email
                    domain.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key);

                    // Send the response
                    req.session.messageid = 2; // Email sent.
                }
                req.session.loginmode = 7;
                delete req.session.cuserid;
                if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
            });
        }
    }

    // Called to process a web based email verification request
    function handleCheckMailRequest(req, res) {
        const domain = checkUserIpAddress(req, res);
        if (domain == null) { return; }
        if ((domain.auth == 'sspi') || (domain.auth == 'ldap') || (domain.mailserver == null)) { parent.debug('web', 'handleCheckMailRequest: failed checks.'); res.sendStatus(404); return; }
        if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key

        if (req.query.c != null) {
            var cookie = obj.parent.decodeCookie(req.query.c, domain.mailserver.mailCookieEncryptionKey, 30);
            if ((cookie != null) && (cookie.u != null) && (cookie.u.startsWith('user/')) && (cookie.e != null)) {
                var idsplit = cookie.u.split('/');
                if ((idsplit.length != 3) || (idsplit[1] != domain.id)) {
                    parent.debug('web', 'handleCheckMailRequest: Invalid domain.');
                    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));
                } else {
                    obj.db.Get(cookie.u, function (err, docs) {
                        if (docs.length == 0) {
                            parent.debug('web', 'handleCheckMailRequest: Invalid username.');
                            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));
                        } else {
                            var user = docs[0];
                            if (user.email != cookie.e) {
                                parent.debug('web', 'handleCheckMailRequest: Invalid e-mail.');
                                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));
                            } else {
                                if (cookie.a == 1) {
                                    // Account email verification
                                    if (user.emailVerified == true) {
                                        parent.debug('web', 'handleCheckMailRequest: email already verified.');
                                        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));
                                    } else {
                                        obj.db.GetUserWithVerifiedEmail(domain.id, user.email, function (err, docs) {
                                            if ((docs.length > 0) && (docs.find(function (u) { return (u._id === user._id); }) < 0)) {
                                                parent.debug('web', 'handleCheckMailRequest: email already in use.');
                                                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));
                                            } else {
                                                parent.debug('web', 'handleCheckMailRequest: email verification success.');

                                                // Set the verified flag
                                                obj.users[user._id].emailVerified = true;
                                                user.emailVerified = true;
                                                obj.db.SetUser(user);

                                                // Event the change
                                                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 };
                                                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.
                                                obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);

                                                // Send the confirmation page
                                                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));

                                                // Send a notification
                                                obj.parent.DispatchEvent([user._id], obj, { action: 'notify', title: 'Email verified', value: user.email, nolog: 1, id: Math.random() });

                                                // Send to authLog
                                                obj.parent.authLog('https', 'Verified email address ' + user.email + ' for user ' + user.name, { useragent: req.headers['user-agent'] });
                                            }
                                        });
                                    }
                                } else if (cookie.a == 2) {
                                    // Account reset
                                    if (user.emailVerified != true) {
                                        parent.debug('web', 'handleCheckMailRequest: email not verified.');
                                        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));
                                    } else {
                                        if (req.query.confirm == 1) {
                                            // Set a temporary password
                                            obj.crypto.randomBytes(16, function (err, buf) {
                                                var newpass = buf.toString('base64').split('=').join('').split('/').join('').split('+').join('');
                                                require('./pass').hash(newpass, function (err, salt, hash, tag) {
                                                    if (err) throw err;

                                                    // Change the password
                                                    var userinfo = obj.users[user._id];
                                                    userinfo.salt = salt;
                                                    userinfo.hash = hash;
                                                    delete userinfo.passtype;
                                                    userinfo.passchange = userinfo.access = Math.floor(Date.now() / 1000);
                                                    delete userinfo.passhint;
                                                    obj.db.SetUser(userinfo);

                                                    // Event the change
                                                    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 };
                                                    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.
                                                    obj.parent.DispatchEvent(['*', 'server-users', user._id], obj, event);

                                                    // Send the new password
                                                    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));
                                                    parent.debug('web', 'handleCheckMailRequest: send temporary password.');

                                                    // Send to authLog
                                                    obj.parent.authLog('https', 'Performed account reset for user ' + user.name);
                                                }, 0);
                                            });
                                        } else {
                                            // Display a link for the user to confirm password reset
                                            // 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.
                                            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));
                                        }
                                    }
                                } else {
                                    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));
                                }
                            }
                        }
                });
                }
            } else {
                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));
            }
        }
    }

    // Called to process an agent invite GET/POST request
    function handleInviteRequest(req, res) {
        const domain = getDomain(req);
        if (domain == null) { parent.debug('web', 'handleInviteRequest: failed checks.'); res.sendStatus(404); return; }
        if (domain.agentinvitecodes != true) { nice404(req, res); return; }
        if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
        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

        // Invite codes are short, so a wrong one counts like a failed login and a blocked address only gets the bad code page
        if (obj.checkAllowLogin(req) === false) { render(req, res, getRenderPage('invite', req, domain), getRenderArgs({ messageid: 100 }, req, domain)); return; }

        // Each for a device group that has this invite code.
        for (var i in obj.meshes) {
            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)) {
                // Send invitation link, valid for 1 minute.
                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)) : ''));
                return;
            }
        }

        obj.setbadLogin(req);
        render(req, res, getRenderPage('invite', req, domain), getRenderArgs({ messageid: 100 }, req, domain)); // Bad invitation code
    }

    // Called to render the MSTSC (RDP) or SSH web page
    function handleMSTSCRequest(req, res, page) {
        const domain = getDomain(req);
        if (domain == null) { parent.debug('web', 'handleMSTSCRequest: failed checks.'); res.sendStatus(404); return; }
        if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key

        // Check if we are in maintenance mode
        if ((parent.config.settings.maintenancemode != null) && (req.query.loginscreen !== '1')) {
            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));
            return;
        }

        // Set features we want to send to this page
        var features = 0;
        if (domain.allowsavingdevicecredentials === false) { features |= 1; }

        // Get the logged in user if present
        var user = null;

        // If there is a login token, use that
        if (req.query.login != null) {
            var ucookie = parent.decodeCookie(req.query.login, parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
            if ((ucookie != null) && (ucookie.a === 3) && (typeof ucookie.u == 'string')) { user = obj.users[ucookie.u]; }
        }

        // If no token, see if we have an active session
        if ((user == null) && (req.session.userid != null)) { user = obj.users[req.session.userid]; }

        // If still no user, see if we have a default user
        if ((user == null) && (obj.args.user)) { user = obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]; }

        // No user login, exit now
        if (user == null) { res.sendStatus(401); return; }
        if (obj.force2factorPending(domain, user, req)) { res.sendStatus(401); return; } // 2FA setup is still required for this account

        if (req.query.ws != null) {
            // This is a query with a websocket relay cookie, check that the cookie is valid and use it.
            var rcookie = parent.decodeCookie(req.query.ws, parent.loginCookieEncryptionKey, 60); // Cookie with 1 hour timeout
            if ((rcookie != null) && (rcookie.domainid == domain.id) && (rcookie.nodeid != null) && (rcookie.tcpport != null)) {

                // Fetch the node from the database
                obj.db.Get(rcookie.nodeid, function (err, nodes) {
                    if ((err != null) || (nodes.length != 1)) { res.sendStatus(404); return; }
                    const node = nodes[0];

                    // Check if we have SSH/RDP credentials for this device
                    var serverCredentials = 0;
                    if (domain.allowsavingdevicecredentials !== false) {
                        if (page == 'ssh') {
                            if ((typeof node.ssh == 'object') && (typeof node.ssh.u == 'string') && (typeof node.ssh.p == 'string')) { serverCredentials = 1; } // Username and password
                            else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string') && (typeof node.ssh.kp == 'string')) { serverCredentials = 2; } // Username, key and password
                            else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string')) { serverCredentials = 3; } // Username and key. No password.
                            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
                            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
                            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
                        } else {
                            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
                            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
                        }
                    }

                    // Render the page
                    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));
                });
                return;
            }
        }

        // Check the nodeid
        if (req.query.node != null) {
            var nodeidsplit = req.query.node.split('/');
            if (nodeidsplit.length == 1) {
                req.query.node = 'node/' + domain.id + '/' + nodeidsplit[0]; // Format the nodeid correctly
            } else if (nodeidsplit.length == 3) {
                if ((nodeidsplit[0] != 'node') || (nodeidsplit[1] != domain.id)) { req.query.node = null; } // Check the nodeid format
            } else {
                req.query.node = null; // Bad nodeid
            }
        }

        // If there is no nodeid, exit now
        if (req.query.node == null) { render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: '', name: '', features: features }, req, domain)); return; }

        // Fetch the node from the database
        obj.db.Get(req.query.node, function (err, nodes) {
            if ((err != null) || (nodes.length != 1)) { res.sendStatus(404); return; }
            const node = nodes[0];

            // Check access rights, must have remote control rights
            if ((obj.GetNodeRights(user, node.meshid, node._id) & MESHRIGHT_REMOTECONTROL) == 0) { res.sendStatus(401); return; }

            // Figure out the target port
            var port = 0, serverCredentials = false;
            if (page == 'ssh') {
                // SSH port
                port = 22;
                if (typeof node.sshport == 'number') { port = node.sshport; }

                // Check if we have SSH credentials for this device
                if (domain.allowsavingdevicecredentials !== false) {
                    if ((typeof node.ssh == 'object') && (typeof node.ssh.u == 'string') && (typeof node.ssh.p == 'string')) { serverCredentials = 1; } // Username and password
                    else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string') && (typeof node.ssh.kp == 'string')) { serverCredentials = 2; } // Username, key and password
                    else if ((typeof node.ssh == 'object') && (typeof node.ssh.k == 'string')) { serverCredentials = 3; } // Username and key. No password.
                    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
                    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
                    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
                }
            } else {
                // RDP port
                port = 3389;
                if (typeof node.rdpport == 'number') { port = node.rdpport; }

                // Check if we have RDP credentials for this device
                if (domain.allowsavingdevicecredentials !== false) {
                    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
                    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
                }
            }
            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; } }

            // Generate a cookie and respond
            var cookie = parent.encodeCookie({ userid: user._id, domainid: user.domain, nodeid: node._id, tcpport: port }, parent.loginCookieEncryptionKey);
            render(req, res, getRenderPage(page, req, domain), getRenderArgs({ cookie: cookie, name: encodeURIComponent(node.name).replace(/'/g, '%27'), serverCredentials: serverCredentials, features: features }, req, domain));
        });
    }

    // Called to handle push-only requests
    function handleFirebasePushOnlyRelayRequest(req, res) {
        parent.debug('email', 'handleFirebasePushOnlyRelayRequest');
        if ((req.body == null) || (req.body.msg == null) || (obj.parent.firebase == null)) { res.sendStatus(404); return; }
        if (obj.parent.config.firebase.pushrelayserver == null) { res.sendStatus(404); return; }
        if ((typeof obj.parent.config.firebase.pushrelayserver == 'string') && (req.query.key != obj.parent.config.firebase.pushrelayserver)) { res.sendStatus(404); return; }
        var data = null;
        try { data = JSON.parse(req.body.msg) } catch (ex) { res.sendStatus(404); return; }
        if (typeof data != 'object') { res.sendStatus(404); return; }
        if (typeof data.pmt != 'string') { res.sendStatus(404); return; }
        if (typeof data.payload != 'object') { res.sendStatus(404); return; }
        if (typeof data.payload.notification != 'object') { res.sendStatus(404); return; }
        if (typeof data.payload.notification.title != 'string') { res.sendStatus(404); return; }
        if (typeof data.payload.notification.body != 'string') { res.sendStatus(404); return; }
        if (typeof data.options != 'object') { res.sendStatus(404); return; }
        if ((data.options.priority != 'Normal') && (data.options.priority != 'High')) { res.sendStatus(404); return; }
        if ((typeof data.options.timeToLive != 'number') || (data.options.timeToLive < 1)) { res.sendStatus(404); return; }
        parent.debug('email', 'handleFirebasePushOnlyRelayRequest - ok');
        obj.parent.firebase.sendToDevice({ pmt: data.pmt }, data.payload, data.options, function (id, err, errdesc) {
            if (err == null) { res.sendStatus(200); } else { res.sendStatus(500); }
        });
    }

    // Called to handle two-way push notification relay request
    function handleFirebaseRelayRequest(ws, req) {
        parent.debug('email', 'handleFirebaseRelayRequest');
        if (obj.parent.firebase == null) { try { ws.close(); } catch (e) { } return; }
        if (obj.parent.firebase.setupRelay == null) { try { ws.close(); } catch (e) { } return; }
        if (obj.parent.config.firebase.relayserver == null) { try { ws.close(); } catch (e) { } return; }
        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; }
        obj.parent.firebase.setupRelay(ws);
    }

    // Called to process an agent invite request
    function handleAgentInviteRequest(req, res) {
        const domain = getDomain(req);
        if ((domain == null) || ((req.query.m == null) && (req.query.c == null))) { parent.debug('web', 'handleAgentInviteRequest: failed checks.'); res.sendStatus(404); return; }
        if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key

        if (req.query.c != null) {
            // A cookie is specified in the query string, use that
            var cookie = obj.parent.decodeCookie(req.query.c, obj.parent.invitationLinkEncryptionKey);
            if (cookie == null) { res.sendStatus(404); return; }
            var mesh = obj.meshes[cookie.mid];
            if (mesh == null) { res.sendStatus(404); return; }
            var installflags = cookie.f;
            if (typeof installflags != 'number') { installflags = 0; }
            var showagents = cookie.ag;
            if (typeof showagents != 'number') { showagents = 0; }
            parent.debug('web', 'handleAgentInviteRequest using cookie.');

            // Build the mobile agent URL, this is used to connect mobile devices
            var agentServerName = obj.getWebServerName(domain, req);
            if (typeof obj.args.agentaliasdns == 'string') { agentServerName = obj.args.agentaliasdns; }
            var xdomain = (domain.dns == null) ? domain.id : '';
            var agentHttpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
            if (obj.args.agentport != null) { agentHttpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
            if (obj.args.agentaliasport != null) { agentHttpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
            var magenturl = 'mc://' + agentServerName + ((agentHttpsPort != 443) ? (':' + agentHttpsPort) : '') + ((xdomain != '') ? ('/' + xdomain) : '') + ',' + obj.agentCertificateHashBase64 + ',' + mesh._id.split('/')[2];

            var meshcookie = parent.encodeCookie({ m: mesh._id.split('/')[2] }, parent.invitationLinkEncryptionKey);
            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));
        } else if (req.query.m != null) {
            // The MeshId is specified in the query string, use that
            var mesh = obj.meshes['mesh/' + domain.id + '/' + req.query.m.toLowerCase()];
            if (mesh == null) { res.sendStatus(404); return; }
            var installflags = 0;
            if (req.query.f) { installflags = parseInt(req.query.f); }
            if (typeof installflags != 'number') { installflags = 0; }
            var showagents = 0;
            if (req.query.f) { showagents = parseInt(req.query.ag); }
            if (typeof showagents != 'number') { showagents = 0; }
            parent.debug('web', 'handleAgentInviteRequest using meshid.');

            // Build the mobile agent URL, this is used to connect mobile devices
            var agentServerName = obj.getWebServerName(domain, req);
            if (typeof obj.args.agentaliasdns == 'string') { agentServerName = obj.args.agentaliasdns; }
            var xdomain = (domain.dns == null) ? domain.id : '';
            var agentHttpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
            if (obj.args.agentport != null) { agentHttpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
            if (obj.args.agentaliasport != null) { agentHttpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
            var magenturl = 'mc://' + agentServerName + ((agentHttpsPort != 443) ? (':' + agentHttpsPort) : '') + ((xdomain != '') ? ('/' + xdomain) : '') + ',' + obj.agentCertificateHashBase64 + ',' + mesh._id.split('/')[2];

            var meshcookie = parent.encodeCookie({ m: mesh._id.split('/')[2] }, parent.invitationLinkEncryptionKey);
            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));
        }
    }

    // Called to process an agent invite request
    function handleUserImageRequest(req, res) {
        const domain = getDomain(req);
        if (domain == null) { parent.debug('web', 'handleUserImageRequest: failed checks.'); res.sendStatus(404); return; }
        if ((req.session == null) || (req.session.userid == null)) { parent.debug('web', 'handleUserImageRequest: failed checks 2.'); res.sendStatus(404); return; }
        var imageUserId = req.session.userid;
        if ((req.query.id != null)) {
            var user = obj.users[req.session.userid];
            if ((user == null) || ((user.siteadmin & SITERIGHT_MANAGEUSERS) == 0)) { res.sendStatus(404); return; }
            imageUserId = 'user/' + domain.id + '/' + req.query.id;
        }
        obj.db.Get('im' + imageUserId, function (err, docs) {
            if ((err != null) || (docs == null) || (docs.length != 1) || (typeof docs[0].image != 'string')) { res.sendStatus(404); return; }
            var imagebase64 = docs[0].image;
            if (imagebase64.startsWith('data:image/png;base64,')) {
                res.set('Content-Type', 'image/png');
                res.set({ 'Cache-Control': 'no-store' });
                res.send(Buffer.from(imagebase64.substring(22), 'base64'));
            } else if (imagebase64.startsWith('data:image/jpeg;base64,')) {
                res.set('Content-Type', 'image/jpeg');
                res.set({ 'Cache-Control': 'no-store' });
                res.send(Buffer.from(imagebase64.substring(23), 'base64'));
            } else {
                res.sendStatus(404);
            }
        });
    }

    function handleDeleteAccountRequest(req, res, direct) {
        parent.debug('web', 'handleDeleteAccountRequest()');
        const domain = checkUserIpAddress(req, res);
        if (domain == null) { return; }
        if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handleDeleteAccountRequest: failed checks.'); res.sendStatus(404); return; }
        if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
        if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
        if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed

        var user = null;
        if (req.body.authcookie) {
            // If a authentication cookie is provided, decode it here
            var loginCookie = obj.parent.decodeCookie(req.body.authcookie, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
            if ((loginCookie != null) && (domain.id == loginCookie.domainid)) { user = obj.users[loginCookie.userid]; }
        } else {
            // Check if the user is logged and we have all required parameters
            if (!req.session || !req.session.userid || !req.body.apassword1 || (req.body.apassword1 != req.body.apassword2) || (req.session.userid.split('/')[1] != domain.id)) {
                parent.debug('web', 'handleDeleteAccountRequest: required parameters not present.');
                if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                return;
            } else {
                user = obj.users[req.session.userid];
            }
        }
        if (!user) { parent.debug('web', 'handleDeleteAccountRequest: user not found.'); res.sendStatus(404); return; }
        if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) { parent.debug('web', 'handleDeleteAccountRequest: account settings locked.'); res.sendStatus(404); return; }

        // Check if the password is correct
        obj.authenticate(user._id.split('/')[2], req.body.apassword1, domain, function (err, userid, passhint, loginOptions) {
            var deluser = obj.users[userid];
            if ((userid != null) && (deluser != null)) {
                // Remove all links to this user
                if (deluser.links != null) {
                    for (var i in deluser.links) {
                        if (i.startsWith('mesh/')) {
                            // Get the device group
                            var mesh = obj.meshes[i];
                            if (mesh) {
                                // Remove user from the mesh
                                if (mesh.links[deluser._id] != null) { delete mesh.links[deluser._id]; parent.db.Set(mesh); }

                                // Notify mesh change
                                var change = 'Removed user ' + deluser.name + ' from group ' + mesh.name;
                                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 };
                                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.
                                parent.DispatchEvent(['*', mesh._id, deluser._id, user._id], obj, event);
                            }
                        } else if (i.startsWith('node/')) {
                            // Get the node and the rights for this node
                            obj.GetNodeWithRights(domain, deluser, i, function (node, rights, visible) {
                                if ((node == null) || (node.links == null) || (node.links[deluser._id] == null)) return;

                                // Remove the link and save the node to the database
                                delete node.links[deluser._id];
                                if (Object.keys(node.links).length == 0) { delete node.links; }
                                db.Set(obj.cleanDevice(node));

                                // Event the node change
                                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) }
                                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.
                                parent.DispatchEvent(['*', node.meshid, node._id], obj, event);
                            });
                        } else if (i.startsWith('ugrp/')) {
                            // Get the device group
                            var ugroup = obj.userGroups[i];
                            if (ugroup) {
                                // Remove user from the user group
                                if (ugroup.links[deluser._id] != null) { delete ugroup.links[deluser._id]; parent.db.Set(ugroup); }

                                // Notify user group change
                                var change = 'Removed user ' + deluser.name + ' from user group ' + ugroup.name;
                                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 };
                                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.
                                parent.DispatchEvent(['*', ugroup._id, user._id, deluser._id], obj, event);
                            }
                        }
                    }
                }

                obj.db.Remove('ws' + deluser._id);  // Remove user web state
                obj.db.Remove('nt' + deluser._id);  // Remove notes for this user
                obj.db.Remove('ntp' + deluser._id); // Remove personal notes for this user
                obj.db.Remove('im' + deluser._id);  // Remove image for this user

                // Delete any login tokens
                parent.db.GetAllTypeNodeFiltered(['logintoken-' + deluser._id], domain.id, 'logintoken', null, function (err, docs) {
                    if ((err == null) && (docs != null)) { for (var i = 0; i < docs.length; i++) { parent.db.Remove(docs[i]._id, function () { }); } }
                });

                // Delete all files on the server for this account
                try {
                    var deluserpath = obj.getServerRootFilePath(deluser);
                    if (deluserpath != null) { obj.deleteFolderRec(deluserpath); }
                } catch (e) { }

                // Remove the user
                obj.db.Remove(deluser._id);
                delete obj.users[deluser._id];
                req.session = null;
                if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                obj.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', userid: deluser._id, username: deluser.name, action: 'accountremove', msg: 'Account removed', domain: domain.id });
                parent.debug('web', 'handleDeleteAccountRequest: removed user.');
            } else {
                parent.debug('web', 'handleDeleteAccountRequest: auth failed.');
                if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
            }
        });
    }

    // Check a user's password
    obj.checkUserPassword = function (domain, user, password, func) {
        // Check the old password
        if (user.passtype != null) {
            // IIS default clear or weak password hashing (SHA-1)
            require('./pass').iishash(user.passtype, password, user.salt, function (err, hash) {
                if (err) { parent.debug('web', 'checkUserPassword: SHA-1 fail.'); return func(false); }
                if (hash == user.hash) {
                    if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { parent.debug('web', 'checkUserPassword: SHA-1 locked.'); return func(false); } // Account is locked
                    parent.debug('web', 'checkUserPassword: SHA-1 ok.');
                    return func(true); // Allow password change
                }
                func(false);
            });
        } else {
            // Default strong password hashing (pbkdf2 SHA384)
            require('./pass').hash(password, user.salt, function (err, hash, tag) {
                if (err) { parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 fail.'); return func(false); }
                if (hash == user.hash) {
                    if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 locked.'); return func(false); } // Account is locked
                    parent.debug('web', 'checkUserPassword: pbkdf2 SHA384 ok.');
                    return func(true); // Allow password change
                }
                func(false);
            }, 0);
        }
    }

    // Check a user's old passwords
    // Callback: 0=OK, 1=OldPass, 2=CommonPass
    obj.checkOldUserPasswords = function (domain, user, password, func) {
        // Check how many old passwords we need to check
        if ((domain.passwordrequirements != null) && (typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
            if (user.oldpasswords != null) {
                const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
                if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
            }
        } else {
            delete user.oldpasswords;
        }

        // If there is no old passwords, exit now.
        var oldPassCount = 1;
        if (user.oldpasswords != null) { oldPassCount += user.oldpasswords.length; }
        var oldPassCheckState = { response: 0, count: oldPassCount, user: user, func: func };

        // Test against common passwords if this feature is enabled
        // Example of common passwords: 123456789, password123
        if ((domain.passwordrequirements != null) && (domain.passwordrequirements.bancommonpasswords == true)) {
            oldPassCheckState.count++;
            require('wildleek')(password).then(function (wild) {
                if (wild == true) { oldPassCheckState.response = 2; }
                if (--oldPassCheckState.count == 0) { oldPassCheckState.func(oldPassCheckState.response); }
            });
        }

        // Try current password
        require('./pass').hash(password, user.salt, function oldPassCheck(err, hash, tag) {
            if ((err == null) && (hash == tag.user.hash)) { tag.response = 1; }
            if (--tag.count == 0) { tag.func(tag.response); }
        }, oldPassCheckState);

        // Try each old password
        if (user.oldpasswords != null) {
            for (var i in user.oldpasswords) {
                const oldpassword = user.oldpasswords[i];
                // Default strong password hashing (pbkdf2 SHA384)
                require('./pass').hash(password, oldpassword.salt, function oldPassCheck(err, hash, tag) {
                    if ((err == null) && (hash == tag.oldPassword.hash)) { tag.state.response = 1; }
                    if (--tag.state.count == 0) { tag.state.func(tag.state.response); }
                }, { oldPassword: oldpassword, state: oldPassCheckState });
            }
        }
    }

    // Handle password changes
    function handlePasswordChangeRequest(req, res, direct) {
        const domain = checkUserIpAddress(req, res);
        if (domain == null) { return; }
        if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { parent.debug('web', 'handlePasswordChangeRequest: failed checks (1).'); res.sendStatus(404); return; }
        if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
        if (req.session.loginToken != null) { res.sendStatus(404); return; } // Do not allow this command when logged in using a login token
        if (req.body == null) { res.sendStatus(404); return; } // Post body is empty or can't be parsed

        // Check if the user is logged and we have all required parameters
        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)) {
            parent.debug('web', 'handlePasswordChangeRequest: failed checks (2).');
            if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
            return;
        }

        // Get the current user
        var user = obj.users[req.session.userid];
        if (!user) {
            parent.debug('web', 'handlePasswordChangeRequest: user not found.');
            if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
            return;
        }

        // Check account settings locked
        if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 1024) != 0)) {
            parent.debug('web', 'handlePasswordChangeRequest: account settings locked.');
            if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
            return;
        }

        // Check old password
        obj.checkUserPassword(domain, user, req.body.apassword1, function (result) {
            if (result == true) {
                // Check if the new password is allowed, only do this if this feature is enabled.
                parent.checkOldUserPasswords(domain, user, command.newpass, function (result) {
                    if (result == 1) {
                        parent.debug('web', 'handlePasswordChangeRequest: old password reuse attempt.');
                        if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                    } else if (result == 2) {
                        parent.debug('web', 'handlePasswordChangeRequest: commonly used password use attempt.');
                        if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                    } else {
                        // Update the password
                        require('./pass').hash(req.body.apassword1, function (err, salt, hash, tag) {
                            const nowSeconds = Math.floor(Date.now() / 1000);
                            if (err) { parent.debug('web', 'handlePasswordChangeRequest: hash error.'); throw err; }
                            if (domain.passwordrequirements != null) {
                                // Save password hint if this feature is enabled
                                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; }

                                // Save previous password if this feature is enabled
                                if ((typeof domain.passwordrequirements.oldpasswordban == 'number') && (domain.passwordrequirements.oldpasswordban > 0)) {
                                    if (user.oldpasswords == null) { user.oldpasswords = []; }
                                    user.oldpasswords.push({ salt: user.salt, hash: user.hash, start: user.passchange, end: nowSeconds });
                                    const extraOldPasswords = user.oldpasswords.length - domain.passwordrequirements.oldpasswordban;
                                    if (extraOldPasswords > 0) { user.oldpasswords.splice(0, extraOldPasswords); }
                                }
                            }
                            user.salt = salt;
                            user.hash = hash;
                            user.passchange = user.access = nowSeconds;
                            delete user.passtype;

                            obj.db.SetUser(user);
                            req.session.viewmode = 2;
                            if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); }
                            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 });
                        }, 0);
                    }
                });
            }
        });
    }

    // Called when a strategy login occurred
    // This is called after a successful Oauth to Twitter, Google, GitHub...
    function handleStrategyLogin(req, res) {
        const domain = checkUserIpAddress(req, res);
        if (domain == null) { return; }
        if ((req.user != null) && (req.user.sid != null) && (req.user.strategy != null)) {
            const strategy = domain.authstrategies[req.user.strategy];
            const groups = { 'enabled': typeof strategy.groups == 'object' }
            parent.authLog(req.user.strategy.toUpperCase(), `User Authorized: ${req.user.sid}`);
            if (groups.enabled) { // Groups only available for OIDC strategy currently
                groups.userMemberships = obj.common.convertStrArray(req.user.groups);
                groups.syncEnabled = (strategy.groups.sync === true || strategy.groups.sync?.filter) ? true : false;
                groups.syncMemberships = [];
                groups.siteAdminEnabled = strategy.groups.siteadmin ? true : false;
                groups.grantAdmin = false;
                groups.revokeAdmin = strategy.groups.revokeAdmin ? strategy.groups.revokeAdmin : true;
                groups.requiredGroups = obj.common.convertStrArray(strategy.groups.required);
                groups.siteAdmin = obj.common.convertStrArray(strategy.groups.siteadmin);
                groups.syncFilter = obj.common.convertStrArray(strategy.groups.sync?.filter);

                // Fancy Logs
                let groupMessage = '';
                if (groups.userMemberships.length == 1) { groupMessage = ` Found membership: "${groups.userMemberships[0]}"` }
                else { groupMessage = ` Found ${groups.userMemberships.length} memberships: ["${groups.userMemberships.join('", "')}"]` }
                parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}"` + groupMessage);

                // Check user membership in required groups
                if (groups.requiredGroups.length > 0) {
                    let match = false
                    for (var i in groups.requiredGroups) {
                        if (groups.userMemberships.indexOf(groups.requiredGroups[i]) != -1) {
                            match = true;
                            parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Membership to required group found: "${groups.requiredGroups[i]}"`);
                        }
                    }
                    if (match === false) {
                        parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Login denied. No membership to required group.`);
                        req.session.loginmode = 1;
                        req.session.messageid = 111; // Access Denied.
                        res.redirect(domain.url + getQueryPortion(req));
                        return;
                    }
                }

                // Check user membership in admin groups
                if (groups.siteAdminEnabled === true) {
                    groups.grantAdmin = false;
                    for (var i in strategy.groups.siteadmin) {
                        if (groups.userMemberships.indexOf(strategy.groups.siteadmin[i]) >= 0) {
                            parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" User membership found in site admin group: "${strategy.groups.siteadmin[i]}"`);
                            groups.siteAdmin = strategy.groups.siteadmin[i];
                            groups.grantAdmin = true;
                            break;
                        }
                    }
                }

                // Check if we need to sync user-memberships (IdP) with user-groups (meshcentral)
                if (groups.syncEnabled === true) {
                    if (groups.syncFilter.length > 0){ // config.json has specified sync.filter so loop and use it
                        for (var i in groups.syncFilter) {
                            if (groups.userMemberships.indexOf(groups.syncFilter[i]) >= 0) { groups.syncMemberships.push(groups.syncFilter[i]); }
                        }
                    } else { // config.json doesnt have sync.filter specified so we are going to sync all the users groups from oidc instead
                        for (var i in groups.userMemberships) {
                            groups.syncMemberships.push(groups.userMemberships[i]);
                        }
                    }
                    if (groups.syncMemberships.length > 0) {
                        parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" User memberships to sync: ${groups.syncMemberships.join(', ')}`);
                    } else {
                        groups.syncMemberships = null;
                        groups.syncEnabled = false;
                        if (groups.syncFilter.length > 0){
                            parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" No sync memberships found using filters: ${groups.syncFilter.join(', ')}`);
                        } else {
                            parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" No sync memberships found`);
                        }
                    }
                }
            }

            // Check if the user already exists
            const userid = 'user/' + domain.id + '/' + req.user.sid;
            var user = obj.users[userid];
            if (user == null) {
                var newAccountAllowed = false;
                var newAccountRealms = null;

                if (domain.newaccounts === true) { newAccountAllowed = true; }
                if (obj.common.validateStrArray(domain.newaccountrealms)) { newAccountRealms = domain.newaccountrealms; }

                if (domain.authstrategies[req.user.strategy]) {
                    if (domain.authstrategies[req.user.strategy].newaccounts === true) { newAccountAllowed = true; }
                    if (obj.common.validateStrArray(domain.authstrategies[req.user.strategy].newaccountrealms)) { newAccountRealms = domain.authstrategies[req.user.strategy].newaccountrealms; }
                }

                if (newAccountAllowed === true) {
                    // Create the user
                    parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: USER: "${req.user.sid}" Creating new login user: "${userid}"`);
                    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 };
                    if (req.user.email != null) { user.email = req.user.email; user.emailVerified = req.user.email_verified ? req.user.email_verified : true; }
                    if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; } // New accounts automatically assigned server rights.
                    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.
                    if (newAccountRealms) { user.groups = newAccountRealms; } // New accounts automatically part of some groups (Realms).
                    obj.users[userid] = user;

                    // Auto-join any user groups
                    var newaccountsusergroups = null;
                    if (typeof domain.newaccountsusergroups == 'object') { newaccountsusergroups = domain.newaccountsusergroups; }
                    if (typeof domain.authstrategies[req.user.strategy].newaccountsusergroups == 'object') { newaccountsusergroups = domain.authstrategies[req.user.strategy].newaccountsusergroups; }
                    if (newaccountsusergroups) {
                        for (var i in newaccountsusergroups) {
                            var ugrpid = newaccountsusergroups[i];
                            if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
                            var ugroup = obj.userGroups[ugrpid];
                            if (ugroup != null) {
                                // Add group to the user
                                if (user.links == null) { user.links = {}; }
                                user.links[ugroup._id] = { rights: 1 };

                                // Add user to the group
                                ugroup.links[user._id] = { userid: user._id, name: user.name, rights: 1 };
                                db.Set(ugroup);

                                // Notify user group change
                                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 };
                                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.
                                parent.DispatchEvent(['*', ugroup._id, user._id], obj, event);
                            }
                        }
                    }

                    if (groups.enabled === true) {
                        // Sync the user groups if enabled
                        if (groups.syncEnabled === true) {
                            // Set groupType to the preset name if it exists, otherwise use the strategy name
                            const groupType = domain.authstrategies[req.user.strategy].custom?.preset ? domain.authstrategies[req.user.strategy].custom.preset : req.user.strategy;
                            syncExternalUserGroups(domain, user, groups.syncMemberships, groupType);
                        }
                        // See if the user is a member of the site admin group.
                        if (groups.grantAdmin === true) {
                            parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Granting site admin privilages`);
                            user.siteadmin = 0xFFFFFFFF;
                        }
                    }

                    // Save the user
                    obj.db.SetUser(user);

                    // Event user creation
                    var targets = ['*', 'server-users'];
                    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 };
                    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.
                    parent.DispatchEvent(targets, obj, event);

                    req.session.userid = userid;
                    setSessionRandom(req);

                    // Notify account login using SSO
                    var targets = ['*', 'server-users', user._id];
                    if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
                    const ua = obj.getUserAgentInfo(req);
                    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' };
                    obj.parent.DispatchEvent(targets, obj, loginEvent);
                } else {
                    // New users not allowed
                    parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: LOGIN FAILED: USER: "${req.user.sid}" New accounts are not allowed`);
                    req.session.loginmode = 1;
                    req.session.messageid = 100; // Unable to create account.
                    res.redirect(domain.url + getQueryPortion(req));
                    return;
                }
            } else { // Login success
                // Check for basic changes
                var userChanged = false;
                if ((req.user.name != null) && (req.user.name != user.name)) { user.name = req.user.name; userChanged = true; }
                if ((req.user.email != null) && (req.user.email != user.email)) { user.email = req.user.email; user.emailVerified = true; userChanged = true; }

                if (groups.enabled === true) {
                    // Sync the user groups if enabled
                    if (groups.syncEnabled === true) {
                        syncExternalUserGroups(domain, user, groups.syncMemberships, req.user.strategy)
                    }
                    // See if the user is a member of the site admin group.
                    if (groups.siteAdminEnabled === true) {
                        if (groups.grantAdmin === true) {
                            parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Granting site admin privilages`);
                            if (user.siteadmin !== 0xFFFFFFFF) { user.siteadmin = 0xFFFFFFFF; userChanged = true; }
                        } else if ((groups.revokeAdmin === true) && (user.siteadmin === 0xFFFFFFFF)) {
                            parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: GROUPS: USER: "${req.user.sid}" Revoking site admin privilages.`);
                            delete user.siteadmin;
                            userChanged = true;
                        }
                    }
                }

                // Update db record for user if there are changes detected
                if (userChanged) {
                    parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: CHANGED: USER: "${req.user.sid}" Updating user database entry`);
                    obj.db.SetUser(user);

                    // Event user change
                    var targets = ['*', 'server-users'];
                    var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Account changed', domain: domain.id };
                    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.
                    parent.DispatchEvent(targets, obj, event);
                }
                req.session.userid = userid;
                setSessionRandom(req);

                // Notify account login using SSO
                var targets = ['*', 'server-users', user._id];
                if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
                const ua = obj.getUserAgentInfo(req);
                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' };
                obj.parent.DispatchEvent(targets, obj, loginEvent);
                parent.authLog('handleStrategyLogin', `${req.user.strategy.toUpperCase()}: LOGIN SUCCESS: USER: "${req.user.sid}"`);
            }
            if (typeof req.user.oidcLogoutUrl == 'string') { obj.oidcLogoutUrls[userid] = req.user.oidcLogoutUrl; } // Carries the id_token hint, keep it out of the session cookie and user record
        } else if (req.session && req.session.userid && obj.users[req.session.userid]) {
            parent.authLog('handleStrategyLogin', `User Already Authorised "${(req.session.passport && req.session.passport.user) ? req.session.passport.user : req.session.userid }"`);
        } else {
            parent.authLog('handleStrategyLogin', `LOGIN FAILED: REQUEST CONTAINS NO USER OR SID`);
        }
        //res.redirect(domain.url); // This does not handle cookie correctly.
        res.set('Content-Type', 'text/html');
        let url = domain.url;
        if (Object.keys(req.query).length > 0) { url += "?" + Object.keys(req.query).map(function(key) { return encodeURIComponent(key) + "=" + encodeURIComponent(req.query[key]); }).join("&"); }

        // RelayState is echoed back by the IdP, only follow it to this server's own root
        if ((req.body != null) && (typeof req.body.RelayState == 'string')) {
            try {
                const base = new URL(obj.generateBaseURL(domain, req)), target = new URL(decodeURIComponent(req.body.RelayState));
                if ((target.origin == base.origin) && (target.pathname == base.pathname) && (target.username == '') && (target.password == '')) { url = target.pathname + target.search; }
            } catch (ex) { }
        }
        res.end('<html><head><meta http-equiv="refresh" content="0;url=' + EscapeHtml(url) + '"></head><body></body></html>');
    }

    // Indicates that any request to "/" should render "default" or "login" depending on login state
    function handleRootRequest(req, res, direct) {
        const domain = checkUserIpAddress(req, res);
        if (domain == null) { return; }
        if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
        if (!obj.args) { parent.debug('web', 'handleRootRequest: no obj.args.'); res.sendStatus(500); return; }

        // If a HTTP header is required, check new UserRequiredHttpHeader
        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; } }

        // If the session is expired, clear it.
        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]; } }

        // Check if we are in maintenance mode
        if ((parent.config.settings.maintenancemode != null) && (req.query.loginscreen !== '1')) {
            parent.debug('web', 'handleLoginRequest: Server under maintenance.');
            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));
            return;
        }

        // If set and there is no user logged in, redirect the root page. Make sure not to redirect if /login is used
        if ((typeof domain.unknownuserrootredirect == 'string') && ((req.session == null) || (req.session.userid == null))) {
            var q = new URL(req.url, 'http://localhost');
            if (!q.pathname.endsWith('/login')) { res.redirect(domain.unknownuserrootredirect + getQueryPortion(req)); return; }
        }

        if ((domain.sspi != null) && ((req.query.login == null) || (obj.parent.loginCookieEncryptionKey == null))) {
            // Login using SSPI
            domain.sspi.authenticate(req, res, function (err) {
                if ((err != null) || (req.connection.user == null)) {
                    obj.parent.authLog('https', 'Failed SSPI-auth for ' + req.connection.user + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'] });
                    parent.debug('web', 'handleRootRequest: SSPI auth required.');
                    try { res.sendStatus(401); } catch (ex) { } // sspi.authenticate() should already have responded to this request.
                } else {
                    parent.debug('web', 'handleRootRequest: SSPI auth ok.');
                    handleRootRequestEx(req, res, domain, direct);
                }
            });
        } else if (req.query.user && req.query.pass) {
            // User credentials are being passed in the URL. WARNING: Putting credentials in a URL is bad security... but people are requesting this option.
            obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid, passhint, loginOptions) {
                // 2FA is not supported in URL authentication method. If user has 2FA enabled, this login method fails.
                var user = obj.users[userid];
                if ((err == null) && checkUserOneTimePasswordRequired(domain, user, req, loginOptions) == true) {
                    handleRootRequestEx(req, res, domain, direct);
                } else if ((userid != null) && (err == null)) {
                    // Login success
                    parent.debug('web', 'handleRootRequest: user/pass in URL auth ok.');
                    req.session.userid = userid;
                    delete req.session.currentNode;
                    req.session.ip = req.clientIp; // Bind this session to the IP address of the request
                    setSessionRandom(req);
                    obj.parent.authLog('https', 'Accepted password for ' + userid + ' from ' + req.clientIp + ' port ' + req.connection.remotePort, { useragent: req.headers['user-agent'], sessionid: req.session.x });
                    handleRootRequestEx(req, res, domain, direct);
                } else {
                    // Login failed
                    handleRootRequestEx(req, res, domain, direct);
                }
            });
        } else if ((req.session != null) && (typeof req.session.loginToken == 'string')) {
            // Check if the loginToken is still valid
            obj.db.Get('logintoken-' + req.session.loginToken, function (err, docs) {
                if ((err != null) || (docs == null) || (docs.length != 1) || (docs[0].tokenUser != req.session.loginToken)) { for (var i in req.session) { delete req.session[i]; } }
                handleRootRequestEx(req, res, domain, direct); // Login using a different system
            });
        } else {
            // Login using a different system
            handleRootRequestEx(req, res, domain, direct);
        }
    }

    function handleRootRequestEx(req, res, domain, direct) {
        var nologout = false, user = null;
        res.set({ 'Cache-Control': 'no-store' });

        // Check if we have an incomplete domain name in the path
        if ((domain.id != '') && (domain.dns == null) && (req.url.split('/').length == 2)) {
            parent.debug('web', 'handleRootRequestEx: incomplete domain name in the path.');
            res.redirect(domain.url + getQueryPortion(req)); // BAD***
            return;
        }

        if (obj.args.nousers == true) {
            // If in single user mode, setup things here.
            delete req.session.loginmode;
            req.session.userid = 'user/' + domain.id + '/~';
            delete req.session.currentNode;
            req.session.ip = req.clientIp; // Bind this session to the IP address of the request
            setSessionRandom(req);
            if (obj.users[req.session.userid] == null) {
                // Create the dummy user ~ with impossible password
                parent.debug('web', 'handleRootRequestEx: created dummy user in nouser mode.');
                obj.users[req.session.userid] = { type: 'user', _id: req.session.userid, name: '~', email: '~', domain: domain.id, siteadmin: 4294967295 };
                obj.db.SetUser(obj.users[req.session.userid]);
            }
        } else if (obj.args.user && obj.users['user/' + domain.id + '/' + obj.args.user.toLowerCase()]) {
            // If a default user is active, setup the session here.
            parent.debug('web', 'handleRootRequestEx: auth using default user.');
            delete req.session.loginmode;
            req.session.userid = 'user/' + domain.id + '/' + obj.args.user.toLowerCase();
            delete req.session.currentNode;
            req.session.ip = req.clientIp; // Bind this session to the IP address of the request
            setSessionRandom(req);
        } else if (req.query.login && (obj.parent.loginCookieEncryptionKey != null)) {
            var loginCookie = obj.parent.decodeCookie(req.query.login, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
            //if ((loginCookie != null) && (loginCookie.ip != null) && !checkCookieIp(loginCookie.ip, req.clientIp)) { loginCookie = null; } // If the cookie is bound to an IP address, check here.
            if ((loginCookie != null) && (loginCookie.a == 3) && (loginCookie.u != null) && (loginCookie.u.split('/')[1] == domain.id)) {
                // If a login cookie was provided, setup the session here.
                parent.debug('web', 'handleRootRequestEx: cookie auth ok.');
                delete req.session.loginmode;
                req.session.userid = loginCookie.u;
                delete req.session.currentNode;
                req.session.ip = req.clientIp; // Bind this session to the IP address of the request
                setSessionRandom(req);
            } else {
                parent.debug('web', 'handleRootRequestEx: cookie auth failed.');
            }
        } else if (domain.sspi != null) {
            // SSPI login (Windows only)
            //console.log(req.connection.user, req.connection.userSid);
            if ((req.connection.user == null) || (req.connection.userSid == null)) {
                parent.debug('web', 'handleRootRequestEx: SSPI no user auth.');
                res.sendStatus(404); return;
            } else {
                nologout = true;
                req.session.userid = 'user/' + domain.id + '/' + req.connection.user.toLowerCase();
                req.session.usersid = req.connection.userSid;
                req.session.usersGroups = req.connection.userGroups;
                delete req.session.currentNode;
                req.session.ip = req.clientIp; // Bind this session to the IP address of the request
                setSessionRandom(req);
                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 });

                // Check if this user exists, create it if not.
                user = obj.users[req.session.userid];
                if ((user == null) || (user.sid != req.session.usersid)) {
                    // Create the domain user
                    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) };
                    if (domain.newaccountsrights) { user2.siteadmin = domain.newaccountsrights; }
                    if (obj.common.validateStrArray(domain.newaccountrealms)) { user2.groups = domain.newaccountrealms; }
                    for (var i in obj.users) { if (obj.users[i].domain == domain.id) { usercount++; } }
                    if (usercount == 0) { user2.siteadmin = 4294967295; } // If this is the first user, give the account site admin.

                    // Auto-join any user groups
                    if (typeof domain.newaccountsusergroups == 'object') {
                        for (var i in domain.newaccountsusergroups) {
                            var ugrpid = domain.newaccountsusergroups[i];
                            if (ugrpid.indexOf('/') < 0) { ugrpid = 'ugrp/' + domain.id + '/' + ugrpid; }
                            var ugroup = obj.userGroups[ugrpid];
                            if (ugroup != null) {
                                // Add group to the user
                                if (user2.links == null) { user2.links = {}; }
                                user2.links[ugroup._id] = { rights: 1 };

                                // Add user to the group
                                ugroup.links[user2._id] = { userid: user2._id, name: user2.name, rights: 1 };
                                db.Set(ugroup);

                                // Notify user group change
                                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 };
                                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.
                                parent.DispatchEvent(['*', ugroup._id, user2._id], obj, event);
                            }
                        }
                    }

                    obj.users[req.session.userid] = user2;
                    obj.db.SetUser(user2);
                    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 };
                    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.
                    obj.parent.DispatchEvent(['*', 'server-users'], obj, event);
                    parent.debug('web', 'handleRootRequestEx: SSPI new domain user.');
                }
            }
        }

        // Figure out the minimal password requirement
        var passRequirements = null;
        if (domain.passwordrequirements != null) {
            if (domain.passrequirementstr == null) {
                var passRequirements = {};
                if (typeof domain.passwordrequirements.min == 'number') { passRequirements.min = domain.passwordrequirements.min; }
                if (typeof domain.passwordrequirements.max == 'number') { passRequirements.max = domain.passwordrequirements.max; }
                if (typeof domain.passwordrequirements.upper == 'number') { passRequirements.upper = domain.passwordrequirements.upper; }
                if (typeof domain.passwordrequirements.lower == 'number') { passRequirements.lower = domain.passwordrequirements.lower; }
                if (typeof domain.passwordrequirements.numeric == 'number') { passRequirements.numeric = domain.passwordrequirements.numeric; }
                if (typeof domain.passwordrequirements.nonalpha == 'number') { passRequirements.nonalpha = domain.passwordrequirements.nonalpha; }
                domain.passwordrequirementsstr = encodeURIComponent(JSON.stringify(passRequirements));
            }
            passRequirements = domain.passwordrequirementsstr;
        }

        // If a user exists and is logged in, serve the default app, otherwise server the login app.
        if (req.session && req.session.userid && obj.users[req.session.userid]) {
            const user = obj.users[req.session.userid];

            // Check if we are in maintenance mode
            if ((parent.config.settings.maintenancemode != null) && (user.siteadmin != 4294967295)) {
                req.session.messageid = 115; // Server under maintenance
                req.session.loginmode = 1;
                res.redirect(domain.url);
                return;
            }

            // If the request has a "meshmessengerid", redirect to MeshMessenger
            // This situation happens when you get a push notification for a chat session, but are not logged in.
            if (req.query.meshmessengerid != null) {
                res.redirect(domain.url + 'messenger?id=' + encodeURIComponent(req.query.meshmessengerid) + ((req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : ''));
                return;
            }

            const xdbGetFunc = function dbGetFunc(err, states) {
                if (dbGetFunc.req.session.userid.split('/')[1] != domain.id) { // Check if the session is for the correct domain
                    parent.debug('web', 'handleRootRequestEx: incorrect domain.');
                    dbGetFunc.req.session = null;
                    dbGetFunc.res.redirect(domain.url + getQueryPortion(dbGetFunc.req)); // BAD***
                    return;
                }

                // Check if this is a locked account
                if ((dbGetFunc.user.siteadmin != null) && ((dbGetFunc.user.siteadmin & 32) != 0) && (dbGetFunc.user.siteadmin != 0xFFFFFFFF)) {
                    // Locked account
                    parent.debug('web', 'handleRootRequestEx: locked account.');
                    delete dbGetFunc.req.session.userid;
                    delete dbGetFunc.req.session.currentNode;
                    delete dbGetFunc.req.session.passhint;
                    delete dbGetFunc.req.session.cuserid;
                    dbGetFunc.req.session.messageid = 110; // Account locked.
                    dbGetFunc.res.redirect(domain.url + getQueryPortion(dbGetFunc.req)); // BAD***
                    return;
                }

                var viewmode = 1;
                if (dbGetFunc.req.session.viewmode) {
                    viewmode = dbGetFunc.req.session.viewmode;
                    delete dbGetFunc.req.session.viewmode;
                } else if (dbGetFunc.req.query.viewmode) {
                    viewmode = dbGetFunc.req.query.viewmode;
                }
                var currentNode = '';
                if (dbGetFunc.req.session.currentNode) {
                    currentNode = dbGetFunc.req.session.currentNode;
                    delete dbGetFunc.req.session.currentNode;
                } else if (dbGetFunc.req.query.node) {
                    currentNode = 'node/' + domain.id + '/' + dbGetFunc.req.query.node;
                }
                var logoutcontrols = {};
                if (obj.args.nousers != true) { logoutcontrols.name = user.name; }

                // Give the web page a list of supported server features for this domain and user
                const allFeatures = obj.getDomainUserFeatures(domain, dbGetFunc.user, dbGetFunc.req);

                // Create a authentication cookie
                const authCookie = obj.parent.encodeCookie({ userid: dbGetFunc.user._id, domainid: domain.id, ip: req.clientIp, x: req.session.x }, obj.parent.loginCookieEncryptionKey);
                const authRelayCookie = obj.parent.encodeCookie({ ruserid: dbGetFunc.user._id, x: req.session.x }, obj.parent.loginCookieEncryptionKey);

                // Send the main web application
                var extras = (dbGetFunc.req.query.key != null) ? ('&key=' + dbGetFunc.req.query.key) : '';
                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
                var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified

                // Clean up the U2F challenge if needed
                if (dbGetFunc.req.session.u2f) { delete dbGetFunc.req.session.u2f; };
                if (dbGetFunc.req.session.e) {
                    const sec = parent.decryptSessionData(dbGetFunc.req.session.e);
                    if (sec.u2f != null) { delete sec.u2f; dbGetFunc.req.session.e = parent.encryptSessionData(sec); }
                }

                // Intel AMT Scanning options
                var amtscanoptions = '';
                if (typeof domain.amtscanoptions == 'string') { amtscanoptions = encodeURIComponent(domain.amtscanoptions); }
                else if (obj.common.validateStrArray(domain.amtscanoptions)) { domain.amtscanoptions = domain.amtscanoptions.join(','); amtscanoptions = encodeURIComponent(domain.amtscanoptions); }

                // Fetch the web state
                parent.debug('web', 'handleRootRequestEx: success.');

                var webstate = '{}';
                if ((err == null) && (states != null) && (Array.isArray(states)) && (states.length == 1) && (states[0].state != null)) { webstate = obj.filterUserWebState(states[0].state); }
                if ((webstate == '{}') && (typeof domain.defaultuserwebstate == 'object')) { webstate = JSON.stringify(domain.defaultuserwebstate); } // User has no web state, use defaults.
                if (typeof domain.forceduserwebstate == 'object') { // Forces initial user web state if present, use it.
                    var webstate2 = {};
                    try { if (webstate != '{}') { webstate2 = JSON.parse(webstate); } } catch (ex) { }
                    for (var i in domain.forceduserwebstate) { webstate2[i] = domain.forceduserwebstate[i]; }
                    webstate = JSON.stringify(webstate2);
                }

                // Custom user interface
                var customui = '';
                if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); }

                // Custom files (CSS and JS)
                var customFiles = '';
                if (domain.customFiles != null) { 
                    customFiles = encodeURIComponent(JSON.stringify(domain.customFiles)); 
                } else if (domain.customfiles != null) {
                    customFiles = encodeURIComponent(JSON.stringify(domain.customfiles)); 
                }

                // Server features
                var serverFeatures = 255;
                if (domain.myserver === false) { serverFeatures = 0; } // 64 = Show "My Server" tab
                else if (typeof domain.myserver == 'object') {
                    if (domain.myserver.backup !== true) { serverFeatures -= 1; } // Disallow simple server backups
                    if (domain.myserver.restore !== true) { serverFeatures -= 2; } // Disallow simple server restore
                    if (domain.myserver.upgrade !== true) { serverFeatures -= 4; } // Disallow server upgrade
                    if (domain.myserver.errorlog !== true) { serverFeatures -= 8; } // Disallow show server crash log
                    if (domain.myserver.console !== true) { serverFeatures -= 16; } // Disallow server console
                    if (domain.myserver.trace !== true) { serverFeatures -= 32; } // Disallow server tracing
                    if (domain.myserver.config !== true) { serverFeatures -= 128; } // Disallow server configuration
                }
                if (obj.db.databaseType != 1) { // If not using NeDB, we can't backup using the simple system.
                    // backup function changed to support all types, only NeDB can be restored through the webinterface
                    // if ((serverFeatures & 1) != 0) { serverFeatures -= 1; } // Disallow server backups
                    if ((serverFeatures & 2) != 0) { serverFeatures -= 2; } // Disallow simple server restore
                }

                // Get WebRTC configuration
                var webRtcConfig = null;
                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'); }
                else if (args.webrtcconfig && (typeof args.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(args.webrtcconfig)).replace(/'/g, '%27'); }

                // Load default page style or new modern ui
                var uiViewMode = 'default';
                var webstateJSON = JSON.parse(webstate);
                if (req.query.sitestyle != null) {
                    if (req.query.sitestyle == 3) { uiViewMode = 'default3'; }
                } else if (webstateJSON && webstateJSON.uiViewMode == 3) {
                    uiViewMode = 'default3';
                } else if (domain.sitestyle == 3) {
                    uiViewMode = 'default3';
                }
                // Refresh the session
                render(dbGetFunc.req, dbGetFunc.res, getRenderPage(uiViewMode, dbGetFunc.req, domain), getRenderArgs({
                    authCookie: authCookie,
                    authRelayCookie: authRelayCookie,
                    viewmode: viewmode,
                    currentNode: currentNode,
                    logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27'),
                    domain: domain.id,
                    debuglevel: parent.debugLevel,
                    serverDnsName: obj.getWebServerName(domain, req),
                    serverRedirPort: args.redirport,
                    serverPublicPort: httpsPort,
                    serverfeatures: serverFeatures,
                    features: allFeatures.features,
                    features2: allFeatures.features2,
                    features3: allFeatures.features3,
                    sessiontime: (args.sessiontime) ? args.sessiontime : 60,
                    mpspass: args.mpspass,
                    passRequirements: passRequirements,
                    customui: customui,
                    customFiles: customFiles,
                    webcerthash: Buffer.from(obj.webCertificateFullHashs[domain.id], 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'),
                    footer: (domain.footer == null) ? '' : obj.common.replacePlaceholders(domain.footer, {
                        'serverversion': obj.parent.currentVer,
                        'servername': obj.getWebServerName(domain, req),
                        'agentsessions': Object.keys(parent.webserver.wsagents).length,
                        'connectedusers': Object.keys(parent.webserver.wssessions).length,
                        'userssessions': Object.keys(parent.webserver.wssessions2).length,
                        'relaysessions': parent.webserver.relaySessionCount,
                        'relaycount': Object.keys(parent.webserver.wsrelays).length
                    }),
                    webstate: encodeURIComponent(webstate).replace(/'/g, '%27'),
                    amtscanoptions: amtscanoptions,
                    pluginHandler: (parent.pluginHandler == null) ? 'null' : parent.pluginHandler.prepExports(),
                    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)),
                    webRelayDns: ((args.relaydns != null) ? args.relaydns[0] : ''),
                    hidePowerTimeline: (domain.hidepowertimeline ? 'true' : 'false'),
                    showNotesPanel: (domain.shownotespanel ? 'true' : 'false'),
                    userSessionsSort: (domain.usersessionssort ? domain.usersessionssort : 'SessionId'),
                    webrtcconfig: webRtcConfig,
                    collapseGroups: (domain.collapsegroups ? 'true' : 'false')
                }, dbGetFunc.req, domain, uiViewMode), user);
            }
            xdbGetFunc.req = req;
            xdbGetFunc.res = res;
            xdbGetFunc.user = user;
            obj.db.Get('ws' + user._id, xdbGetFunc);
        } else {
            // Send back the login application
            // If this is a 2 factor auth request, look for a hardware key challenge.
            // Normal login 2 factor request
            if (req.session && (req.session.loginmode == 4)) {
                const sec = parent.decryptSessionData(req.session.e);
                if ((sec != null) && (typeof sec.tuserid == 'string')) {
                    const user = obj.users[sec.tuserid];
                    if (user != null) {
                        parent.debug('web', 'handleRootRequestEx: sending 2FA challenge.');
                        getHardwareKeyChallenge(req, domain, user, function (hwchallenge) { handleRootRequestLogin(req, res, domain, hwchallenge, passRequirements); });
                        return;
                    }
                }
            }
            // Password recovery 2 factor request
            if (req.session && (req.session.loginmode == 5) && (req.session.temail)) {
                obj.db.GetUserWithVerifiedEmail(domain.id, req.session.temail, function (err, docs) {
                    if ((err != null) || (docs.length == 0)) {
                        parent.debug('web', 'handleRootRequestEx: password recover 2FA fail.');
                        req.session = null;
                        res.redirect(domain.url + getQueryPortion(req)); // BAD***
                    } else {
                        var user = obj.users[docs[0]._id];
                        if (user != null) {
                            parent.debug('web', 'handleRootRequestEx: password recover 2FA challenge.');
                            getHardwareKeyChallenge(req, domain, user, function (hwchallenge) { handleRootRequestLogin(req, res, domain, hwchallenge, passRequirements); });
                        } else {
                            parent.debug('web', 'handleRootRequestEx: password recover 2FA no user.');
                            req.session = null;
                            res.redirect(domain.url + getQueryPortion(req)); // BAD***
                        }
                    }
                });
                return;
            }
            handleRootRequestLogin(req, res, domain, '', passRequirements);
        }
    }

    // Return a list of server supported features for a given domain and user
    obj.getDomainUserFeatures = function (domain, user, req) {
        var features = 0;
        var features2 = 0;
        var features3 = 0;
        if (obj.args.wanonly == true) { features += 0x00000001; } // WAN-only mode
        if (obj.args.lanonly == true) { features += 0x00000002; } // LAN-only mode
        if (obj.args.nousers == true) { features += 0x00000004; } // Single user mode
        if (domain.userQuota == -1) { features += 0x00000008; } // No server files mode
        if (obj.args.mpstlsoffload) { features += 0x00000010; } // No mutual-auth CIRA
        if ((parent.config.settings.allowframing != null) || (domain.allowframing != null) || (parent.config.settings.allowedframingorigins != null) || (domain.allowedframingorigins != null)) { features += 0x00000020; } // Allow site within iframe
        if ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true)) { features += 0x00000040; } // Email invites
        if (obj.args.webrtc == true) { features += 0x00000080; } // Enable WebRTC (Default false for now)
        // 0x00000100 --> This feature flag is free for future use.
        if (obj.args.allowhighqualitydesktop !== false) { features += 0x00000200; } // Enable AllowHighQualityDesktop (Default true)
        if ((obj.args.lanonly == true) || (obj.args.mpsport == 0)) { features += 0x00000400; } // No CIRA
        if ((obj.parent.serverSelfWriteAllowed == true) && (user != null) && ((user.siteadmin & 0x00000010) != 0)) { features += 0x00000800; } // Server can self-write (Allows self-update)
        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
        if (domain.agentnoproxy === true) { features += 0x00002000; } // Indicates that agents should be installed without using a HTTP proxy
        if ((parent.config.settings.no2factorauth !== true) && domain.yubikey && domain.yubikey.id && domain.yubikey.secret && (user._id.split('/')[2][0] != '~')) { features += 0x00004000; } // Indicates Yubikey support
        if (domain.geolocation == true) { features += 0x00008000; } // Enable geo-location features
        if ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true)) { features += 0x00010000; } // Enable password hints
        if (parent.config.settings.no2factorauth !== true) { features += 0x00020000; } // Enable WebAuthn/FIDO2 support
        if ((obj.args.nousers != true) && (domain.passwordrequirements != null) && (domain.passwordrequirements.force2factor === true) && (user._id.split('/')[2][0] != '~')) {
            // Check if we can skip 2nd factor auth because of the source IP address
            var skip2factor = false;
            if ((req != null) && (req.clientIp != null) && (domain.passwordrequirements != null) && (domain.passwordrequirements.skip2factor != null)) {
                for (var i in domain.passwordrequirements.skip2factor) {
                    if (require('ipcheck').match(req.clientIp, domain.passwordrequirements.skip2factor[i]) === true) { skip2factor = true; }
                }
            }
            if (skip2factor == false) { features += 0x00040000; } // Force 2-factor auth
        }
        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.
        if (domain.amtacmactivation) { features += 0x00100000; } // Intel AMT ACM activation/upgrade is possible
        if (domain.usernameisemail) { features += 0x00200000; } // Username is email address
        if (parent.mqttbroker != null) { features += 0x00400000; } // This server supports MQTT channels
        if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null)) { features += 0x00800000; } // using email for 2FA is allowed
        if (domain.agentinvitecodes == true) { features += 0x01000000; } // Support for agent invite codes
        if (parent.smsserver != null) { features += 0x02000000; } // SMS messaging is supported
        if ((parent.smsserver != null) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false))) { features += 0x04000000; } // SMS 2FA is allowed
        if (domain.sessionrecording != null) { features += 0x08000000; } // Server recordings enabled
        if (domain.urlswitching === false) { features += 0x10000000; } // Disables the URL switching feature
        if (domain.novnc === false) { features += 0x20000000; } // Disables noVNC
        if (domain.mstsc === false) { features += 0x40000000; } // Disables MSTSC.js
        if (obj.isTrustedCert(domain) == false) { features += 0x80000000; } // Indicate we are not using a trusted certificate
        if (obj.parent.amtManager != null) { features2 += 0x00000001; } // Indicates that the Intel AMT manager is active
        if (obj.parent.firebase != null) { features2 += 0x00000002; } // Indicates the server supports Firebase push messaging
        if ((obj.parent.firebase != null) && (obj.parent.firebase.pushOnly != true)) { features2 += 0x00000004; } // Indicates the server supports Firebase two-way push messaging
        if (obj.parent.webpush != null) { features2 += 0x00000008; } // Indicates web push is enabled
        if (((obj.args.noagentupdate == 1) || (obj.args.noagentupdate == true))) { features2 += 0x00000010; } // No agent update
        if (parent.amtProvisioningServer != null) { features2 += 0x00000020; } // Intel AMT LAN provisioning server
        if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.push2factor != false)) && (obj.parent.firebase != null)) { features2 += 0x00000040; } // Indicates device push notification 2FA is enabled
        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
        if (req.session.loginToken != null) { features2 += 0x00000100; } // LoginToken mode, no account changes.
        if (domain.ssh == true) { features2 += 0x00000200; } // SSH is enabled
        if (domain.localsessionrecording === false) { features2 += 0x00000400; } // Disable local recording feature
        if (domain.clipboardget == false) { features2 += 0x00000800; } // Disable clipboard get
        if (domain.clipboardset == false) { features2 += 0x00001000; } // Disable clipboard set
        if ((typeof domain.desktop == 'object') && (domain.desktop.viewonly == true)) { features2 += 0x00002000; } // Indicates remote desktop is viewonly
        if (domain.mailserver != null) { features2 += 0x00004000; } // Indicates email server is active
        if (domain.devicesearchbarserverandclientname) { features2 += 0x00008000; } // Search bar will find both server name and client name
        if (domain.ipkvm) { features2 += 0x00010000; } // Indicates support for IP KVM device groups
        if ((domain.passwordrequirements) && (domain.passwordrequirements.otp2factor == false)) { features2 += 0x00020000; } // Indicates support for OTP 2FA is disabled
        if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.backupcode2factor === false)) { features2 += 0x00040000; } // Indicates 2FA backup codes are disabled
        if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.single2factorwarning === false)) { features2 += 0x00080000; } // Indicates no warning if a single 2FA is in use
        if (domain.nightmode === 1) { features2 += 0x00100000; } // Always night mode
        if (domain.nightmode === 2) { features2 += 0x00200000; } // Always day mode
        if (domain.allowsavingdevicecredentials == false) { features2 += 0x00400000; } // Do not allow device credentials to be saved on the server
        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
        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
        if ((parent.msgserver != null) && (parent.msgserver.providers != 0)) { features2 += 0x02000000; } // User messaging server is enabled
        if ((parent.msgserver != null) && (parent.msgserver.providers != 0) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.msg2factor != false))) { features2 += 0x04000000; } // User messaging 2FA is allowed
        if (domain.scrolltotop == true) { features2 += 0x08000000; } // Show the "Scroll to top" button
        if (domain.devicesearchbargroupname === true) { features2 += 0x10000000; } // Search bar will find by group name too
        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
        if (domain.showmodernuitoggle == true) { features2 += 0x40000000; } // Indicates that the new UI should be shown
        if (domain.sitestyle === 3) { features2 |= 0x80000000; } // Indicates that Modern UI is forced (siteStyle = 3)
        if ((typeof domain.desktop == 'object') && (domain.desktop.disableconnectall == true)) { features3 += 0x00000001; } // Disable "Connect All" button when multiple sessions are active on a device
        if (domain.upninsteadofuser === true) { features3 += 0x00000002; } // Show UPN instead of username in General tab
        return { features: features, features2: features2, features3: features3 };
    }

    function handleRootRequestLogin(req, res, domain, hardwareKeyChallenge, passRequirements) {
        parent.debug('web', 'handleRootRequestLogin()');
        var features = 0;
        if ((parent.config != null) && (parent.config.settings != null) && ((parent.config.settings.allowframing == true) || (typeof parent.config.settings.allowframing == 'string') || (parent.config.settings.allowedframingorigins != null) || (domain != null && domain.allowedframingorigins != null))) { features += 32; } // Allow site within iframe
        if (domain.usernameisemail) { features += 0x00200000; } // Username is email address
        var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
        var loginmode = 0;
        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.

        // Format an error message if needed
        var passhint = null, msgid = 0;
        if (req.session != null) {
            msgid = req.session.messageid;
            if ((msgid == 5) || (loginmode == 7) || ((domain.passwordrequirements != null) && (domain.passwordrequirements.hint === true))) { passhint = EscapeHtml(req.session.passhint); }
            delete req.session.messageid;
            delete req.session.passhint;
        }
        const allowAccountReset = ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.allowaccountreset !== false));
        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'))

        // Check if we are allowed to create new users using the login screen
        var newAccountsAllowed = true;
        if ((domain.newaccounts !== 1) && (domain.newaccounts !== true)) { for (var i in obj.users) { if (obj.users[i].domain == domain.id) { newAccountsAllowed = false; break; } } }
        if (parent.config.settings.maintenancemode != null) { newAccountsAllowed = false; }

        // Encrypt the hardware key challenge state if needed
        var hwstate = null;
        if (hardwareKeyChallenge && req.session) {
            const sec = parent.decryptSessionData(req.session.e);
            hwstate = obj.parent.encodeCookie({ u: sec.tuser, p: sec.tpass, c: sec.u2f }, obj.parent.loginCookieEncryptionKey)
        }

        // Check if we can use OTP tokens with email. We can't use email for 2FA password recovery (loginmode 5).
        var otpemail = (loginmode != 5) && (domain.mailserver != null) && (req.session != null) && ((req.session.temail === 1) || (typeof req.session.temail == 'string'));
        if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; }
        var otpduo = (req.session != null) && (req.session.tduo === 1);
        if (((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.duo2factor == false)) || (typeof domain.duo2factor != 'object')) { otpduo = false; }
        var otpsms = (parent.smsserver != null) && (req.session != null) && (req.session.tsms === 1);
        if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; }
        var otpmsg = (parent.msgserver != null) && (req.session != null) && (req.session.tmsg === 1);
        if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.msg2factor == false)) { otpmsg = false; }
        var otppush = (parent.firebase != null) && (req.session != null) && (req.session.tpush === 1);
        if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.push2factor == false)) { otppush = false; }
        const autofido = ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.autofido2fa == true)); // See if FIDO should be automatically prompted if user account has it.

        // See if we support two-factor trusted cookies
        var twoFactorCookieDays = 30;
        if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; }

        // See what authentication strategies we have
        var authStrategies = [];
        if (typeof domain.authstrategies == 'object') {
            if (typeof domain.authstrategies.twitter == 'object') { authStrategies.push('twitter'); }
            if (typeof domain.authstrategies.google == 'object') { authStrategies.push('google'); }
            if (typeof domain.authstrategies.github == 'object') { authStrategies.push('github'); }
            if (typeof domain.authstrategies.azure == 'object') { authStrategies.push('azure'); }
            if (typeof domain.authstrategies.oidc == 'object') {
                if (obj.common.validateObject(domain.authstrategies.oidc.custom) && obj.common.validateString(domain.authstrategies.oidc.custom.preset)) {
                    authStrategies.push('oidc-' + domain.authstrategies.oidc.custom.preset);
                } else {
                    authStrategies.push('oidc');
                }
            }
            if (typeof domain.authstrategies.intel == 'object') { authStrategies.push('intel'); }
            if (typeof domain.authstrategies.jumpcloud == 'object') { authStrategies.push('jumpcloud'); }
            if (typeof domain.authstrategies.saml == 'object') { authStrategies.push('saml'); }
        }

        // Custom user interface
        var customui = '';
        if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); }

        // Custom files (CSS and JS)
        var customFiles = '';
        if (domain.customFiles != null) { 
            customFiles = encodeURIComponent(JSON.stringify(domain.customFiles)); 
        } else if (domain.customfiles != null) {
            customFiles = encodeURIComponent(JSON.stringify(domain.customfiles)); 
        }

        // Get two-factor screen timeout
        var twoFactorTimeout = 300000; // Default is 5 minutes, 0 for no timeout.
        if ((typeof domain.passwordrequirements == 'object') && (typeof domain.passwordrequirements.twofactortimeout == 'number')) {
            twoFactorTimeout = domain.passwordrequirements.twofactortimeout * 1000;
        }

        // Setup CAPTCHA if needed
        var newAccountCaptcha = '', newAccountCaptchaImage = '';
        if ((domain.newaccountscaptcha != null) && (domain.newaccountscaptcha !== false)) {
            newAccountCaptcha = obj.parent.encodeCookie({ type: 'newAccount', captcha: require('svg-captcha').randomText(5) }, obj.parent.loginCookieEncryptionKey);
            newAccountCaptchaImage = 'newAccountCaptcha.ashx?x=' + newAccountCaptcha;
        }

        // Check for flash errors from passport.js and make the array unique
        var flashErrors = [];
        if (req.session.flash && req.session.flash.error) {
            flashErrors = obj.common.uniqueArray(req.session.flash.error);
            req.session.flash = null;
        }

        // Render the login page
        // Allow configurable OIDC login button text via domain.authstrategies.oidc.custom
        var oidcButtonIcon, oidcButtonIcon2x, oidcButtonText;
        if (obj.common.validateObject(domain.authstrategies) && obj.common.validateObject(domain.authstrategies.oidc) && obj.common.validateObject(domain.authstrategies.oidc.custom)) {
            if (obj.common.validateUrl(domain.authstrategies.oidc.custom.buttoniconurl)) {
                oidcButtonIcon = domain.authstrategies.oidc.custom.buttoniconurl;
                if (obj.common.validateUrl(domain.authstrategies.oidc.custom.buttoniconurl2x)) {
                    oidcButtonIcon2x = domain.authstrategies.oidc.custom.buttoniconurl2x + ' 2x';
                } else {
                    oidcButtonIcon2x = domain.authstrategies.oidc.custom.buttoniconurl + ' 2x';
                }
            } else {
                switch (domain.authstrategies.oidc.custom.preset) {
                    case 'azure':
                        oidcButtonIcon = "images/login/azure32.png";
                        oidcButtonIcon2x = "images/login/azure64.png 2x";
                        break;
                    case 'google':
                        oidcButtonIcon = "images/login/google32.png";
                        oidcButtonIcon2x = "images/login/google64.png 2x";
                        break;
                    default:
                        oidcButtonIcon = "images/login/oidc32.png";
                        oidcButtonIcon2x = "images/login/oidc64.png 2x";
                }
            }

            if (obj.common.validateString(domain.authstrategies.oidc.custom.buttontext, 1, 128)) {
                oidcButtonText = domain.authstrategies.oidc.custom.buttontext;
            }
        }
        render(req, res,
            getRenderPage((domain.sitestyle >= 2) ? 'login2' : 'login', req, domain),
            getRenderArgs({
                loginmode: loginmode,
                rootCertLink: getRootCertLink(domain),
                newAccount: newAccountsAllowed, // True if new accounts are allowed from the login page
                newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1), // 1 if new account creation requires password
                newAccountCaptcha: newAccountCaptcha, // If new account creation requires a CAPTCHA, this string will not be empty
                newAccountCaptchaImage: newAccountCaptchaImage, // Set to the URL of the CAPTCHA image
                serverDnsName: obj.getWebServerName(domain, req),
                serverPublicPort: httpsPort,
                passlogin: (typeof domain.showpasswordlogin == 'boolean') ? domain.showpasswordlogin : true,
                emailcheck: emailcheck,
                features: features,
                sessiontime: (args.sessiontime) ? args.sessiontime : 60, // Session time in minutes, 60 minutes is the default
                passRequirements: passRequirements,
                customui: customui,
                customFiles: customFiles,
                footer: (domain.loginfooter == null) ? '' : obj.common.replacePlaceholders(domain.loginfooter, { 
                    'serverversion': obj.parent.currentVer,
                    'servername': obj.getWebServerName(domain, req),
                    'agentsessions': Object.keys(parent.webserver.wsagents).length,
                    'connectedusers': Object.keys(parent.webserver.wssessions).length,
                    'userssessions': Object.keys(parent.webserver.wssessions2).length,
                    'relaysessions': parent.webserver.relaySessionCount,
                    'relaycount': Object.keys(parent.webserver.wsrelays).length
                }),
                hkey: encodeURIComponent(hardwareKeyChallenge).replace(/'/g, '%27'),
                messageid: msgid,
                flashErrors: JSON.stringify(flashErrors).replace(/"/g, '\\"'),
                passhint: passhint,

                welcometext: domain.welcometext ? encodeURIComponent(obj.common.replacePlaceholders(domain.welcometext, {
                    'serverversion': obj.parent.currentVer,
                    'servername': obj.getWebServerName(domain, req),
                    'agentsessions': Object.keys(parent.webserver.wsagents).length,
                    'connectedusers': Object.keys(parent.webserver.wssessions).length,
                    'userssessions': Object.keys(parent.webserver.wssessions2).length,
                    'relaysessions': parent.webserver.relaySessionCount,
                    'relaycount': Object.keys(parent.webserver.wsrelays).length
                })).split('\'').join('\\\'') : null,
                welcomePictureFullScreen: ((typeof domain.welcomepicturefullscreen == 'boolean') ? domain.welcomepicturefullscreen : false),
                hwstate: hwstate,
                otpemail: otpemail,
                otpduo: otpduo,
                otpsms: otpsms,
                otpmsg: otpmsg,
                otppush: otppush,
                autofido: autofido,
                twoFactorCookieDays: twoFactorCookieDays,
                authStrategies: authStrategies.join(','),
                oidcButtonText: oidcButtonText || '',
                oidcButtonIcon: oidcButtonIcon || 'images/login/oidc32.png',
                oidcButtonIcon2x: oidcButtonIcon2x || 'images/login/oidc64.png 2x',
                loginpicture: (typeof domain.loginpicture == 'string'),
                tokenTimeout: twoFactorTimeout, // Two-factor authentication screen timeout in milliseconds,
                renderLanguages: obj.renderLanguages,
                showLanguageSelect: domain.showlanguageselect ? domain.showlanguageselect : false,
            }, req, domain, (domain.sitestyle >= 2) ? 'login2' : 'login'));
    }

    // Handle a post request on the root
    function handleRootPostRequest(req, res) {
        const domain = checkUserIpAddress(req, res);
        if (domain == null) { return; }
        if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.end("Not Found"); return; } // Check 3FA URL key
        if (req.body == null) { req.body = {}; }
        parent.debug('web', 'handleRootPostRequest, action: ' + req.body.action);

        // If a HTTP header is required, check new UserRequiredHttpHeader
        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; } }

        switch (req.body.action) {
            case 'login': { handleLoginRequest(req, res, true); break; }
            case 'tokenlogin': {
                if (req.body.hwstate) {
                    var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 10);
                    if (cookie != null) { req.session.e = parent.encryptSessionData({ tuser: cookie.u, tpass: cookie.p, u2f: cookie.c }); }
                }
                handleLoginRequest(req, res, true); break;
            }
            case 'pushlogin': {
                if (req.body.hwstate) {
                    var cookie = obj.parent.decodeCookie(req.body.hwstate, obj.parent.loginCookieEncryptionKey, 1);
                    if ((cookie != null) && (typeof cookie.u == 'string') && (cookie.d == domain.id) && (cookie.a == 'pushAuth')) {
                        // Push authentication is a success, login the user
                        req.session = { userid: cookie.u };

                        // Check if we need to remember this device
                        if ((req.body.remembertoken === 'on') && ((domain.twofactorcookiedurationdays == null) || (domain.twofactorcookiedurationdays > 0))) {
                            var maxCookieAge = domain.twofactorcookiedurationdays;
                            if (typeof maxCookieAge != 'number') { maxCookieAge = 30; }
                            const twoFactorCookie = obj.parent.encodeCookie({ userid: cookie.u, expire: maxCookieAge * 24 * 60 /*, ip: req.clientIp*/ }, obj.parent.loginCookieEncryptionKey);
                            res.cookie('twofactor', twoFactorCookie, { maxAge: (maxCookieAge * 24 * 60 * 60 * 1000), httpOnly: true, sameSite: parent.config.settings.sessionsamesite, secure: true });
                        }
                        var user = obj.users[cookie.u];
                        // Notify account login
                        var targets = ['*', 'server-users', user._id];
                        if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
                        const ua = obj.getUserAgentInfo(req);
                        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' };
                        obj.parent.DispatchEvent(targets, obj, loginEvent);
                        handleRootRequestEx(req, res, domain);
                        return;
                    }
                }
                handleLoginRequest(req, res, true); break;
            }
            case 'changepassword': { handlePasswordChangeRequest(req, res, true); break; }
            case 'deleteaccount': { handleDeleteAccountRequest(req, res, true); break; }
            case 'createaccount': { handleCreateAccountRequest(req, res, true); break; }
            case 'resetpassword': { handleResetPasswordRequest(req, res, true); break; }
            case 'resetaccount': { handleResetAccountRequest(req, res, true); break; }
            case 'checkemail': { handleCheckAccountEmailRequest(req, res, true); break; }
            default: { handleLoginRequest(req, res, true); break; }
        }
    }

    // Return true if it looks like we are using a real TLS certificate.
    obj.isTrustedCert = function (domain) {
        if ((domain != null) && (typeof domain.trustedcert == 'boolean')) return domain.trustedcert; // If the status of the cert specified, use that.
        if (typeof obj.args.trustedcert == 'boolean') return obj.args.trustedcert; // If the status of the cert specified, use that.
        if (obj.args.tlsoffload != null) return true; // We are using TLS offload, a real cert is likely used.
        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.
        if ((typeof obj.certificates.WebIssuer == 'string') && (obj.certificates.WebIssuer.indexOf('MeshCentralRoot-') == 0)) return false; // Our cert is issued by self-signed cert.
        if (obj.certificates.CommonName.indexOf('.') == -1) return false; // Our cert is named with a fake name
        return true; // This is a guess
    }

    // Get the link to the root certificate if needed
    function getRootCertLink(domain) {
        // Check if the HTTPS certificate is issued from MeshCentralRoot, if so, add download link to root certificate.
        if (obj.isTrustedCert(domain) == false) {
            // Get the domain suffix
            var xdomain = (domain.dns == null) ? domain.id : '';
            if (xdomain != '') xdomain += '/';
            return '<a href=/' + xdomain + 'MeshServerRootCert.cer title="Download the root certificate for this server">Root Certificate</a>';
        }
        return '';
    }

    // Serve the xterm page
    function handleXTermRequest(req, res) {
        const domain = checkUserIpAddress(req, res);
        if (domain == null) { return; }
        if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key

        parent.debug('web', 'handleXTermRequest: sending xterm');
        res.set({ 'Cache-Control': 'no-store' });
        if (req.session && req.session.userid) {
            if (req.session.userid.split('/')[1] != domain.id) { res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the session is for the correct domain
            var user = obj.users[req.session.userid];
            if ((user == null) || (req.query.nodeid == null)) { res.redirect(domain.url + getQueryPortion(req)); return; } // Check if the user exists

            // Check permissions
            obj.GetNodeWithRights(domain, user, req.query.nodeid, function (node, rights, visible) {
                if ((node == null) || ((rights & 8) == 0) || ((rights != 0xFFFFFFFF) && ((rights & 512) != 0))) { res.redirect(domain.url + getQueryPortion(req)); return; }

                var logoutcontrols = { name: user.name };
                var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : '';
                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

                // Create a authentication cookie
                const authCookie = obj.parent.encodeCookie({ userid: user._id, domainid: domain.id, ip: req.clientIp, x: req.session.x }, obj.parent.loginCookieEncryptionKey);
                const authRelayCookie = obj.parent.encodeCookie({ ruserid: user._id, domainid: domain.id }, obj.parent.loginCookieEncryptionKey);
                var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
                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));
            });
        } else {
            res.redirect(domain.url + getQueryPortion(req));
            return;
        }
    }

    // Handle new account Captcha GET
    function handleNewAccountCaptchaRequest(req, res) {
        const domain = checkUserIpAddress(req, res);
        if (domain == null) { return; }
        if ((domain.newaccountscaptcha == null) || (domain.newaccountscaptcha === false) || (req.query.x == null)) { res.sendStatus(404); return; }
        const c = obj.parent.decodeCookie(req.query.x, obj.parent.loginCookieEncryptionKey);
        if ((c == null) || (c.type !== 'newAccount') || (typeof c.captcha != 'string')) { res.sendStatus(404); return; }
        res.type('svg');
        res.status(200).end(require('svg-captcha')(c.captcha, {}));
    }

    // Handle Captcha GET
    function handleCaptchaGetRequest(req, res) {
        const domain = checkUserIpAddress(req, res);
        if (domain == null) { return; }
        if (parent.crowdSecBounser == null) { res.sendStatus(404); return; }
        parent.crowdSecBounser.applyCaptcha(req, res, function () { res.redirect((((domain.id == '') && (domain.dns == null)) ? '/' : ('/' + domain.id))); });
    }

    // Handle Captcha POST
    function handleCaptchaPostRequest(req, res) {
        if (parent.crowdSecBounser == null) { res.sendStatus(404); return; }
        const domain = checkUserIpAddress(req, res);
        if (domain == null) { return; }
        req.originalUrl = (((domain.id == '') && (domain.dns == null)) ? '/' : ('/' + domain.id));
        parent.crowdSecBounser.applyCaptcha(req, res, function () { res.redirect(req.originalUrl); });
    }

    // Render the terms of service.
    function handleTermsRequest(req, res) {
        const domain = checkUserIpAddress(req, res);
        if (domain == null) { return; }
        if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key

        // See if term.txt was loaded from the database
        if ((parent.configurationFiles != null) && (parent.configurationFiles['terms.txt'] != null)) {
            // Send the terms from the database
            res.set({ 'Cache-Control': 'no-store' });
            if (req.session && req.session.userid) {
                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
                var user = obj.users[req.session.userid];
                var logoutcontrols = { name: user.name };
                var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : '';
                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
                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));
            } else {
                render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(parent.configurationFiles['terms.txt'].toString()).split('\'').join('\\\''), logoutControls: encodeURIComponent('{}') }, req, domain));
            }
        } else {
            // See if there is a terms.txt file in meshcentral-data
            var p = obj.path.join(obj.parent.datapath, 'terms.txt');
            if (obj.fs.existsSync(p)) {
                obj.fs.readFile(p, 'utf8', function (err, data) {
                    if (err != null) { parent.debug('web', 'handleTermsRequest: no terms.txt'); res.sendStatus(404); return; }

                    // Send the terms from terms.txt
                    res.set({ 'Cache-Control': 'no-store' });
                    if (req.session && req.session.userid) {
                        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
                        var user = obj.users[req.session.userid];
                        var logoutcontrols = { name: user.name };
                        var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : '';
                        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
                        render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(data).split('\'').join('\\\''), logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain));
                    } else {
                        render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ terms: encodeURIComponent(data).split('\'').join('\\\''), logoutControls: encodeURIComponent('{}') }, req, domain));
                    }
                });
            } else {
                // Send the default terms
                parent.debug('web', 'handleTermsRequest: sending default terms');
                res.set({ 'Cache-Control': 'no-store' });
                if (req.session && req.session.userid) {
                    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
                    var user = obj.users[req.session.userid];
                    var logoutcontrols = { name: user.name };
                    var extras = (req.query.key != null) ? ('&key=' + encodeURIComponent(req.query.key)) : '';
                    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
                    render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ logoutControls: encodeURIComponent(JSON.stringify(logoutcontrols)).replace(/'/g, '%27') }, req, domain));
                } else {
                    render(req, res, getRenderPage('terms', req, domain), getRenderArgs({ logoutControls: encodeURIComponent('{}') }, req, domain));
                }
            }
        }
    }

    // Render the messenger application.
    function handleMessengerRequest(req, res) {
        const domain = getDomain(req);
        if (domain == null) { parent.debug('web', 'handleMessengerRequest: no domain'); res.sendStatus(404); return; }
        parent.debug('web', 'handleMessengerRequest()');

        // Check if we are in maintenance mode
        if (parent.config.settings.maintenancemode != null) {
            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));
            return;
        }

        // Check if this session is for a user
        if (req.query.id == null) { res.sendStatus(404); return; }
        var idSplit = decodeURIComponent(req.query.id).split('/');
        if ((idSplit.length != 7) || (idSplit[0] != 'meshmessenger')) { res.sendStatus(404); return; }
        if ((idSplit[1] == 'user') && (idSplit[4] == 'user')) {
            // This is a user to user conversation, both users must be logged in.
            var user1 = idSplit[1] + '/' + idSplit[2] + '/' + idSplit[3]
            var user2 = idSplit[4] + '/' + idSplit[5] + '/' + idSplit[6]
            if (!req.session || !req.session.userid) {
                // Redirect to login page
                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)); }
                return;
            }
            if ((req.session.userid != user1) && (req.session.userid != user2)) { res.sendStatus(404); return; }
        }

        // Get WebRTC configuration
        var webRtcConfig = null;
        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'); }
        else if (args.webrtcconfig && (typeof args.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(args.webrtcconfig)).replace(/'/g, '%27'); }

        // Setup other options
        var options = { webrtcconfig: webRtcConfig };
        if (typeof domain.meshmessengertitle == 'string') { options.meshMessengerTitle = domain.meshmessengertitle; } else { options.meshMessengerTitle = '!'; }

        // Get the userid and name
        if ((domain.meshmessengertitle != null) && (req.query.id != null) && (req.query.id.startsWith('meshmessenger/node'))) {
            if (idSplit.length == 7) {
                const user = obj.users[idSplit[4] + '/' + idSplit[5] + '/' + idSplit[6]];
                if (user != null) {
                    if (domain.meshmessengertitle.indexOf('{0}') >= 0) { options.username = encodeURIComponent(user.realname ? user.realname : user.name).replace(/'/g, '%27'); }
                    if (domain.meshmessengertitle.indexOf('{1}') >= 0) { options.userid = encodeURIComponent(user.name).replace(/'/g, '%27'); }
                }
            }
        }

        // Render the page
        res.set({ 'Cache-Control': 'no-store' });
        render(req, res, getRenderPage('messenger', req, domain), getRenderArgs(options, req, domain));
    }

    // Handle messenger image request
    function handleMessengerImageRequest(req, res) {
        const domain = getDomain(req);
        if (domain == null) { parent.debug('web', 'handleMessengerImageRequest: no domain'); res.sendStatus(404); return; }
        parent.debug('web', 'handleMessengerImageRequest()');

        // Check if we are in maintenance mode
        if (parent.config.settings.maintenancemode != null) { res.sendStatus(404); return; }

        //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
        if (domain.meshmessengerpicture) {
            // Use the configured messenger logo picture
            try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.meshmessengerpicture)); return; } catch (ex) { }
        }

        var imagefile = 'images/messenger.png';
        if (domain.webpublicpath != null) {
            obj.fs.exists(obj.path.join(domain.webpublicpath, imagefile), function (exists) {
                if (exists) {
                    // Use the domain logo picture
                    try { res.sendFile(obj.path.join(domain.webpublicpath, imagefile)); } catch (ex) { res.sendStatus(404); }
                } else {
                    // Use the default logo picture
                    try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
                }
            });
        } else if (parent.webPublicOverridePath) {
            obj.fs.exists(obj.path.join(obj.parent.webPublicOverridePath, imagefile), function (exists) {
                if (exists) {
                    // Use the override logo picture
                    try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, imagefile)); } catch (ex) { res.sendStatus(404); }
                } else {
                    // Use the default logo picture
                    try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
                }
            });
        } else {
            // Use the default logo picture
            try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
        }
    }

    // Returns the server root certificate encoded in base64
    function getRootCertBase64() {
        var rootcert = obj.certificates.root.cert;
        var i = rootcert.indexOf('-----BEGIN CERTIFICATE-----\r\n');
        if (i >= 0) { rootcert = rootcert.substring(i + 29); }
        i = rootcert.indexOf('-----END CERTIFICATE-----');
        if (i >= 0) { rootcert = rootcert.substring(i, 0); }
        return Buffer.from(rootcert, 'base64').toString('base64');
    }

    // Returns the mesh server root certificate
    function handleRootCertRequest(req, res) {
        const domain = getDomain(req);
        if (domain == null) { parent.debug('web', 'handleRootCertRequest: no domain'); res.sendStatus(404); return; }
        if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key
        if ((obj.userAllowedIp != null) && (checkIpAddressEx(req, res, obj.userAllowedIp, false) === false)) { parent.debug('web', 'handleRootCertRequest: invalid ip'); return; } // Check server-wide IP filter only.
        parent.debug('web', 'handleRootCertRequest()');
        setContentDispositionHeader(res, 'application/octet-stream', certificates.RootName + '.cer', null, 'rootcert.cer');
        res.send(Buffer.from(getRootCertBase64(), 'base64'));
    }

    // Return a customised mainifest.json for PWA
    function handleManifestRequest(req, res){
        const domain = checkUserIpAddress(req, res);
        if (domain == null) { parent.debug('web', 'handleManifestRequest: no domain'); res.sendStatus(404); return; }
        if ((domain.pwa != null) && (domain.pwa.disabled == true)) { parent.debug('web', 'handleManifestRequest: PWA disabled'); res.sendStatus(404); return; }
        const pwa = (domain.pwa != null) ? domain.pwa : {};
        parent.debug('web', 'handleManifestRequest()');
        var manifest = {
            "name": (pwa.name != null) ? pwa.name : ((domain.title != null) ? domain.title : 'MeshCentral'),
            "short_name": (pwa.short_name != null) ? pwa.short_name : ((domain.title != null) ? domain.title : 'MeshCentral'),
            "description": "Open source web based, remote computer management.",
            "scope": ".",
            "start_url": "/",
            "display": (pwa.display != null) ? pwa.display : "fullscreen",
            "orientation": (pwa.orientation != null) ? pwa.orientation : "any",
            "theme_color": (pwa.themecolor != null) ? pwa.themecolor : "#ffffff",
            "background_color": (pwa.backgroundcolor != null) ? pwa.backgroundcolor : "#ffffff",
            "icons": (pwa.icons != null) ? pwa.icons : [{
                "src": "pwalogo.png",
                "sizes": "512x512",
                "type": "image/png"
            }],
            "shortcuts": pwa.shortcuts
        };
        res.json(manifest);
    }

    // Handle user public file downloads
    function handleDownloadUserFiles(req, res) {
        const domain = checkUserIpAddress(req, res);
        if (domain == null) { return; }
        if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key

        if (obj.common.validateString(req.path, 1, 4096) == false) { res.sendStatus(404); return; }
        var domainname = 'domain', spliturl = decodeURIComponent(req.path).split('/'), filename = '';
        if (spliturl[1] != 'userfiles') { spliturl.splice(1,1); } // remove domain.id from url for domains without dns
        if ((spliturl.length < 3) || (obj.common.IsFilenameValid(spliturl[2]) == false) || (domain.userQuota == -1)) { res.sendStatus(404); return; }
        if (domain.id != '') { domainname = 'domain-' + domain.id; }
        var path = obj.path.join(obj.filespath, domainname + '/user-' + spliturl[2] + '/Public');
        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; } }

        var stat = null;
        try { stat = obj.fs.statSync(path); } catch (e) { }
        if ((stat != null) && ((stat.mode & 0x004000) == 0)) {
            if (req.query.download == 1) {
                setContentDispositionHeader(res, 'application/octet-stream', filename, null, 'file.bin');
                try { res.sendFile(obj.path.resolve(__dirname, path)); } catch (e) { res.sendStatus(404); }
            } else {
                // The download page puts the filename inside a JavaScript string (var filename = '...'),
                // so escape backslashes and single quotes to keep it inside that string.
                var filenamejs = filename.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
                var fileurljs = (req.path + '?download=1').replace(/\\/g, '\\\\').replace(/'/g, "\\'");
                render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'download2' : 'download', req, domain), getRenderArgs({ rootCertLink: getRootCertLink(domain), messageid: 1, fileurl: fileurljs, filename: filenamejs, filesize: stat.size }, req, domain));
            }
        } else {
            render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'download2' : 'download', req, domain), getRenderArgs({ rootCertLink: getRootCertLink(domain), messageid: 2 }, req, domain));
        }
    }

    // Handle device file request
    function handleDeviceFile(req, res) {
        const domain = getDomain(req, res);
        if (domain == null) { return; }
        if ((req.method == 'POST') && (req.body != null)) { // The web pages post the download so the cookie stays out of the URL, browser history and proxy logs
            const fields = ['c', 'm', 'n', 'f'];
            for (var i in fields) { if (typeof req.body[fields[i]] == 'string') { req.query[fields[i]] = req.body[fields[i]]; } }
        }
        if ((req.query.c == null) || (req.query.f == null)) { res.sendStatus(404); return; }

        // Check the inbound desktop sharing cookie
        var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 60); // 60 minute timeout
        if ((c == null) || (c.domainid !== domain.id)) { res.sendStatus(404); return; }
        if (checkAuthCookie(c, req) == false) { res.sendStatus(404); return; } // Check the IP and session binding

        // Check userid
        const user = obj.users[c.userid];
        if (user == null) { res.sendStatus(404); return; }
        if (obj.force2factorPending(domain, user, req)) { res.sendStatus(404); return; } // 2FA setup is still required for this account

        if ((c.p != null) && (!Number.isInteger(c.p) || ((c.p & 4) == 0))) { res.sendStatus(404); return; }
        if ((typeof c.expire == 'number') && (c.expire <= Date.now())) { res.sendStatus(404); return; }

        // If this cookie has restricted usages, check that it's allowed to perform downloads
        if (Array.isArray(c.usages) && (c.usages.indexOf(10) < 0)) { res.sendStatus(404); return; } // Check protocol #10

        if (c.nid != null) {
            if ((typeof c.nid != 'string') || (c.nid.split('/').length != 3) || !c.nid.startsWith('node/' + domain.id + '/')) { res.sendStatus(404); return; }
            req.query.n = c.nid.split('/')[2];
        }
        if (req.query.n == null) { res.sendStatus(404); return; }

        // Check if this user has permission to manage this computer
        obj.GetNodeWithRights(domain, user, 'node/' + domain.id + '/' + req.query.n, function (node, rights, visible) {
            if ((node == null) || ((rights & MESHRIGHT_REMOTECONTROL) == 0) || (visible == false)) { res.sendStatus(404); return; } // We don't have remote control rights to this device
            if ((rights != MESHRIGHT_ADMIN) && ((rights & MESHRIGHT_NOFILES) != 0)) { res.sendStatus(404); return; } // This user is not allowed to use files on this device

            if (c.pid != null) {
                obj.db.GetAllTypeNodeFiltered([node._id], domain.id, 'deviceshare', null, function (err, docs) {
                    if ((err != null) || !Array.isArray(docs) || !docs.some(function (share) { return (share.publicid === c.pid) && ((share.extrakey == null) || (share.extrakey === c.k)); })) { res.sendStatus(404); return; }
                    startTransfer();
                });
            } else { startTransfer(); }

            function startTransfer() {
                req.query.id = getRandomLowerCase(12);
                obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, null, res, req, domain, user, node.meshid, node._id, c);
            }
        });
    }

    // Handle download of a server file by an agent
    function handleAgentDownloadFile(req, res) {
        const domain = checkAgentIpAddress(req, res);
        if (domain == null) { return; }
        if (req.query.c == null) { res.sendStatus(404); return; }

        // Check the inbound desktop sharing cookie
        var c = obj.parent.decodeCookie(req.query.c, obj.parent.loginCookieEncryptionKey, 5); // 5 minute timeout
        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; }

        // Send the file back
        try { res.sendFile(obj.path.join(obj.filespath, 'tmp', c.f)); return; } catch (ex) { res.sendStatus(404); }
    }

    // Handle logo request
    function handleLogoRequest(req, res) {
        const domain = checkUserIpAddress(req, res);
        if (domain == null) { return; }

        //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
        if (domain.titlepicture) {
            if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.titlepicture] != null)) {
                // Use the logo in the database
                res.set({ 'Content-Type': domain.titlepicture.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
                res.send(parent.configurationFiles[domain.titlepicture]);
                return;
            } else {
                // Use the logo on file
                try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.titlepicture)); return; } catch (ex) { }
            }
        }

        if ((domain.webpublicpath != null) && (obj.fs.existsSync(obj.path.join(domain.webpublicpath, 'images/logoback.png')))) {
            // Use the domain logo picture
            try { res.sendFile(obj.path.join(domain.webpublicpath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
        } else if (parent.webPublicOverridePath && obj.fs.existsSync(obj.path.join(obj.parent.webPublicOverridePath, 'images/logoback.png'))) {
            // Use the override logo picture
            try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
        } else {
            // Use the default logo picture
            try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'images/logoback.png')); } catch (ex) { res.sendStatus(404); }
        }
    }

    // Handle login logo request
    function handleLoginLogoRequest(req, res) {
        const domain = checkUserIpAddress(req, res);
        if (domain == null) { return; }

        //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
        if (domain.loginpicture) {
            if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.loginpicture] != null)) {
                // Use the logo in the database
                res.set({ 'Content-Type': domain.loginpicture.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
                res.send(parent.configurationFiles[domain.loginpicture]);
                return;
            } else {
                // Use the logo on file
                try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.loginpicture)); return; } catch (ex) { res.sendStatus(404); }
            }
        } else {
            res.sendStatus(404);
        }
    }

    // Handle PWA logo request
    function handlePWALogoRequest(req, res) {
        const domain = checkUserIpAddress(req, res);
        if (domain == null) { return; }

        //res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
        if (domain.pwalogo) {
            if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.pwalogo] != null)) {
                // Use the logo in the database
                res.set({ 'Content-Type': domain.pwalogo.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
                res.send(parent.configurationFiles[domain.pwalogo]);
                return;
            } else {
                // Use the logo on file
                try { res.sendFile(obj.common.joinPath(obj.parent.datapath, domain.pwalogo)); return; } catch (ex) { }
            }
        }

        if ((domain.webpublicpath != null) && (obj.fs.existsSync(obj.path.join(domain.webpublicpath, 'android-chrome-512x512.png')))) {
            // Use the domain logo picture
            try { res.sendFile(obj.path.join(domain.webpublicpath, 'android-chrome-512x512.png')); } catch (ex) { res.sendStatus(404); }
        } else if (parent.webPublicOverridePath && obj.fs.existsSync(obj.path.join(obj.parent.webPublicOverridePath, 'android-chrome-512x512.png'))) {