meshcentral
Version:
Web based remote computer management server
798 lines (725 loc) • 719 kB
JavaScript
/**
* @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, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/'/g, '''); if (typeof x == 'boolean') return x; if (typeof x == 'number') return x; }
//function EscapeHtmlBreaks(x) { if (typeof x == "string") return x.replace(/&/g, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/'/g, ''').replace(/\r/g, '<br />').replace(/\n/g, '').replace(/\t/g, ' '); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
// 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;