UNPKG

meshcentral

Version:

Web based remote computer management and file server

772 lines (714 loc) • 415 kB
/** * @description MeshCentral web server * @author Ylian Saint-Hilaire * @copyright Intel Corporation 2018-2020 * @license Apache-2.0 * @version v0.0.1 */ /*jslint node: true */ /*jshint node: true */ /*jshint strict:false */ /*jshint -W097 */ /*jshint esversion: 6 */ 'use strict'; /* class SerialTunnel extends require('stream').Duplex { constructor(options) { super(options); this.forwardwrite = null; } updateBuffer(chunk) { this.push(chunk); } _write(chunk, encoding, callback) { if (this.forwardwrite != null) { this.forwardwrite(chunk); } else { console.err("Failed to fwd _write."); } if (callback) callback(); } // Pass data written to forward _read(size) { } // Push nothing, anything to read should be pushed from updateBuffer() } */ // Older NodeJS does not support the keyword "class", so we do without using this syntax // TODO: Validate that it's the same as above and that it works. 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) { 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.session = require('cookie-session'); 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.meshDesktopMultiplexHandler = require('./meshdesktopmultiplex.js'); obj.meshIderHandler = require('./amt/amt-ider.js'); obj.meshUserHandler = require('./meshuser.js'); obj.interceptor = require('./interceptor'); 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(); } obj.app.use(require('compression')()); 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.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 = []; // Mesh Rights const MESHRIGHT_EDITMESH = 1; const MESHRIGHT_MANAGEUSERS = 2; const MESHRIGHT_MANAGECOMPUTERS = 4; const MESHRIGHT_REMOTECONTROL = 8; const MESHRIGHT_AGENTCONSOLE = 16; const MESHRIGHT_SERVERFILES = 32; const MESHRIGHT_WAKEDEVICE = 64; const MESHRIGHT_SETNOTES = 128; // Site rights const SITERIGHT_SERVERBACKUP = 1; const SITERIGHT_MANAGEUSERS = 2; const SITERIGHT_SERVERRESTORE = 4; const SITERIGHT_FILEACCESS = 8; const SITERIGHT_SERVERUPDATE = 16; const SITERIGHT_LOCKED = 32; // 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: true, offerBasic: false }); } } } // Perform hash on web certificate and agent certificate obj.webCertificateHash = obj.defaultWebCertificateHash = 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 = obj.defaultWebCertificateFullHash = parent.certificateOperations.getCertHashBinary(obj.certificates.web.cert); obj.webCertificateFullHashs = { '': obj.webCertificateFullHash }; 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(); // 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'); } } 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); 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); } else if (i != '') { // For any other domain, use the default cert. obj.webCertificateFullHashs[i] = obj.webCertificateFullHashs['']; obj.webCertificateHashs[i] = obj.webCertificateHashs['']; } } // 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 Multiplexor 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 (domainUserCount[i] == 0) { // If newaccounts is set to no new accounts, but no accounts exists, temporarly 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, 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 } obj.getAgentStats = function () { return obj.agentStats; } // 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 (domain.auth == 'ldap') { if (domain.ldapoptions.url == 'test') { // Fake LDAP login var xxuser = domain.ldapoptions[name.toLowerCase()]; if (xxuser == null) { fn(new Error('invalid password')); return; } else { var username = xxuser['displayName']; if (domain.ldapusername) { username = xxuser[domain.ldapusername]; } var shortname = null; if (domain.ldapuserbinarykey) { // Use a binary key as the userid if (xxuser[domain.ldapuserbinarykey]) { shortname = Buffer.from(xxuser[domain.ldapuserbinarykey], 'binary').toString('hex'); } } 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 (username == null) { fn(new Error('no user name')); return; } if (shortname == null) { fn(new Error('no user identifier')); return; } var userid = 'user/' + domain.id + '/' + shortname; var user = obj.users[userid]; var email = null; if (domain.ldapuseremail) { email = xxuser[domain.ldapuseremail]; } else if (xxuser.mail) { // use default email = xxuser.mail; } if ('[object Array]' == Object.prototype.toString.call(email)) { // mail may be multivalued in ldap in which case, answer is an array. Use the 1st value. email = email[0]; } if (email) { email = email.toLowerCase(); } // it seems some code otherwhere also lowercase the emailaddress. be compatible. if (user == null) { // Create a new user var user = { type: 'user', _id: userid, name: username, creation: Math.floor(Date.now() / 1000), login: 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, 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; obj.db.SetUser(user); var event = { etype: 'user', userid: userid, username: username, account: obj.CloneSafeUser(user), action: 'accountcreate', msg: 'Account created, name is ' + 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 { // This is an existing user // If the display username has changes, update it. if (user.name != username) { user.name = username; obj.db.SetUser(user); var event = { etype: 'user', userid: userid, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Changed account display name to ' + username, 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); } // Check if user email has changed var emailreason = null; if (user.email && !email) { // email unset in ldap => unset delete user.email; delete user.emailVerified; emailreason = 'Unset email (no more email in LDAP)' } else if (user.email != email) { // update email user['email'] = email; user['emailVerified'] = true; emailreason = 'Set account email to ' + email + '. Sync with LDAP.'; } if (emailreason) { obj.db.SetUser(user); var event = { etype: 'user', userid: userid, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: emailreason, 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); } } } else { // LDAP login var LdapAuth = require('ldapauth-fork'); var ldap = new LdapAuth(domain.ldapoptions); ldap.authenticate(name, pass, function (err, xxuser) { try { ldap.close(); } catch (ex) { console.log(ex); } // Close the LDAP object if (err) { fn(new Error('invalid password')); return; } var shortname = null; var email = null; if (domain.ldapuseremail) { email = xxuser[domain.ldapuseremail]; } else if (xxuser.mail) { email = xxuser.mail; } if ('[object Array]' == Object.prototype.toString.call(email)) { // mail may be multivalued in ldap in which case, answer would be an array. Use the 1st one. email = email[0]; } if (email) { email = email.toLowerCase(); } // it seems some code otherwhere also lowercase the emailaddress. be compatible. var username = xxuser['displayName']; if (domain.ldapusername) { username = xxuser[domain.ldapusername]; } 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 (username == null) { fn(new Error('no user name')); return; } if (shortname == null) { fn(new Error('no user identifier')); return; } var userid = 'user/' + domain.id + '/' + shortname; var user = obj.users[userid]; if (user == null) { // This user does not exist, create a new account. var user = { type: 'user', _id: userid, name: shortname, creation: Math.floor(Date.now() / 1000), login: 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, 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; obj.db.SetUser(user); var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountcreate', msg: 'Account created, name is ' + 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 { // This is an existing user // If the display username has changes, update it. if (user.name != username) { user.name = username; obj.db.SetUser(user); var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Changed account display name to ' + username, 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); } // Check if user email has changed var emailreason = null; if (user.email && !email) { // email unset in ldap => unset delete user.email; delete user.emailVerified; emailreason = 'Unset email (no more email in LDAP)' } else if (user.email != email) { // update email user['email'] = email; user['emailVerified'] = true; emailreason = 'Set account email to ' + email + '. Sync with LDAP.'; } if (emailreason) { obj.db.SetUser(user); var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: emailreason, 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); } }); } } 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) { 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) { res.sendStatus(401); } return true; } } } if (closeIfThis === false) { 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) == true)) { obj.blockedUsers++; return null; } if ((parent.config.settings.userallowedip != null) && (checkIpAddressEx(req, res, parent.config.settings.userallowedip, false) == 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) == true)) { obj.blockedUsers++; return null; } if ((domain.userallowedip != null) && (checkIpAddressEx(req, res, domain.userallowedip, false) == 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 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; } // 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 (req.headers.host != null) { var d = obj.dnsDomains[req.headers.host.split(':')[0].toLowerCase()]; if (d != null) return d; } // If this is a DNS name domain, return it here. var x = req.url.split('/'); if (x.length < 2) return parent.config.domains['']; var 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 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 res.set({ 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0' }); // Destroy the user's session to log them out will be re-created next request if (req.session.userid) { var user = obj.users[req.session.userid]; if (user != null) { obj.parent.DispatchEvent(['*'], obj, { etype: 'user', userid: user._id, username: user.name, action: 'logout', msg: 'Account logout', domain: domain.id }); } } req.session = null; if (req.query.key != null) { res.redirect(domain.url + '?key=' + req.query.key); } else { res.redirect(domain.url); } parent.debug('web', 'handleLogoutRequest: success.'); } // Return true if this user has 2-step auth active function checkUserOneTimePasswordRequired(domain, user, req) { // 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 = obj.parent.decodeCookie(decodeURIComponent(cookies[i].substring(10)), obj.parent.loginCookieEncryptionKey, (30 * 24 * 60)); // If the cookies does not have an expire feild, assume 30 day timeout. if ((twoFactorCookie != null) && ((obj.args.cookieipcheck === false) || (twoFactorCookie.ip == null) || (twoFactorCookie.ip === req.clientIp)) && (twoFactorCookie.userid == user._id)) { return false; } } } } // See if SMS 2FA is available var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null)); // Check if a 2nd factor is present return ((parent.config.settings.no2factorauth !== true) && (sms2fa || (user.otpsecret != null) || ((user.email != null) && (user.emailVerified == true) && (parent.mailserver != null) && (user.otpekey != null)) || ((user.otphkeys != null) && (user.otphkeys.length > 0)))); } // 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; }; // Check if we can use OTP tokens with email var otpemail = (parent.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; } // 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); 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); 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) && (authResponse.clientDataJSON)) { // 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 ori