UNPKG

meshcentral

Version:

Web based remote computer management server

798 lines (725 loc) • 719 kB
/** * @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; } // 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.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(); // 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.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.renderPages = null; obj.renderLanguages = []; obj.destroyedSessions = {}; // userid/req.session.x --> destroyed session time // 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_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 } // 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 } 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) { obj.agentIssues.push([new Date().toLocaleString(), agent.remoteaddrport, issue]); while (obj.setAgentIssue.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;